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>
283 lines
7.5 KiB
C++
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);
|
|
}
|