// Copyright ExByte Studios. Item Interaction Ecosystem. #include "PlacementPreviewComponent.h" #include "ItemPlacementLog.h" #include "ItemDatabaseSubsystem.h" #include "ItemDefinitionRow.h" #include "InventoryComponent.h" #include "PickupItemActor.h" #include "Components/StaticMeshComponent.h" #include "Engine/StaticMesh.h" #include "GameFramework/Pawn.h" #include "GameFramework/PlayerController.h" UPlacementPreviewComponent::UPlacementPreviewComponent() { PrimaryComponentTick.bCanEverTick = true; PrimaryComponentTick.bStartWithTickEnabled = false; SetIsReplicatedByDefault(true); // for the server confirm RPC } bool UPlacementPreviewComponent::GetViewPoint(FVector& OutLoc, FRotator& OutRot) const { const APawn* Pawn = Cast(GetOwner()); const APlayerController* PC = Pawn ? Cast(Pawn->GetController()) : nullptr; if (!PC || !PC->IsLocalController()) { return false; } PC->GetPlayerViewPoint(OutLoc, OutRot); return true; } void UPlacementPreviewComponent::StartPlacement(FGuid InstanceId) { APawn* Pawn = Cast(GetOwner()); UInventoryComponent* Inv = Pawn ? Pawn->FindComponentByClass() : nullptr; if (!Inv) { return; } FItemInstanceData Item; if (!Inv->FindItem(InstanceId, Item)) { return; } const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this); const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Item.ItemId) : nullptr; if (!Def || !Def->bCanPlace) { return; } ItemInstanceId = InstanceId; ItemId = Item.ItemId; bIsPlacing = true; PreviewYaw = 0.f; // Build the local ghost mesh. if (!Ghost) { Ghost = NewObject(GetOwner(), TEXT("PlacementGhost")); Ghost->SetupAttachment(GetOwner()->GetRootComponent()); Ghost->SetCollisionEnabled(ECollisionEnabled::NoCollision); Ghost->SetCastShadow(false); Ghost->RegisterComponent(); } if (!Def->WorldMesh.IsNull()) { if (UStaticMesh* M = Def->WorldMesh.LoadSynchronous()) { Ghost->SetStaticMesh(M); } } Ghost->SetVisibility(true); SetComponentTickEnabled(true); } void UPlacementPreviewComponent::CancelPlacement() { bIsPlacing = false; bCurrentValid = false; SetComponentTickEnabled(false); DestroyGhost(); } void UPlacementPreviewComponent::RotatePreview(float DeltaYawDegrees) { PreviewYaw = FMath::Fmod(PreviewYaw + DeltaYawDegrees, 360.f); } void UPlacementPreviewComponent::ConfirmPlacement() { if (!bIsPlacing || !bCurrentValid) { return; } Server_ConfirmPlacement(ItemInstanceId, PreviewTransform); CancelPlacement(); } void UPlacementPreviewComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if (!bIsPlacing) { return; } FTransform Xf; if (ComputePreviewTransform(Xf)) { PreviewTransform = Xf; if (Ghost) { Ghost->SetWorldTransform(Xf); } const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this); const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(ItemId) : nullptr; FString Reason; const bool bValid = Def && ValidatePlacement(this, *Def, Xf, GetOwner(), Reason); SetValidity(bValid); } else { SetValidity(false); } } bool UPlacementPreviewComponent::ComputePreviewTransform(FTransform& OutTransform) const { FVector ViewLoc; FRotator ViewRot; if (!GetViewPoint(ViewLoc, ViewRot)) { return false; } const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this); const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(ItemId) : nullptr; const float MaxDist = Def ? Def->PlacementRules.MaxDistance : 400.f; const FVector Start = ViewLoc; const FVector End = ViewLoc + ViewRot.Vector() * MaxDist; FCollisionQueryParams Params(SCENE_QUERY_STAT(PlacementTrace), false, GetOwner()); FHitResult Hit; if (!GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Visibility, Params)) { return false; } FRotator Rot(0.f, PreviewYaw, 0.f); // Optionally align to surface normal when not requiring a flat surface. if (Def && !Def->PlacementRules.bRequiresFlatSurface) { const FRotator Aligned = FRotationMatrix::MakeFromZX(Hit.ImpactNormal, ViewRot.Vector()).Rotator(); Rot = FRotator(Aligned.Pitch, PreviewYaw, Aligned.Roll); } OutTransform = FTransform(Rot, Hit.ImpactPoint); return true; } bool UPlacementPreviewComponent::ValidatePlacement(const UObject* WorldContext, const FItemDefinitionRow& Def, const FTransform& Transform, AActor* Instigator, FString& OutReason) { const FItemPlacementRules& Rules = Def.PlacementRules; UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::ReturnNull) : nullptr; if (!World) { OutReason = TEXT("No world"); return false; } // Distance from the instigator. if (Instigator) { const float DistSq = FVector::DistSquared(Instigator->GetActorLocation(), Transform.GetLocation()); if (DistSq > FMath::Square(Rules.MaxDistance + 100.f)) { OutReason = TEXT("Too far"); return false; } } // Slope: trace down and measure the surface normal. { const FVector Loc = Transform.GetLocation(); FCollisionQueryParams Params(SCENE_QUERY_STAT(PlacementSlope), false, Instigator); FHitResult Hit; if (World->LineTraceSingleByChannel(Hit, Loc + FVector(0, 0, 50.f), Loc - FVector(0, 0, 100.f), ECC_Visibility, Params)) { const float SlopeDeg = FMath::RadiansToDegrees(FMath::Acos(FVector::DotProduct(Hit.ImpactNormal, FVector::UpVector))); if (Rules.bCanPlaceOnGround && SlopeDeg > Rules.MaxSlopeAngle) { OutReason = TEXT("Surface too steep"); return false; } } else if (Rules.bRequiresFlatSurface) { OutReason = TEXT("No surface"); return false; } } // Collision: overlap a box at the target; any blocking world geometry rejects. { const FVector Extent(40.f, 40.f, 40.f); FCollisionQueryParams Params(SCENE_QUERY_STAT(PlacementOverlap), false, Instigator); FCollisionShape Box = FCollisionShape::MakeBox(Extent); if (World->OverlapBlockingTestByChannel(Transform.GetLocation() + FVector(0, 0, Extent.Z), FQuat::Identity, ECC_WorldStatic, Box, Params)) { OutReason = TEXT("Blocked"); return false; } } OutReason.Reset(); return true; } void UPlacementPreviewComponent::Server_ConfirmPlacement_Implementation(FGuid InstanceId, FTransform Transform) { APawn* Pawn = Cast(GetOwner()); UInventoryComponent* Inv = Pawn ? Pawn->FindComponentByClass() : nullptr; if (!Inv) { return; } FItemInstanceData Item; if (!Inv->FindItem(InstanceId, Item) || Item.Lock.bLocked) { return; } const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this); const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Item.ItemId) : nullptr; if (!Def || !Def->bCanPlace) { return; } FString Reason; if (!ValidatePlacement(this, *Def, Transform, GetOwner(), Reason)) { UE_LOG(LogItemPlacement, Warning, TEXT("[ServerReject] Placement failed: %s"), *Reason); return; } if (APickupItemActor::SpawnPickupFromInstance(this, Item, Transform)) { Inv->RemoveItem(InstanceId); UE_LOG(LogItemPlacement, Log, TEXT("[Placement] placed %s"), *Item.ItemId.ToString()); } } void UPlacementPreviewComponent::SetValidity(bool bNewValid) { if (bCurrentValid != bNewValid) { bCurrentValid = bNewValid; OnPlacementStateChanged.Broadcast(bNewValid); } } void UPlacementPreviewComponent::DestroyGhost() { if (Ghost) { Ghost->DestroyComponent(); Ghost = nullptr; } } void UPlacementPreviewComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) { DestroyGhost(); Super::EndPlay(EndPlayReason); }