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>
262 lines
7.5 KiB
C++
262 lines
7.5 KiB
C++
// 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;
|
|
}
|