Files
inventory-system-plugin/Source/ItemWorld/Private/WorldInteractionComponent.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

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;
}