From ca6f0debe992b10a5a64f8ffa2d095822e22f2dc Mon Sep 17 00:00:00 2001 From: "adnan.mujkic" Date: Mon, 14 Sep 2026 09:39:31 +0200 Subject: [PATCH] Planting interaction blocker implemented, ui showing planting mode --- Content/ST_CommonWords.uasset | 4 +- .../Blueprints/MainPlayerController.uasset | 4 +- Content/UI/MainUI.uasset | 4 +- .../Subsystem/InteractionSubsystem.cpp | 110 +++++++++++++++++- .../Public/Subsystem/InteractionSubsystem.h | 33 ++++++ Source/ProjectEleri/Items/SeedBedActor.cpp | 7 -- Source/ProjectEleri/Items/SeedBedActor.h | 1 - .../Private/EleriPlayerController.cpp | 38 ++---- Source/ProjectEleri/Private/MyCharacter.cpp | 9 ++ .../Public/EleriPlayerController.h | 8 -- 10 files changed, 161 insertions(+), 57 deletions(-) diff --git a/Content/ST_CommonWords.uasset b/Content/ST_CommonWords.uasset index 8eb48ed1..9f8c16ae 100644 --- a/Content/ST_CommonWords.uasset +++ b/Content/ST_CommonWords.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:074e1cd41493201fa65a2c3631ce7ecc030728bf13d916a219a66c71b9e5f430 -size 6364 +oid sha256:8f0b0775219e4124dc75a5434beab2786f7aea02323125ba6b2f9676fd3427f5 +size 6622 diff --git a/Content/ThirdPersonBP/Blueprints/MainPlayerController.uasset b/Content/ThirdPersonBP/Blueprints/MainPlayerController.uasset index 6c851246..7ed5f101 100644 --- a/Content/ThirdPersonBP/Blueprints/MainPlayerController.uasset +++ b/Content/ThirdPersonBP/Blueprints/MainPlayerController.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:590049ccaf27c690fdb492d457300d7ddce3ce238dd298c4ec6829d6cb1c600e -size 75379 +oid sha256:4724be011ecf01af59f7e553c1e357971982171cd9e51aea8e51fada2d5ccafc +size 74975 diff --git a/Content/UI/MainUI.uasset b/Content/UI/MainUI.uasset index eebc36e4..c719b568 100644 --- a/Content/UI/MainUI.uasset +++ b/Content/UI/MainUI.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b15994b484fdf508395371ff106b93956b975c2a609bb1d1c9b6716a5400e5c5 -size 622457 +oid sha256:303176577e50e1dad9eefc1dff0505cc870bc557ea315a057280a384a0cbb4c1 +size 637990 diff --git a/Plugins/InteractionSystem/Source/InteractionSystem/Private/Subsystem/InteractionSubsystem.cpp b/Plugins/InteractionSystem/Source/InteractionSystem/Private/Subsystem/InteractionSubsystem.cpp index 4cdb3dc5..c801d81a 100644 --- a/Plugins/InteractionSystem/Source/InteractionSystem/Private/Subsystem/InteractionSubsystem.cpp +++ b/Plugins/InteractionSystem/Source/InteractionSystem/Private/Subsystem/InteractionSubsystem.cpp @@ -21,6 +21,7 @@ void UInteractionSubsystem::Initialize(FSubsystemCollectionBase& Collection) void UInteractionSubsystem::Deinitialize() { InteractableActorList.Empty(); + InteractionUIBlockers.Empty(); if (UInteractionSubsystem* IS = GetWorld()->GetSubsystem()) { @@ -104,19 +105,23 @@ AActor* UInteractionSubsystem::GetClosestInteractable(const UObject* WorldContex Closest = Actor.Get(); } - if (IS->CurrentlyInteractableObject != Closest) + // UI-only gate: blocking never changes what this function returns to gameplay code, + // it only decides which actor is allowed to show the interaction UI. + AActor* UIActor = IS->HasInteractionUIBlocks() ? nullptr : Closest; + + if (IS->CurrentlyInteractableObject.Get() != UIActor) { if (IS->CurrentlyInteractableObject.IsValid()) { // UE_LOG(LogTemp, Warning, TEXT("Turning off last interactable: %s"), *IS->CurrentlyInteractableObject.Get()->GetActorNameOrLabel()) IInteractableActorInterface::Execute_ToggleInteractableUI(IS->CurrentlyInteractableObject.Get(), false); } - if (Closest) + if (UIActor) { - // UE_LOG(LogTemp, Warning, TEXT("Turning ON interactable: %s"), *Closest->GetActorNameOrLabel()) - IInteractableActorInterface::Execute_ToggleInteractableUI(Closest, true); + // UE_LOG(LogTemp, Warning, TEXT("Turning ON interactable: %s"), *UIActor->GetActorNameOrLabel()) + IInteractableActorInterface::Execute_ToggleInteractableUI(UIActor, true); } - IS->CurrentlyInteractableObject = Closest; + IS->CurrentlyInteractableObject = UIActor; } return Closest; @@ -131,9 +136,104 @@ void UInteractionSubsystem::UpdateInteractableText(AActor* Interactable) if (InteractionSubsystem->CurrentlyInteractableObject != Interactable) return; + // The interaction UI is being held hidden by a blocker - leave it alone. + if (InteractionSubsystem->HasInteractionUIBlocks()) return; + IInteractableActorInterface::Execute_ToggleInteractableUI(InteractionSubsystem->CurrentlyInteractableObject.Get(), true); } +void UInteractionSubsystem::AddInteractionUIBlock(const UObject* WorldContextObject, UObject* Blocker) +{ + if (!IsValid(WorldContextObject) || !IsValid(WorldContextObject->GetWorld()) || !IsValid(Blocker)) + { + return; + } + + UInteractionSubsystem* IS = WorldContextObject->GetWorld()->GetSubsystem(); + if (!IS) + { + return; + } + + // Drop dead entries before touching the array so a recycled weak pointer can never be "revived". + IS->PruneInteractionUIBlockers(); + + const bool bAlreadyBlocking = IS->InteractionUIBlockers.ContainsByPredicate( + [Blocker](const TWeakObjectPtr& Entry) { return Entry.Get() == Blocker; }); + + if (bAlreadyBlocking) + { + return; + } + + const TWeakObjectPtr BlockerRef(Blocker); + IS->InteractionUIBlockers.Add(BlockerRef); + + UE_LOG(LogTemp, Verbose, TEXT("Interaction UI blocked by %s (%d blocker(s) active)"), + *Blocker->GetName(), IS->InteractionUIBlockers.Num()); + + // A widget might be on screen right now, take it down instead of waiting for the next tick. + IS->HideCurrentInteractableUI(); +} + +void UInteractionSubsystem::RemoveInteractionUIBlock(const UObject* WorldContextObject, UObject* Blocker) +{ + if (!IsValid(WorldContextObject) || !IsValid(WorldContextObject->GetWorld()) || !IsValid(Blocker)) + { + return; + } + + UInteractionSubsystem* IS = WorldContextObject->GetWorld()->GetSubsystem(); + if (!IS) + { + return; + } + + IS->InteractionUIBlockers.RemoveAll( + [Blocker](const TWeakObjectPtr& Entry) { return Entry.Get() == Blocker; }); + + // Nothing else to do - the next Tick re-evaluates the closest interactable and brings the UI back. +} + +bool UInteractionSubsystem::IsInteractionUIBlocked(const UObject* WorldContextObject) +{ + if (!IsValid(WorldContextObject) || !IsValid(WorldContextObject->GetWorld())) + { + return false; + } + + UInteractionSubsystem* IS = WorldContextObject->GetWorld()->GetSubsystem(); + return IS ? IS->HasInteractionUIBlocks() : false; +} + +bool UInteractionSubsystem::HasInteractionUIBlocks() const +{ + for (const TWeakObjectPtr& Blocker : InteractionUIBlockers) + { + if (Blocker.IsValid()) + { + return true; + } + } + + return false; +} + +void UInteractionSubsystem::PruneInteractionUIBlockers() +{ + InteractionUIBlockers.RemoveAll([](const TWeakObjectPtr& Entry) { return !Entry.IsValid(); }); +} + +void UInteractionSubsystem::HideCurrentInteractableUI() +{ + if (CurrentlyInteractableObject.IsValid()) + { + IInteractableActorInterface::Execute_ToggleInteractableUI(CurrentlyInteractableObject.Get(), false); + } + + CurrentlyInteractableObject = nullptr; +} + void UInteractionSubsystem::RescanWorld() { UWorld* World = GetWorld(); diff --git a/Plugins/InteractionSystem/Source/InteractionSystem/Public/Subsystem/InteractionSubsystem.h b/Plugins/InteractionSystem/Source/InteractionSystem/Public/Subsystem/InteractionSubsystem.h index 4879132b..d67e9766 100644 --- a/Plugins/InteractionSystem/Source/InteractionSystem/Public/Subsystem/InteractionSubsystem.h +++ b/Plugins/InteractionSystem/Source/InteractionSystem/Public/Subsystem/InteractionSubsystem.h @@ -32,6 +32,22 @@ public: UFUNCTION(BlueprintCallable, Category = "Interactable") static void UpdateInteractableText(AActor* Interactable); + /** + * Keeps the interaction UI hidden until the same Blocker calls RemoveInteractionUIBlock. + * Only the UI is affected - GetClosestInteractable() and Execute_Interact() keep working normally, + * so gameplay interaction is never disabled by this. + * Any number of blockers can be registered at the same time; the UI only comes back once the + * last one has released it. + */ + UFUNCTION(BlueprintCallable, Category = "Interactable|UI", meta = (WorldContext = "WorldContextObject")) + static void AddInteractionUIBlock(const UObject* WorldContextObject, UObject* Blocker); + + UFUNCTION(BlueprintCallable, Category = "Interactable|UI", meta = (WorldContext = "WorldContextObject")) + static void RemoveInteractionUIBlock(const UObject* WorldContextObject, UObject* Blocker); + + UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Interactable|UI", meta = (WorldContext = "WorldContextObject")) + static bool IsInteractionUIBlocked(const UObject* WorldContextObject); + protected: UFUNCTION() @@ -40,9 +56,26 @@ protected: void RegisterInteractableActor(AActor* InteractableActor); void UnregisterInteractableActor(AActor* InteractableActor); + /** True while at least one still-alive blocker is registered */ + bool HasInteractionUIBlocks() const; + + /** Drops blockers that have been destroyed, so a stale entry can never keep the UI hidden forever */ + void PruneInteractionUIBlockers(); + + /** Immediately takes down whatever interaction UI is currently shown */ + void HideCurrentInteractableUI(); + UPROPERTY() TArray> InteractableActorList; UPROPERTY() TWeakObjectPtr CurrentlyInteractableObject; + + /** + * Objects currently holding the interaction UI hidden. + * Weak on purpose: if a blocker is destroyed without releasing, the entry stops counting by itself. + * Deliberately not a UPROPERTY - TWeakObjectPtr needs no GC reflection and this array must never + * keep a blocker alive. + */ + TArray> InteractionUIBlockers; }; diff --git a/Source/ProjectEleri/Items/SeedBedActor.cpp b/Source/ProjectEleri/Items/SeedBedActor.cpp index d96f7163..1376921a 100644 --- a/Source/ProjectEleri/Items/SeedBedActor.cpp +++ b/Source/ProjectEleri/Items/SeedBedActor.cpp @@ -153,13 +153,6 @@ void ASeedBedActor::EndPlay(const EEndPlayReason::Type EndPlayReason) Super::EndPlay(EndPlayReason); } -void ASeedBedActor::OnConstruction(const FTransform& Transform) -{ - Super::OnConstruction(Transform); - - RefreshAllSlotVisuals(); -} - void ASeedBedActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); diff --git a/Source/ProjectEleri/Items/SeedBedActor.h b/Source/ProjectEleri/Items/SeedBedActor.h index 250f0549..8b6fe8fc 100644 --- a/Source/ProjectEleri/Items/SeedBedActor.h +++ b/Source/ProjectEleri/Items/SeedBedActor.h @@ -213,7 +213,6 @@ public: protected: virtual void BeginPlay() override; virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; - virtual void OnConstruction(const FTransform& Transform) override; /** Creates (or rebuilds) the components of a single slot and adds it to the slot map */ FSeedBedSeedSlot& CreateSlot(FIntVector2 SlotCoordinate, bool bRebuildIfExisting); diff --git a/Source/ProjectEleri/Private/EleriPlayerController.cpp b/Source/ProjectEleri/Private/EleriPlayerController.cpp index be63d7b3..bff6baa2 100644 --- a/Source/ProjectEleri/Private/EleriPlayerController.cpp +++ b/Source/ProjectEleri/Private/EleriPlayerController.cpp @@ -190,11 +190,6 @@ void AEleriPlayerController::SetupInputComponent() PlayerEnhancedInputComponent->BindAction(WaterSeedAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnHarvestSeed); } - if (PlantSeedAction) - { - PlayerEnhancedInputComponent->BindAction(PlantSeedAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnPlantSeedAction); - } - if (UiBackAction) { PlayerEnhancedInputComponent->BindAction(UiBackAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnControllerBackAction); @@ -1132,7 +1127,10 @@ void AEleriPlayerController::OnInteract(const FInputActionValue &Value) return; if (PlayerCharacter->GetIsPlanting()) + { TryPlantSelectedSeed(); + return; + } // Ask the InteractionSystem plugin's subsystem for the closest valid interactable // and fire the interaction on it if we have one. @@ -1149,7 +1147,11 @@ void AEleriPlayerController::OnInteractExit(const FInputActionValue &Value) if (!IsValid(PlayerCharacter)) return; - // if (PlayerCharacter->GetIsPlanting()) PlayerCharacter->PlantSeed(); + if (PlayerCharacter->GetIsPlanting()) + { + ClearSeedToPlant(); + return; + } PlayerCharacter->OnInteractExit.Broadcast(); OnChangeInputContext.Broadcast(1); @@ -1200,22 +1202,6 @@ void AEleriPlayerController::SetPlantingMode(bool bActive) return; PlayerCharacter->SetPlanting(bActive); - - // Planting gets its own context so it can take a key over without touching the other contexts - if (IsValid(PlantingContext)) - { - if (UEnhancedInputLocalPlayerSubsystem *Subsystem = ULocalPlayer::GetSubsystem(GetLocalPlayer())) - { - if (bActive) - { - Subsystem->AddMappingContext(PlantingContext, 6); - } - else - { - Subsystem->RemoveMappingContext(PlantingContext); - } - } - } } void AEleriPlayerController::TryPlantSelectedSeed() @@ -1260,14 +1246,6 @@ void AEleriPlayerController::TryPlantSelectedSeed() } } -void AEleriPlayerController::OnPlantSeedAction(const FInputActionValue &Value) -{ - if (!IsValid(PlayerCharacter)) - return; - - TryPlantSelectedSeed(); -} - void AEleriPlayerController::StartMinigame(EMinigameType MinigameType, const TSoftObjectPtr &DataAsset) { if (!IsValid(PlayerCharacter) || diff --git a/Source/ProjectEleri/Private/MyCharacter.cpp b/Source/ProjectEleri/Private/MyCharacter.cpp index a4b154fe..490171a4 100644 --- a/Source/ProjectEleri/Private/MyCharacter.cpp +++ b/Source/ProjectEleri/Private/MyCharacter.cpp @@ -18,6 +18,7 @@ #include "Components/StatForgeComponent.h" #include "GameFramework/SpringArmComponent.h" #include "ProjectEleri/Items/SeedBedActor.h" +#include "Subsystem/InteractionSubsystem.h" #if WITH_EDITOR @@ -339,6 +340,10 @@ void AMyCharacter::SetPlanting(bool bActive) if (bIsPlanting) { + // Planting takes over the interact input, so keep the plugin's interaction UI out of the way. + // The plugin knows nothing about planting - it just holds/releases a generic UI block. + UInteractionSubsystem::AddInteractionUIBlock(this, this); + SeedBedActors.Empty(); TArray OutActors; UGameplayStatics::GetAllActorsOfClass(this, ASeedBedActor::StaticClass(), OutActors); @@ -349,12 +354,16 @@ void AMyCharacter::SetPlanting(bool bActive) } else { + UInteractionSubsystem::RemoveInteractionUIBlock(this, this); + if (LastClosestSeedBedActor) { LastClosestSeedBedActor->StopHighlightingSlot(); LastClosestSeedBedActor = nullptr; } } + + PlayerController->GetMainGameWidget()->SwitchKeybindsDisplay(bActive ? 6 : 1); } void AMyCharacter::BeginWatering() diff --git a/Source/ProjectEleri/Public/EleriPlayerController.h b/Source/ProjectEleri/Public/EleriPlayerController.h index 699e0a76..d6b8cbcd 100644 --- a/Source/ProjectEleri/Public/EleriPlayerController.h +++ b/Source/ProjectEleri/Public/EleriPlayerController.h @@ -225,14 +225,6 @@ public: UPROPERTY(EditDefaultsOnly, Category = "Action Input") UInputAction* WaterSeedAction; - /** Action used to plant the selected seed into the closest seed bed slot */ - UPROPERTY(EditDefaultsOnly, Category = "Action Input") - UInputAction* PlantSeedAction; - - /** Mapping context pushed while planting, it may only contain actions that planting needs */ - UPROPERTY(EditDefaultsOnly, Category = "Action Input") - UInputMappingContext* PlantingContext; - UPROPERTY(EditDefaultsOnly, Category = "Book Input") UInputAction* BookNextPageAction; UPROPERTY(EditDefaultsOnly, Category = "Book Input")