Files
inventory-system-plugin/Source/ItemPlacement/Private/PlacementPreviewComponent.cpp
Bonchellon 7f7e043a88 Initial commit: Item Interaction Ecosystem plugin (UE5.7)
Server-authoritative, data-driven item/inventory/interaction/carry/equipment/
world-slot/placement/crafting framework. 8 modules, compiles against UE 5.7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:49:56 +03:00

283 lines
7.5 KiB
C++

// 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<APawn>(GetOwner());
const APlayerController* PC = Pawn ? Cast<APlayerController>(Pawn->GetController()) : nullptr;
if (!PC || !PC->IsLocalController())
{
return false;
}
PC->GetPlayerViewPoint(OutLoc, OutRot);
return true;
}
void UPlacementPreviewComponent::StartPlacement(FGuid InstanceId)
{
APawn* Pawn = Cast<APawn>(GetOwner());
UInventoryComponent* Inv = Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : 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<UStaticMeshComponent>(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<APawn>(GetOwner());
UInventoryComponent* Inv = Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : 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);
}