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>
207 lines
5.1 KiB
C++
207 lines
5.1 KiB
C++
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
|
|
|
#include "PickupItemActor.h"
|
|
#include "ItemWorldLog.h"
|
|
#include "InteractableComponent.h"
|
|
#include "ItemDatabaseSubsystem.h"
|
|
#include "ItemDefinitionRow.h"
|
|
#include "Components/StaticMeshComponent.h"
|
|
#include "Engine/StaticMesh.h"
|
|
#include "Net/UnrealNetwork.h"
|
|
|
|
APickupItemActor::APickupItemActor()
|
|
{
|
|
bReplicates = true;
|
|
SetReplicateMovement(true);
|
|
PrimaryActorTick.bCanEverTick = false;
|
|
|
|
Root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
|
|
SetRootComponent(Root);
|
|
|
|
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
|
|
Mesh->SetupAttachment(Root);
|
|
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
|
Mesh->SetCollisionResponseToAllChannels(ECR_Block);
|
|
Mesh->SetCollisionResponseToChannel(ECC_Pawn, ECR_Overlap);
|
|
|
|
Interactable = CreateDefaultSubobject<UInteractableComponent>(TEXT("Interactable"));
|
|
}
|
|
|
|
void APickupItemActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
|
{
|
|
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
|
DOREPLIFETIME(APickupItemActor, ItemData);
|
|
DOREPLIFETIME(APickupItemActor, bIsLocked);
|
|
DOREPLIFETIME(APickupItemActor, CurrentHolder);
|
|
}
|
|
|
|
void APickupItemActor::BeginPlay()
|
|
{
|
|
Super::BeginPlay();
|
|
RefreshVisuals();
|
|
}
|
|
|
|
void APickupItemActor::InitializeFromItem(const FItemInstanceData& InItem)
|
|
{
|
|
if (!HasAuthority())
|
|
{
|
|
return;
|
|
}
|
|
ItemData = InItem;
|
|
ItemData.EnsureInstanceId();
|
|
ItemData.LocationType = EItemLocationType::World;
|
|
RefreshVisuals();
|
|
}
|
|
|
|
APickupItemActor* APickupItemActor::SpawnPickup(UObject* WorldContext, FName ItemId, int32 Quantity, const FTransform& Transform)
|
|
{
|
|
UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext);
|
|
if (!DB || !DB->HasItem(ItemId))
|
|
{
|
|
UE_LOG(LogItemWorld, Warning, TEXT("SpawnPickup: unknown item '%s'."), *ItemId.ToString());
|
|
return nullptr;
|
|
}
|
|
const FItemInstanceData Item = DB->MakeItemInstance(ItemId, Quantity);
|
|
return SpawnPickupFromInstance(WorldContext, Item, Transform);
|
|
}
|
|
|
|
APickupItemActor* APickupItemActor::SpawnPickupFromInstance(UObject* WorldContext, const FItemInstanceData& Item, const FTransform& Transform)
|
|
{
|
|
UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::ReturnNull) : nullptr;
|
|
if (!World)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
// Prefer the item's authored pickup actor class; fall back to the base class.
|
|
TSubclassOf<AActor> SpawnClass = APickupItemActor::StaticClass();
|
|
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext))
|
|
{
|
|
if (const FItemDefinitionRow* Def = DB->GetItemDefinition(Item.ItemId))
|
|
{
|
|
if (!Def->PickupActorClass.IsNull())
|
|
{
|
|
if (UClass* Loaded = Def->PickupActorClass.LoadSynchronous())
|
|
{
|
|
if (Loaded->IsChildOf(APickupItemActor::StaticClass()))
|
|
{
|
|
SpawnClass = Loaded;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FActorSpawnParameters Params;
|
|
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
|
|
APickupItemActor* Pickup = World->SpawnActor<APickupItemActor>(SpawnClass, Transform, Params);
|
|
if (Pickup)
|
|
{
|
|
Pickup->InitializeFromItem(Item);
|
|
}
|
|
return Pickup;
|
|
}
|
|
|
|
bool APickupItemActor::GetProvidedItem(FItemInstanceData& OutItem) const
|
|
{
|
|
if (!ItemData.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
OutItem = ItemData;
|
|
return true;
|
|
}
|
|
|
|
bool APickupItemActor::IsItemAvailable() const
|
|
{
|
|
return !bIsLocked && ItemData.IsValid();
|
|
}
|
|
|
|
void APickupItemActor::NotifyItemTaken(AActor* Taker)
|
|
{
|
|
if (!HasAuthority())
|
|
{
|
|
return;
|
|
}
|
|
UE_LOG(LogItemWorld, Verbose, TEXT("Pickup %s taken by %s; destroying."), *GetName(), *GetNameSafe(Taker));
|
|
Destroy();
|
|
}
|
|
|
|
void APickupItemActor::UpdateProvidedItem(const FItemInstanceData& RemainingItem)
|
|
{
|
|
if (!HasAuthority())
|
|
{
|
|
return;
|
|
}
|
|
if (RemainingItem.Quantity <= 0)
|
|
{
|
|
Destroy();
|
|
return;
|
|
}
|
|
ItemData = RemainingItem;
|
|
ItemData.LocationType = EItemLocationType::World;
|
|
RefreshVisuals();
|
|
}
|
|
|
|
void APickupItemActor::SetCarried(AActor* Holder, bool bCarried)
|
|
{
|
|
if (!HasAuthority())
|
|
{
|
|
return;
|
|
}
|
|
bIsLocked = bCarried;
|
|
CurrentHolder = bCarried ? Holder : nullptr;
|
|
if (Mesh)
|
|
{
|
|
Mesh->SetSimulatePhysics(false);
|
|
Mesh->SetCollisionEnabled(bCarried ? ECollisionEnabled::NoCollision : ECollisionEnabled::QueryAndPhysics);
|
|
}
|
|
}
|
|
|
|
void APickupItemActor::ReleaseToWorld(const FTransform& WorldTransform)
|
|
{
|
|
if (!HasAuthority())
|
|
{
|
|
return;
|
|
}
|
|
DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
|
|
SetActorTransform(WorldTransform);
|
|
bIsLocked = false;
|
|
CurrentHolder = nullptr;
|
|
if (Mesh)
|
|
{
|
|
Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
|
|
}
|
|
if (ItemData.IsValid())
|
|
{
|
|
ItemData.LocationType = EItemLocationType::World;
|
|
}
|
|
}
|
|
|
|
void APickupItemActor::OnRep_ItemData()
|
|
{
|
|
RefreshVisuals();
|
|
}
|
|
|
|
void APickupItemActor::RefreshVisuals()
|
|
{
|
|
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
|
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(ItemData.ItemId) : nullptr;
|
|
if (!Def)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Mesh && !Def->WorldMesh.IsNull())
|
|
{
|
|
if (UStaticMesh* LoadedMesh = Def->WorldMesh.LoadSynchronous())
|
|
{
|
|
Mesh->SetStaticMesh(LoadedMesh);
|
|
}
|
|
}
|
|
if (Interactable && Interactable->DisplayName.IsEmpty())
|
|
{
|
|
Interactable->DisplayName = Def->DisplayName;
|
|
}
|
|
}
|