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>
This commit is contained in:
Bonchellon
2026-06-22 20:49:56 +03:00
commit 7f7e043a88
98 changed files with 9328 additions and 0 deletions

View File

@ -0,0 +1,150 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#include "EquipmentComponent.h"
#include "ItemWorldLog.h"
#include "ItemDatabaseSubsystem.h"
#include "ItemDefinitionRow.h"
#include "InventoryComponent.h"
#include "GameFramework/Character.h"
#include "Components/SkeletalMeshComponent.h"
#include "Net/UnrealNetwork.h"
UEquipmentComponent::UEquipmentComponent()
{
PrimaryComponentTick.bCanEverTick = false;
SetIsReplicatedByDefault(true);
}
void UEquipmentComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UEquipmentComponent, Equipped);
}
void UEquipmentComponent::OnUnregister()
{
if (HasAuthority())
{
for (const FEquippedItemEntry& Entry : Equipped)
{
if (Entry.EquippedActor)
{
Entry.EquippedActor->Destroy();
}
}
}
Super::OnUnregister();
}
bool UEquipmentComponent::HasAuthority() const
{
return GetOwner() && GetOwner()->HasAuthority();
}
USceneComponent* UEquipmentComponent::GetAttachTarget() const
{
if (const ACharacter* Char = Cast<ACharacter>(GetOwner()))
{
return Char->GetMesh();
}
return GetOwner() ? GetOwner()->GetRootComponent() : nullptr;
}
FName UEquipmentComponent::ResolveSocket(const FGameplayTag& SlotTag) const
{
if (const FName* Found = SlotSockets.Find(SlotTag))
{
return *Found;
}
return DefaultEquipSocket;
}
bool UEquipmentComponent::IsSlotEquipped(FGameplayTag SlotTag) const
{
return Equipped.ContainsByPredicate([&SlotTag](const FEquippedItemEntry& E) { return E.SlotTag == SlotTag; });
}
bool UEquipmentComponent::ServerEquip(FGuid InstanceId, FGameplayTag SlotTag)
{
if (!HasAuthority())
{
return false;
}
UInventoryComponent* Inv = GetOwner()->FindComponentByClass<UInventoryComponent>();
if (!Inv)
{
return false;
}
FItemInstanceData Item;
if (!Inv->FindItem(InstanceId, Item))
{
return false;
}
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Item.ItemId) : nullptr;
if (!Def || !Def->bCanEquip)
{
return false;
}
// Slot compatibility (if the item restricts equipment slots).
if (!Def->AllowedEquipmentSlots.IsEmpty() && !Def->AllowedEquipmentSlots.HasTag(SlotTag))
{
return false;
}
// Free the slot first.
ServerUnequip(SlotTag);
FEquippedItemEntry Entry;
Entry.SlotTag = SlotTag;
Entry.InstanceId = InstanceId;
Entry.ItemId = Item.ItemId;
if (!Def->EquippedActorClass.IsNull())
{
if (UClass* ActorClass = Def->EquippedActorClass.LoadSynchronous())
{
FActorSpawnParameters Params;
Params.Owner = GetOwner();
Params.Instigator = Cast<APawn>(GetOwner());
AActor* Spawned = GetWorld()->SpawnActor<AActor>(ActorClass, FTransform::Identity, Params);
if (Spawned)
{
Spawned->AttachToComponent(GetAttachTarget(), FAttachmentTransformRules::SnapToTargetIncludingScale, ResolveSocket(SlotTag));
Entry.EquippedActor = Spawned;
}
}
}
Equipped.Add(Entry);
OnEquipmentChanged.Broadcast();
UE_LOG(LogItemWorld, Log, TEXT("[Inventory] equipped %s in %s"), *Item.ItemId.ToString(), *SlotTag.ToString());
return true;
}
bool UEquipmentComponent::ServerUnequip(FGameplayTag SlotTag)
{
if (!HasAuthority())
{
return false;
}
const int32 Index = Equipped.IndexOfByPredicate([&SlotTag](const FEquippedItemEntry& E) { return E.SlotTag == SlotTag; });
if (Index == INDEX_NONE)
{
return false;
}
if (Equipped[Index].EquippedActor)
{
Equipped[Index].EquippedActor->Destroy();
}
Equipped.RemoveAt(Index);
OnEquipmentChanged.Broadcast();
return true;
}
void UEquipmentComponent::OnRep_Equipped()
{
OnEquipmentChanged.Broadcast();
}

View File

@ -0,0 +1,50 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#include "FuelTankComponent.h"
#include "ItemWorldLog.h"
#include "ItemNativeTags.h"
#include "Net/UnrealNetwork.h"
UFuelTankComponent::UFuelTankComponent()
{
PrimaryComponentTick.bCanEverTick = false;
SetIsReplicatedByDefault(true);
}
void UFuelTankComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UFuelTankComponent, CurrentFuel);
}
bool UFuelTankComponent::HasAuthority() const
{
return GetOwner() && GetOwner()->HasAuthority();
}
bool UFuelTankComponent::ReceiveHeldUse(FName ResultVerb, FItemInstanceData& HeldItem, AActor* Instigator)
{
if (!HasAuthority() || ResultVerb != FName("TransferFuel"))
{
return false;
}
const float Available = HeldItem.RuntimeProperties.GetFloat(ItemEcosystemTags::Property_FuelAmount, 0.f);
const float Room = FMath::Max(0.f, MaxFuel - CurrentFuel);
const float Moved = FMath::Min3(PourAmount, Available, Room);
if (Moved <= 0.f)
{
return false;
}
CurrentFuel += Moved;
HeldItem.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_FuelAmount, Available - Moved);
OnFuelChanged.Broadcast(CurrentFuel);
UE_LOG(LogItemWorld, Log, TEXT("[Interaction] poured %.1f fuel (now %.1f/%.1f)"), Moved, CurrentFuel, MaxFuel);
return true;
}
void UFuelTankComponent::OnRep_Fuel()
{
OnFuelChanged.Broadcast(CurrentFuel);
}

View File

@ -0,0 +1,336 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#include "HeldItemComponent.h"
#include "PickupItemActor.h"
#include "ItemWorldLog.h"
#include "ItemNativeTags.h"
#include "ItemDatabaseSubsystem.h"
#include "ItemDefinitionRow.h"
#include "ItemAuxiliaryRows.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/PrimitiveComponent.h"
#include "PhysicsEngine/PhysicsHandleComponent.h"
#include "Net/UnrealNetwork.h"
UHeldItemComponent::UHeldItemComponent()
{
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = false;
SetIsReplicatedByDefault(true);
}
void UHeldItemComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (HeldItem.Mode == EHeldItemMode::CarryPhysicsHandle && HasAuthority())
{
UpdatePhysicsCarryTarget();
}
}
bool UHeldItemComponent::ServerHoldWorldItemPhysics(AActor* WorldItem)
{
if (!HasAuthority() || HeldItem.bHasHeldItem || !WorldItem)
{
return false;
}
IWorldItemProvider* Provider = Cast<IWorldItemProvider>(WorldItem);
FItemInstanceData Item;
if (!Provider || !Provider->IsItemAvailable() || !Provider->GetProvidedItem(Item) || !CanHoldItem(Item))
{
return false;
}
UPrimitiveComponent* Prim = nullptr;
if (APickupItemActor* Pickup = Cast<APickupItemActor>(WorldItem))
{
Prim = Pickup->GetMeshComponent();
}
if (!Prim)
{
Prim = Cast<UPrimitiveComponent>(WorldItem->GetRootComponent());
}
if (!Prim)
{
return false;
}
// Claim the item but keep physics enabled (unlike attached carry).
Provider->SetCarried(GetOwner(), true);
Prim->SetSimulatePhysics(true);
Prim->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
if (!PhysicsHandle)
{
PhysicsHandle = NewObject<UPhysicsHandleComponent>(this, TEXT("CarryPhysicsHandle"));
PhysicsHandle->RegisterComponent();
}
PhysicsHandle->GrabComponentAtLocation(Prim, NAME_None, Prim->GetComponentLocation());
HeldItem.bHasHeldItem = true;
HeldItem.InstanceId = Item.InstanceId;
HeldItem.ItemId = Item.ItemId;
HeldItem.Mode = EHeldItemMode::CarryPhysicsHandle;
HeldItem.HeldWorldActor = WorldItem;
HeldItem.MoveSpeedMultiplier = 0.9f;
HeldItem.Item = Item;
HeldItem.Item.LocationType = EItemLocationType::Held;
ApplyMovementState();
SetComponentTickEnabled(true);
OnHeldItemChanged.Broadcast(HeldItem);
return true;
}
void UHeldItemComponent::UpdatePhysicsCarryTarget()
{
if (!PhysicsHandle || !PhysicsHandle->GetGrabbedComponent() || !GetOwner())
{
return;
}
FRotator ViewRot = GetOwner()->GetActorRotation();
if (const APawn* Pawn = Cast<APawn>(GetOwner()))
{
ViewRot = Pawn->GetControlRotation();
}
const FVector Target = GetOwner()->GetActorLocation() + ViewRot.Vector() * PhysicsHoldDistance + FVector(0, 0, 50.f);
PhysicsHandle->SetTargetLocationAndRotation(Target, ViewRot);
}
void UHeldItemComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UHeldItemComponent, HeldItem);
}
bool UHeldItemComponent::HasAuthority() const
{
return GetOwner() && GetOwner()->HasAuthority();
}
bool UHeldItemComponent::CanHoldItem(const FItemInstanceData& Item) const
{
if (HeldItem.bHasHeldItem || !Item.IsValid())
{
return false;
}
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
{
if (const FItemDefinitionRow* Def = DB->GetItemDefinition(Item.ItemId))
{
return Def->bCanWorldCarry;
}
}
return false;
}
bool UHeldItemComponent::ServerHoldWorldItem(AActor* WorldItem)
{
if (!HasAuthority() || HeldItem.bHasHeldItem || !WorldItem)
{
return false;
}
IWorldItemProvider* Provider = Cast<IWorldItemProvider>(WorldItem);
if (!Provider || !Provider->IsItemAvailable())
{
return false;
}
FItemInstanceData Item;
if (!Provider->GetProvidedItem(Item) || !CanHoldItem(Item))
{
return false;
}
// Attach the world actor to the character's carry socket.
USceneComponent* AttachParent = nullptr;
if (const ACharacter* Char = Cast<ACharacter>(GetOwner()))
{
AttachParent = Char->GetMesh();
}
if (!AttachParent)
{
AttachParent = GetOwner()->GetRootComponent();
}
WorldItem->AttachToComponent(AttachParent, FAttachmentTransformRules::SnapToTargetIncludingScale, CarrySocket);
Provider->SetCarried(GetOwner(), true);
// Resolve carry move-speed multiplier from the feature table.
float MoveMult = 1.f;
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
{
TArray<FItemFeatureRow> Features;
DB->GetFeatureRows(Item.ItemId, Features);
for (const FItemFeatureRow& Row : Features)
{
if (Row.Param == FName("MoveSpeedMultiplier"))
{
MoveMult = Row.GetFloat(1.f);
break;
}
}
}
HeldItem.bHasHeldItem = true;
HeldItem.InstanceId = Item.InstanceId;
HeldItem.ItemId = Item.ItemId;
HeldItem.Mode = EHeldItemMode::CarryAttached;
HeldItem.HeldWorldActor = WorldItem;
HeldItem.HoldSocket = CarrySocket;
HeldItem.MoveSpeedMultiplier = MoveMult;
HeldItem.bBlocksSprint = MoveMult < 0.9f;
HeldItem.Item = Item;
HeldItem.Item.LocationType = EItemLocationType::Held;
ApplyMovementState();
OnHeldItemChanged.Broadcast(HeldItem);
UE_LOG(LogItemWorld, Log, TEXT("[Interaction] %s carried %s"), *GetNameSafe(GetOwner()), *Item.ItemId.ToString());
return true;
}
void UHeldItemComponent::RequestDrop()
{
Server_Drop();
}
void UHeldItemComponent::Server_Drop_Implementation()
{
ServerDropHeldItem();
}
bool UHeldItemComponent::ServerDropHeldItem()
{
if (!HasAuthority() || !HeldItem.bHasHeldItem)
{
return false;
}
const FVector Fwd = GetOwner()->GetActorForwardVector();
const FTransform DropXf(GetOwner()->GetActorRotation(),
GetOwner()->GetActorLocation() + Fwd * 100.f + FVector(0, 0, 20.f));
if (APickupItemActor* Pickup = Cast<APickupItemActor>(HeldItem.HeldWorldActor))
{
Pickup->ReleaseToWorld(DropXf);
}
else
{
APickupItemActor::SpawnPickupFromInstance(this, HeldItem.Item, DropXf);
if (HeldItem.HeldWorldActor)
{
HeldItem.HeldWorldActor->Destroy();
}
}
// NoiseOnDrop feature: emit a hearing stimulus (section 21.6).
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
{
TArray<FItemFeatureRow> Noise;
DB->GetFeatureRows(HeldItem.ItemId, ItemEcosystemTags::Feature_NoiseOnDrop, Noise);
for (const FItemFeatureRow& Row : Noise)
{
if (Row.Param == FName("Radius"))
{
const float Radius = Row.GetFloat(0.f);
if (Radius > 0.f)
{
GetOwner()->MakeNoise(1.f, Cast<APawn>(GetOwner()), DropXf.GetLocation(), Radius);
}
break;
}
}
}
if (PhysicsHandle && PhysicsHandle->GetGrabbedComponent()) { PhysicsHandle->ReleaseComponent(); }
SetComponentTickEnabled(false);
HeldItem = FHeldItemState();
ApplyMovementState();
OnHeldItemChanged.Broadcast(HeldItem);
return true;
}
bool UHeldItemComponent::ServerConsumeHeldItem(FItemInstanceData& OutItem)
{
if (!HasAuthority() || !HeldItem.bHasHeldItem)
{
return false;
}
OutItem = HeldItem.Item;
if (HeldItem.HeldWorldActor)
{
HeldItem.HeldWorldActor->Destroy();
}
if (PhysicsHandle && PhysicsHandle->GetGrabbedComponent()) { PhysicsHandle->ReleaseComponent(); }
SetComponentTickEnabled(false);
HeldItem = FHeldItemState();
ApplyMovementState();
OnHeldItemChanged.Broadcast(HeldItem);
return true;
}
bool UHeldItemComponent::ServerHandOffHeldItem(FItemInstanceData& OutItem, AActor*& OutActor)
{
if (!HasAuthority() || !HeldItem.bHasHeldItem)
{
return false;
}
OutItem = HeldItem.Item;
OutActor = HeldItem.HeldWorldActor;
if (OutActor)
{
OutActor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
}
if (PhysicsHandle && PhysicsHandle->GetGrabbedComponent()) { PhysicsHandle->ReleaseComponent(); }
SetComponentTickEnabled(false);
HeldItem = FHeldItemState();
ApplyMovementState();
OnHeldItemChanged.Broadcast(HeldItem);
return true;
}
bool UHeldItemComponent::ServerUpdateHeldItem(const FItemInstanceData& NewItem)
{
if (!HasAuthority() || !HeldItem.bHasHeldItem || NewItem.InstanceId != HeldItem.InstanceId)
{
return false;
}
HeldItem.Item = NewItem;
HeldItem.Item.LocationType = EItemLocationType::Held;
OnHeldItemChanged.Broadcast(HeldItem);
return true;
}
bool UHeldItemComponent::GetHeldItem(FItemInstanceData& OutItem) const
{
if (!HeldItem.bHasHeldItem)
{
return false;
}
OutItem = HeldItem.Item;
return true;
}
void UHeldItemComponent::OnRep_HeldItem(FHeldItemState /*OldState*/)
{
ApplyMovementState();
OnHeldItemChanged.Broadcast(HeldItem);
}
void UHeldItemComponent::ApplyMovementState()
{
ACharacter* Char = Cast<ACharacter>(GetOwner());
UCharacterMovementComponent* Move = Char ? Char->GetCharacterMovement() : nullptr;
if (!Move)
{
return;
}
// Cache the unmodified speed once, then re-derive from it each time.
if (CachedBaseWalkSpeed <= 0.f)
{
CachedBaseWalkSpeed = Move->MaxWalkSpeed;
}
const float Mult = HeldItem.bHasHeldItem ? HeldItem.MoveSpeedMultiplier : 1.f;
Move->MaxWalkSpeed = CachedBaseWalkSpeed * Mult;
}

View File

@ -0,0 +1,8 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#include "Modules/ModuleManager.h"
#include "ItemWorldLog.h"
DEFINE_LOG_CATEGORY(LogItemWorld);
IMPLEMENT_MODULE(FDefaultModuleImpl, ItemWorld)

View File

@ -0,0 +1,206 @@
// 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;
}
}

View File

@ -0,0 +1,21 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#include "StorageContainerActor.h"
#include "InteractableComponent.h"
#include "InventoryComponent.h"
#include "Components/StaticMeshComponent.h"
AStorageContainerActor::AStorageContainerActor()
{
bReplicates = true;
PrimaryActorTick.bCanEverTick = false;
Mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("Mesh"));
SetRootComponent(Mesh);
Interactable = CreateDefaultSubobject<UInteractableComponent>(TEXT("Interactable"));
Storage = CreateDefaultSubobject<UInventoryComponent>(TEXT("Storage"));
// Shared container: visible to whoever opens it, not just an owning client.
Storage->bOwnerOnlyReplication = false;
}

View File

@ -0,0 +1,106 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#include "WorldInteractionActions.h"
#include "ItemInteractionComponent.h"
#include "ItemNativeTags.h"
#include "GameFramework/Character.h"
namespace
{
bool RouteToComponent(const FInteractionContext& Context, EItemInteractionMode Mode, const FGameplayTag& ActionTag)
{
if (!Context.Character)
{
return false;
}
if (UItemInteractionComponent* Comp = Context.Character->FindComponentByClass<UItemInteractionComponent>())
{
return Comp->ServerExecuteMode(Context, Mode, ActionTag);
}
return false;
}
}
UInteractionAction_CarryAttached::UInteractionAction_CarryAttached()
{
ActionTag = ItemEcosystemTags::Action_Carry;
Mode = EItemInteractionMode::CarryAttached;
Priority = 500;
DisplayText = NSLOCTEXT("ItemWorld", "Carry", "Carry");
}
void UInteractionAction_CarryAttached::Execute_Implementation(const FInteractionContext& Context)
{
RouteToComponent(Context, Mode, ActionTag);
}
UInteractionAction_CarryPhysics::UInteractionAction_CarryPhysics()
{
ActionTag = ItemEcosystemTags::Action_Carry;
Mode = EItemInteractionMode::CarryPhysicsHandle;
Priority = 500;
DisplayText = NSLOCTEXT("ItemWorld", "CarryPhysics", "Pick Up");
}
void UInteractionAction_CarryPhysics::Execute_Implementation(const FInteractionContext& Context)
{
RouteToComponent(Context, Mode, ActionTag);
}
UInteractionAction_Equip::UInteractionAction_Equip()
{
ActionTag = ItemEcosystemTags::Action_Equip;
Mode = EItemInteractionMode::EquipFromInventory;
Priority = 800;
DisplayText = NSLOCTEXT("ItemWorld", "Equip", "Equip");
}
void UInteractionAction_Equip::Execute_Implementation(const FInteractionContext& Context)
{
RouteToComponent(Context, Mode, ActionTag);
}
UInteractionAction_DropHeldItem::UInteractionAction_DropHeldItem()
{
ActionTag = ItemEcosystemTags::Action_Drop;
Mode = EItemInteractionMode::DropHeldItem;
Priority = 300;
DisplayText = NSLOCTEXT("ItemWorld", "Drop", "Drop");
}
void UInteractionAction_DropHeldItem::Execute_Implementation(const FInteractionContext& Context)
{
RouteToComponent(Context, Mode, ActionTag);
}
UInteractionAction_InstallIntoWorldSlot::UInteractionAction_InstallIntoWorldSlot()
{
ActionTag = ItemEcosystemTags::Action_Install;
Mode = EItemInteractionMode::InstallIntoWorldSlot;
Priority = 900;
DisplayText = NSLOCTEXT("ItemWorld", "Install", "Install");
}
void UInteractionAction_InstallIntoWorldSlot::Execute_Implementation(const FInteractionContext& Context)
{
RouteToComponent(Context, Mode, ActionTag);
}
UInteractionAction_RemoveFromWorldSlot::UInteractionAction_RemoveFromWorldSlot()
{
ActionTag = ItemEcosystemTags::Action_Remove;
Mode = EItemInteractionMode::RemoveFromWorldSlot;
Priority = 400;
DisplayText = NSLOCTEXT("ItemWorld", "Remove", "Remove");
}
void UInteractionAction_RemoveFromWorldSlot::Execute_Implementation(const FInteractionContext& Context)
{
RouteToComponent(Context, Mode, ActionTag);
}
UInteractionAction_UseHeldOnTarget::UInteractionAction_UseHeldOnTarget()
{
ActionTag = ItemEcosystemTags::Action_UseHeldOnTarget;
Mode = EItemInteractionMode::UseHeldOnTarget;
Priority = 800;
DisplayText = NSLOCTEXT("ItemWorld", "Use", "Use");
}
void UInteractionAction_UseHeldOnTarget::Execute_Implementation(const FInteractionContext& Context)
{
RouteToComponent(Context, Mode, ActionTag);
}

View File

@ -0,0 +1,261 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#include "WorldInteractionComponent.h"
#include "HeldItemComponent.h"
#include "EquipmentComponent.h"
#include "WorldItemSlotComponent.h"
#include "PickupItemActor.h"
#include "ItemWorldLog.h"
#include "InventoryComponent.h"
#include "ItemDatabaseSubsystem.h"
#include "ItemDefinitionRow.h"
#include "ItemAuxiliaryRows.h"
#include "ItemInteractionInterfaces.h"
#include "GameFramework/Pawn.h"
bool UWorldInteractionComponent::ExecuteAuthorized(const FInteractionContext& Context, const FInteractionOption& Option, const FInteractionRequest& Request)
{
switch (Option.Mode)
{
case EItemInteractionMode::CarryAttached:
return ExecuteCarry(Context, /*bPhysics*/ false);
case EItemInteractionMode::CarryPhysicsHandle:
return ExecuteCarry(Context, /*bPhysics*/ true);
case EItemInteractionMode::DropHeldItem:
return ExecuteDropHeld(Context);
case EItemInteractionMode::InstallIntoWorldSlot:
return ExecuteInstall(Context);
case EItemInteractionMode::RemoveFromWorldSlot:
return ExecuteRemoveFromSlot(Context);
case EItemInteractionMode::EquipFromInventory:
return ExecuteEquip(Context, Request);
case EItemInteractionMode::UseHeldOnTarget:
return ExecuteUseHeldOnTarget(Context, Option);
default:
return Super::ExecuteAuthorized(Context, Option, Request);
}
}
bool UWorldInteractionComponent::ExecuteCarry(const FInteractionContext& Context, bool bPhysics)
{
UHeldItemComponent* Held = GetOwner()->FindComponentByClass<UHeldItemComponent>();
if (!Held || !Context.TargetActor)
{
return false;
}
return bPhysics ? Held->ServerHoldWorldItemPhysics(Context.TargetActor)
: Held->ServerHoldWorldItem(Context.TargetActor);
}
bool UWorldInteractionComponent::ExecuteDropHeld(const FInteractionContext& Context)
{
UHeldItemComponent* Held = GetOwner()->FindComponentByClass<UHeldItemComponent>();
return Held && Held->ServerDropHeldItem();
}
bool UWorldInteractionComponent::ExecuteInstall(const FInteractionContext& Context)
{
UHeldItemComponent* Held = GetOwner()->FindComponentByClass<UHeldItemComponent>();
if (!Held || !Held->HasHeldItem() || !Context.TargetActor)
{
return false;
}
FItemInstanceData HeldItem;
Held->GetHeldItem(HeldItem);
// Find a compatible, free slot on the target.
UWorldItemSlotComponent* Chosen = nullptr;
TArray<UWorldItemSlotComponent*> Slots;
Context.TargetActor->GetComponents<UWorldItemSlotComponent>(Slots);
for (UWorldItemSlotComponent* Slot : Slots)
{
if (Slot && Slot->CanInstall(HeldItem))
{
Chosen = Slot;
break;
}
}
if (!Chosen)
{
UE_LOG(LogItemWorld, Warning, TEXT("[ServerReject] Install failed: no compatible free slot."));
return false;
}
// Hand the carried actor over to the slot as its visual.
FItemInstanceData Item;
AActor* Visual = nullptr;
if (!Held->ServerHandOffHeldItem(Item, Visual))
{
return false;
}
if (!Chosen->InstallItem(Item, Visual))
{
// Roll back: drop the item back to the world so it is not lost.
const FTransform Xf(GetOwner()->GetActorRotation(), GetOwner()->GetActorLocation() + GetOwner()->GetActorForwardVector() * 100.f);
if (APickupItemActor* Pickup = Cast<APickupItemActor>(Visual))
{
Pickup->ReleaseToWorld(Xf);
}
else
{
APickupItemActor::SpawnPickupFromInstance(this, Item, Xf);
if (Visual) { Visual->Destroy(); }
}
return false;
}
return true;
}
bool UWorldInteractionComponent::ExecuteEquip(const FInteractionContext& Context, const FInteractionRequest& Request)
{
UEquipmentComponent* Equip = GetOwner()->FindComponentByClass<UEquipmentComponent>();
if (!Equip)
{
return false;
}
FGameplayTag SlotTag = Request.TargetSlotTag;
if (!SlotTag.IsValid() && Context.PlayerInventory)
{
// Fall back to the item's first allowed equipment slot.
FItemInstanceData Item;
if (Context.PlayerInventory->FindItem(Request.ItemInstanceId, Item))
{
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
{
if (const FItemDefinitionRow* Def = DB->GetItemDefinition(Item.ItemId))
{
if (!Def->AllowedEquipmentSlots.IsEmpty())
{
SlotTag = Def->AllowedEquipmentSlots.First();
}
}
}
}
}
return Equip->ServerEquip(Request.ItemInstanceId, SlotTag);
}
bool UWorldInteractionComponent::ExecuteUseHeldOnTarget(const FInteractionContext& Context, const FInteractionOption& Option)
{
UHeldItemComponent* Held = GetOwner()->FindComponentByClass<UHeldItemComponent>();
if (!Held || !Held->HasHeldItem() || !Context.TargetActor)
{
return false;
}
FItemInstanceData HeldItem;
Held->GetHeldItem(HeldItem);
// Find the result verb from the data-driven interaction row for this held item.
FName ResultVerb = NAME_None;
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
{
TArray<FItemInteractionRow> Rows;
DB->GetInteractionRows(HeldItem.ItemId, Rows);
for (const FItemInteractionRow& Row : Rows)
{
if (Row.ActionTag == Option.ActionTag) { ResultVerb = Row.Result; break; }
}
}
if (ResultVerb.IsNone())
{
return false;
}
// Locate an IItemUseTarget on the target actor or its components.
IItemUseTarget* UseTarget = Cast<IItemUseTarget>(Context.TargetActor);
if (!UseTarget)
{
for (UActorComponent* Comp : Context.TargetActor->GetComponents())
{
if (IItemUseTarget* AsTarget = Cast<IItemUseTarget>(Comp)) { UseTarget = AsTarget; break; }
}
}
if (!UseTarget)
{
return false;
}
if (UseTarget->ReceiveHeldUse(ResultVerb, HeldItem, GetOwner()))
{
Held->ServerUpdateHeldItem(HeldItem);
return true;
}
return false;
}
bool UWorldInteractionComponent::ExecuteRemoveFromSlot(const FInteractionContext& Context)
{
if (!Context.TargetActor)
{
return false;
}
// Find the first occupied slot on the target.
UWorldItemSlotComponent* Occupied = nullptr;
TArray<UWorldItemSlotComponent*> Slots;
Context.TargetActor->GetComponents<UWorldItemSlotComponent>(Slots);
for (UWorldItemSlotComponent* Slot : Slots)
{
if (Slot && Slot->IsOccupied()) { Occupied = Slot; break; }
}
if (!Occupied)
{
return false;
}
FItemInstanceData Item;
if (!Occupied->RemoveItem(Item))
{
return false;
}
// Prefer the player's inventory; otherwise drop it at their feet.
UInventoryComponent* Inv = Context.PlayerInventory;
if (Inv && Inv->AddItem(Item) && Item.Quantity <= 0)
{
return true;
}
const FTransform Xf(GetOwner()->GetActorRotation(), GetOwner()->GetActorLocation() + GetOwner()->GetActorForwardVector() * 120.f + FVector(0, 0, 30.f));
APickupItemActor::SpawnPickupFromInstance(this, Item, Xf);
return true;
}
bool UWorldInteractionComponent::HandleDropFromInventory(FGuid InstanceId)
{
APawn* Pawn = Cast<APawn>(GetOwner());
UInventoryComponent* Inv = Pawn ? Pawn->FindComponentByClass<UInventoryComponent>() : nullptr;
if (!Inv)
{
return false;
}
FItemInstanceData Item;
if (!Inv->FindItem(InstanceId, Item) || Item.Lock.bLocked)
{
return false;
}
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Item.ItemId) : nullptr;
if (Def && !Def->bCanDrop)
{
UE_LOG(LogItemWorld, Warning, TEXT("[ServerReject] Drop failed: %s cannot be dropped."), *Item.ItemId.ToString());
return false;
}
const FTransform Xf(GetOwner()->GetActorRotation(), GetOwner()->GetActorLocation() + GetOwner()->GetActorForwardVector() * 120.f + FVector(0, 0, 20.f));
if (APickupItemActor::SpawnPickupFromInstance(this, Item, Xf))
{
Inv->RemoveItem(InstanceId);
UE_LOG(LogItemWorld, Log, TEXT("[Inventory] dropped %s"), *Item.ItemId.ToString());
return true;
}
return false;
}

View File

@ -0,0 +1,129 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#include "WorldItemSlotComponent.h"
#include "PickupItemActor.h"
#include "ItemWorldLog.h"
#include "ItemDatabaseSubsystem.h"
#include "ItemDefinitionRow.h"
#include "Net/UnrealNetwork.h"
UWorldItemSlotComponent::UWorldItemSlotComponent()
{
PrimaryComponentTick.bCanEverTick = false;
SetIsReplicatedByDefault(true);
}
void UWorldItemSlotComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UWorldItemSlotComponent, InstalledItem);
DOREPLIFETIME(UWorldItemSlotComponent, InstalledVisual);
}
bool UWorldItemSlotComponent::HasAuthority() const
{
return GetOwner() && GetOwner()->HasAuthority();
}
USceneComponent* UWorldItemSlotComponent::GetAttachTarget() const
{
return GetOwner() ? GetOwner()->GetRootComponent() : nullptr;
}
bool UWorldItemSlotComponent::CanInstall(const FItemInstanceData& Item) const
{
if (InstalledItem.bHasItem || !Item.IsValid())
{
return false;
}
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Item.ItemId) : nullptr;
if (!Def || !Def->bCanInstallIntoWorldSlot)
{
return false;
}
// The item must declare this slot as compatible.
if (!Def->CompatibleWorldSlots.HasTag(SlotTag))
{
return false;
}
// Optional extra tag gate.
if (!AcceptedItemTags.IsEmpty())
{
FGameplayTagContainer ItemTags = Def->ItemTags;
ItemTags.AddTag(Def->ItemType);
if (!ItemTags.HasAny(AcceptedItemTags))
{
return false;
}
}
return true;
}
bool UWorldItemSlotComponent::InstallItem(const FItemInstanceData& Item, AActor* ProvidedVisual)
{
if (!HasAuthority() || !CanInstall(Item))
{
return false;
}
InstalledItem.bHasItem = true;
InstalledItem.Item = Item;
InstalledItem.Item.LocationType = EItemLocationType::Installed;
if (ProvidedVisual)
{
// Reuse the carried actor as the visual.
if (USceneComponent* Target = GetAttachTarget())
{
ProvidedVisual->AttachToComponent(Target, FAttachmentTransformRules::SnapToTargetIncludingScale, AttachSocketName);
}
if (IWorldItemProvider* Provider = Cast<IWorldItemProvider>(ProvidedVisual))
{
Provider->SetCarried(GetOwner(), true); // keep collision off, locked
}
InstalledVisual = ProvidedVisual;
}
else
{
// Spawn a fresh visual pickup and attach it.
const FTransform Xf = GetAttachTarget() ? GetAttachTarget()->GetComponentTransform() : GetOwner()->GetActorTransform();
if (APickupItemActor* Visual = APickupItemActor::SpawnPickupFromInstance(this, Item, Xf))
{
Visual->SetCarried(GetOwner(), true);
if (USceneComponent* Target = GetAttachTarget())
{
Visual->AttachToComponent(Target, FAttachmentTransformRules::SnapToTargetIncludingScale, AttachSocketName);
}
InstalledVisual = Visual;
}
}
OnSlotChanged.Broadcast();
UE_LOG(LogItemWorld, Log, TEXT("[Interaction] installed %s into %s"), *Item.ItemId.ToString(), *SlotTag.ToString());
return true;
}
bool UWorldItemSlotComponent::RemoveItem(FItemInstanceData& OutItem)
{
if (!HasAuthority() || !InstalledItem.bHasItem)
{
return false;
}
OutItem = InstalledItem.Item;
if (InstalledVisual)
{
InstalledVisual->Destroy();
InstalledVisual = nullptr;
}
InstalledItem = FWorldSlotInstalledState();
OnSlotChanged.Broadcast();
return true;
}
void UWorldItemSlotComponent::OnRep_InstalledItem()
{
OnSlotChanged.Broadcast();
}