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,85 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "GameplayTagContainer.h"
#include "ItemInstanceData.h"
#include "EquipmentComponent.generated.h"
/**
* One equipped slot (section 11.2 Equipped). The item itself stays in the
* inventory; only the visual actor is spawned and replicated.
*/
USTRUCT(BlueprintType)
struct FEquippedItemEntry
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Equipment")
FGameplayTag SlotTag;
UPROPERTY(BlueprintReadOnly, Category = "Equipment")
FGuid InstanceId;
UPROPERTY(BlueprintReadOnly, Category = "Equipment")
FName ItemId;
UPROPERTY(BlueprintReadOnly, Category = "Equipment")
TObjectPtr<AActor> EquippedActor = nullptr;
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnEquipmentChanged);
/**
* Spawns/attaches the visual actors for equipped items (section 7, 11.2, 31.5).
*
* Equip keeps the item in the inventory and only manages the EquippedActorClass
* visual - distinct from carry (UHeldItemComponent), which physically removes the
* item from storage. Owner-only item truth, everyone sees the visual (section 22/25).
*/
UCLASS(ClassGroup = (Inventory), meta = (BlueprintSpawnableComponent))
class ITEMWORLD_API UEquipmentComponent : public UActorComponent
{
GENERATED_BODY()
public:
UEquipmentComponent();
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
virtual void OnUnregister() override;
/** Socket per slot tag; falls back to DefaultEquipSocket. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Equipment")
TMap<FGameplayTag, FName> SlotSockets;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Equipment")
FName DefaultEquipSocket = FName("hand_rSocket");
UPROPERTY(BlueprintAssignable, Category = "Equipment")
FOnEquipmentChanged OnEquipmentChanged;
/** Server: equip an inventory item into a slot (Inventory -> Equipped). */
bool ServerEquip(FGuid InstanceId, FGameplayTag SlotTag);
/** Server: remove the equipped visual from a slot. */
bool ServerUnequip(FGameplayTag SlotTag);
UFUNCTION(BlueprintPure, Category = "Equipment")
bool IsSlotEquipped(FGameplayTag SlotTag) const;
UFUNCTION(BlueprintPure, Category = "Equipment")
void GetEquippedEntries(TArray<FEquippedItemEntry>& OutEntries) const { OutEntries = Equipped; }
protected:
UPROPERTY(ReplicatedUsing = OnRep_Equipped)
TArray<FEquippedItemEntry> Equipped;
UFUNCTION()
void OnRep_Equipped();
FName ResolveSocket(const FGameplayTag& SlotTag) const;
USceneComponent* GetAttachTarget() const;
bool HasAuthority() const;
};

View File

@ -0,0 +1,48 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "ItemInteractionInterfaces.h"
#include "FuelTankComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnFuelChanged, float, NewFuel);
/**
* A fuel reservoir on a world object (generator, vehicle) that accepts fuel poured
* from a held canister (section 15, 31.4). Implements IItemUseTarget for the
* "TransferFuel" result verb.
*/
UCLASS(ClassGroup = (Interaction), meta = (BlueprintSpawnableComponent))
class ITEMWORLD_API UFuelTankComponent : public UActorComponent, public IItemUseTarget
{
GENERATED_BODY()
public:
UFuelTankComponent();
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Fuel")
float MaxFuel = 40.f;
/** Max litres poured per single use action. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Fuel")
float PourAmount = 10.f;
UPROPERTY(ReplicatedUsing = OnRep_Fuel, BlueprintReadOnly, Category = "Fuel")
float CurrentFuel = 0.f;
UPROPERTY(BlueprintAssignable, Category = "Fuel")
FOnFuelChanged OnFuelChanged;
//~ IItemUseTarget
virtual bool ReceiveHeldUse(FName ResultVerb, FItemInstanceData& HeldItem, AActor* Instigator) override;
protected:
UFUNCTION()
void OnRep_Fuel();
bool HasAuthority() const;
};

View File

@ -0,0 +1,151 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "GameplayTagContainer.h"
#include "ItemEnums.h"
#include "ItemInstanceData.h"
#include "ItemInteractionInterfaces.h"
#include "HeldItemComponent.generated.h"
class APickupItemActor;
/** Replicated snapshot of what the character is currently holding (section 11.1). */
USTRUCT(BlueprintType)
struct ITEMWORLD_API FHeldItemState
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Held")
bool bHasHeldItem = false;
UPROPERTY(BlueprintReadOnly, Category = "Held")
FGuid InstanceId;
UPROPERTY(BlueprintReadOnly, Category = "Held")
FName ItemId;
UPROPERTY(BlueprintReadOnly, Category = "Held")
EHeldItemMode Mode = EHeldItemMode::None;
UPROPERTY(BlueprintReadOnly, Category = "Held")
TObjectPtr<AActor> HeldWorldActor = nullptr;
UPROPERTY(BlueprintReadOnly, Category = "Held")
FName HoldSocket = NAME_None;
UPROPERTY(BlueprintReadOnly, Category = "Held")
float MoveSpeedMultiplier = 1.f;
UPROPERTY(BlueprintReadOnly, Category = "Held")
bool bBlocksSprint = false;
UPROPERTY(BlueprintReadOnly, Category = "Held")
bool bBlocksClimb = false;
/** Snapshot of the carried item (so we can drop/install it without the actor). */
UPROPERTY(BlueprintReadOnly, Category = "Held")
FItemInstanceData Item;
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHeldItemChanged, const FHeldItemState&, NewState);
/**
* Physically carries one bulky item attached to the character (section 11.2-11.3).
*
* This is the "two-handed carry" path for batteries, canisters, wheels, crates,
* corpses, etc. - distinct from equipment (which keeps the item in the inventory
* and only spawns a visual). Server-authoritative; visuals/penalties apply on all
* clients via OnRep. Implements IHeldItemInterface so the resolver can build
* held+target interactions.
*/
UCLASS(ClassGroup = (Inventory), meta = (BlueprintSpawnableComponent))
class ITEMWORLD_API UHeldItemComponent : public UActorComponent, public IHeldItemInterface
{
GENERATED_BODY()
public:
UHeldItemComponent();
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Held")
FName CarrySocket = FName("CarrySocket");
/** Hold distance in front of the view for physics-handle carry (section 11.4). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Held")
float PhysicsHoldDistance = 200.f;
UPROPERTY(BlueprintAssignable, Category = "Held")
FOnHeldItemChanged OnHeldItemChanged;
UFUNCTION(BlueprintPure, Category = "Held")
bool HasHeldItemBP() const { return HeldItem.bHasHeldItem; }
UFUNCTION(BlueprintPure, Category = "Held")
const FHeldItemState& GetHeldItemState() const { return HeldItem; }
UFUNCTION(BlueprintPure, Category = "Held")
bool CanHoldItem(const FItemInstanceData& Item) const;
/** Client entry: ask the server to drop the carried item (bind to the Drop input). */
UFUNCTION(BlueprintCallable, Category = "Held")
void RequestDrop();
// --- Server entry points (section 11.1) ---
/** Server: pick up and carry a world item actor attached to a socket (World -> Held). */
bool ServerHoldWorldItem(AActor* WorldItem);
/** Server: carry a world item via a physics handle instead of a fixed socket (section 11.4). */
bool ServerHoldWorldItemPhysics(AActor* WorldItem);
/** Server: drop the carried item back into the world (Held -> World). */
bool ServerDropHeldItem();
/** Server: relinquish the carried item without spawning a drop (e.g. on install). */
bool ServerConsumeHeldItem(FItemInstanceData& OutItem);
/**
* Server: hand off the carried item AND its actor to a new owner (e.g. a world
* slot) without destroying the actor. Clears the held state. Returns false if
* nothing was held.
*/
bool ServerHandOffHeldItem(FItemInstanceData& OutItem, AActor*& OutActor);
/** Server: update the carried item's data in place (e.g. after pouring out fuel). */
bool ServerUpdateHeldItem(const FItemInstanceData& NewItem);
//~ IHeldItemInterface
virtual bool GetHeldItem(FItemInstanceData& OutItem) const override;
virtual bool HasHeldItem() const override { return HeldItem.bHasHeldItem; }
protected:
UPROPERTY(ReplicatedUsing = OnRep_HeldItem)
FHeldItemState HeldItem;
UFUNCTION(Server, Reliable)
void Server_Drop();
UFUNCTION()
void OnRep_HeldItem(FHeldItemState OldState);
/** Applies/removes the movement-speed penalty on the owning character. */
void ApplyMovementState();
bool HasAuthority() const;
private:
/** Owning character's unmodified MaxWalkSpeed, captured the first time we modify it. */
UPROPERTY(Transient)
float CachedBaseWalkSpeed = 0.f;
/** Created on demand for physics-handle carry. */
UPROPERTY(Transient)
TObjectPtr<class UPhysicsHandleComponent> PhysicsHandle = nullptr;
void UpdatePhysicsCarryTarget();
};

View File

@ -0,0 +1,8 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "CoreMinimal.h"
#include "Logging/LogMacros.h"
ITEMWORLD_API DECLARE_LOG_CATEGORY_EXTERN(LogItemWorld, Log, All);

View File

@ -0,0 +1,82 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ItemInstanceData.h"
#include "ItemInteractionInterfaces.h"
#include "PickupItemActor.generated.h"
class UStaticMeshComponent;
class UInteractableComponent;
/**
* The world representation of an item lying on the ground / a surface (section 12).
*
* One actor per world item only (never one per inventory item - section 32/34).
* Implements IWorldItemProvider so the interaction layer can pick it up, carry it
* or take part of its stack without ItemWorld <-> ItemInteraction coupling.
*/
UCLASS()
class ITEMWORLD_API APickupItemActor : public AActor, public IWorldItemProvider
{
GENERATED_BODY()
public:
APickupItemActor();
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
virtual void BeginPlay() override;
/** Server: stamp this pickup with an item and refresh its visuals. */
UFUNCTION(BlueprintCallable, Category = "Item|World")
void InitializeFromItem(const FItemInstanceData& InItem);
/** Server convenience: spawn a fresh pickup for ItemId at a transform. */
UFUNCTION(BlueprintCallable, Category = "Item|World", meta = (WorldContext = "WorldContext"))
static APickupItemActor* SpawnPickup(UObject* WorldContext, FName ItemId, int32 Quantity, const FTransform& Transform);
/** Server convenience: spawn a pickup carrying an existing instance (e.g. on drop). */
static APickupItemActor* SpawnPickupFromInstance(UObject* WorldContext, const FItemInstanceData& Item, const FTransform& Transform);
//~ IWorldItemProvider
virtual bool GetProvidedItem(FItemInstanceData& OutItem) const override;
virtual bool IsItemAvailable() const override;
virtual void NotifyItemTaken(AActor* Taker) override;
virtual void UpdateProvidedItem(const FItemInstanceData& RemainingItem) override;
virtual void SetCarried(AActor* Holder, bool bCarried) override;
/** Server: drop this carried actor back into the world at a transform. */
void ReleaseToWorld(const FTransform& WorldTransform);
UStaticMeshComponent* GetMeshComponent() const { return Mesh; }
UFUNCTION(BlueprintPure, Category = "Item|World")
const FItemInstanceData& GetItemData() const { return ItemData; }
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|World")
TObjectPtr<USceneComponent> Root;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|World")
TObjectPtr<UStaticMeshComponent> Mesh;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Item|World")
TObjectPtr<UInteractableComponent> Interactable;
UPROPERTY(ReplicatedUsing = OnRep_ItemData, BlueprintReadOnly, Category = "Item|World")
FItemInstanceData ItemData;
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Item|World")
bool bIsLocked = false;
UPROPERTY(Replicated, BlueprintReadOnly, Category = "Item|World")
TObjectPtr<AActor> CurrentHolder = nullptr;
UFUNCTION()
void OnRep_ItemData();
/** Resolves the definition and applies WorldMesh + interactable display name. */
void RefreshVisuals();
};

View File

@ -0,0 +1,38 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "StorageContainerActor.generated.h"
class UStaticMeshComponent;
class UInteractableComponent;
class UInventoryComponent;
/**
* A lootable world container (chest, crate, fridge, vehicle cargo, corpse) (section 5).
* Its inventory is shared-replicated so any nearby player who opens it sees the
* contents; transfers go through the validated server path (section 31.2).
*/
UCLASS()
class ITEMWORLD_API AStorageContainerActor : public AActor
{
GENERATED_BODY()
public:
AStorageContainerActor();
UFUNCTION(BlueprintPure, Category = "Storage")
UInventoryComponent* GetStorageInventory() const { return Storage; }
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
TObjectPtr<UStaticMeshComponent> Mesh;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
TObjectPtr<UInteractableComponent> Interactable;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Storage")
TObjectPtr<UInventoryComponent> Storage;
};

View File

@ -0,0 +1,76 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "CoreMinimal.h"
#include "InteractionAction.h"
#include "WorldInteractionActions.generated.h"
/**
* World interaction actions (section 16). Each routes through the player's
* UItemInteractionComponent::ServerExecuteMode so it shares the one validated
* execution path (carry/equip/install/use live in UWorldInteractionComponent).
*/
UCLASS(meta = (DisplayName = "Action: Carry (Attached)"))
class ITEMWORLD_API UInteractionAction_CarryAttached : public UInteractionAction
{
GENERATED_BODY()
public:
UInteractionAction_CarryAttached();
virtual void Execute_Implementation(const FInteractionContext& Context) override;
};
UCLASS(meta = (DisplayName = "Action: Carry (Physics)"))
class ITEMWORLD_API UInteractionAction_CarryPhysics : public UInteractionAction
{
GENERATED_BODY()
public:
UInteractionAction_CarryPhysics();
virtual void Execute_Implementation(const FInteractionContext& Context) override;
};
UCLASS(meta = (DisplayName = "Action: Equip"))
class ITEMWORLD_API UInteractionAction_Equip : public UInteractionAction
{
GENERATED_BODY()
public:
UInteractionAction_Equip();
virtual void Execute_Implementation(const FInteractionContext& Context) override;
};
UCLASS(meta = (DisplayName = "Action: Drop Held Item"))
class ITEMWORLD_API UInteractionAction_DropHeldItem : public UInteractionAction
{
GENERATED_BODY()
public:
UInteractionAction_DropHeldItem();
virtual void Execute_Implementation(const FInteractionContext& Context) override;
};
UCLASS(meta = (DisplayName = "Action: Install Into World Slot"))
class ITEMWORLD_API UInteractionAction_InstallIntoWorldSlot : public UInteractionAction
{
GENERATED_BODY()
public:
UInteractionAction_InstallIntoWorldSlot();
virtual void Execute_Implementation(const FInteractionContext& Context) override;
};
UCLASS(meta = (DisplayName = "Action: Remove From World Slot"))
class ITEMWORLD_API UInteractionAction_RemoveFromWorldSlot : public UInteractionAction
{
GENERATED_BODY()
public:
UInteractionAction_RemoveFromWorldSlot();
virtual void Execute_Implementation(const FInteractionContext& Context) override;
};
UCLASS(meta = (DisplayName = "Action: Use Held On Target"))
class ITEMWORLD_API UInteractionAction_UseHeldOnTarget : public UInteractionAction
{
GENERATED_BODY()
public:
UInteractionAction_UseHeldOnTarget();
virtual void Execute_Implementation(const FInteractionContext& Context) override;
};

View File

@ -0,0 +1,31 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "CoreMinimal.h"
#include "ItemInteractionComponent.h"
#include "WorldInteractionComponent.generated.h"
/**
* The interaction executor to put on a player pawn in a project that uses the
* world modules. Extends the base with the modes that need ItemWorld types:
* carry, drop-held, equip, install/remove from world slots, and drop-from-inventory
* (stages 6-8). Keeps ItemInteraction free of any ItemWorld dependency.
*/
UCLASS(ClassGroup = (Interaction), meta = (BlueprintSpawnableComponent))
class ITEMWORLD_API UWorldInteractionComponent : public UItemInteractionComponent
{
GENERATED_BODY()
protected:
virtual bool ExecuteAuthorized(const FInteractionContext& Context, const FInteractionOption& Option, const FInteractionRequest& Request) override;
virtual bool HandleDropFromInventory(FGuid InstanceId) override;
private:
bool ExecuteCarry(const FInteractionContext& Context, bool bPhysics);
bool ExecuteDropHeld(const FInteractionContext& Context);
bool ExecuteInstall(const FInteractionContext& Context);
bool ExecuteEquip(const FInteractionContext& Context, const FInteractionRequest& Request);
bool ExecuteUseHeldOnTarget(const FInteractionContext& Context, const FInteractionOption& Option);
bool ExecuteRemoveFromSlot(const FInteractionContext& Context);
};

View File

@ -0,0 +1,83 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "GameplayTagContainer.h"
#include "ItemInstanceData.h"
#include "WorldItemSlotComponent.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnWorldSlotChanged);
/** Replicated state of a world slot's contents (section 14). */
USTRUCT()
struct FWorldSlotInstalledState
{
GENERATED_BODY()
UPROPERTY()
bool bHasItem = false;
UPROPERTY()
FItemInstanceData Item;
};
/**
* A socket on a world object that accepts an installed item (section 14):
* Vehicle.Socket.Battery, Generator.Socket.Fuel, Door.Socket.BarricadePlank,
* Trap.Socket.Bait, etc. One component per slot.
*/
UCLASS(ClassGroup = (Interaction), meta = (BlueprintSpawnableComponent))
class ITEMWORLD_API UWorldItemSlotComponent : public UActorComponent
{
GENERATED_BODY()
public:
UWorldItemSlotComponent();
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
/** This slot's identity; items list it in CompatibleWorldSlots to fit. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "World Slot")
FGameplayTag SlotTag;
/** Optional extra gate; empty = accept anything whose CompatibleWorldSlots has SlotTag. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "World Slot")
FGameplayTagContainer AcceptedItemTags;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "World Slot")
FName AttachSocketName = NAME_None;
UPROPERTY(BlueprintAssignable, Category = "World Slot")
FOnWorldSlotChanged OnSlotChanged;
UFUNCTION(BlueprintPure, Category = "World Slot")
bool IsOccupied() const { return InstalledItem.bHasItem; }
UFUNCTION(BlueprintPure, Category = "World Slot")
bool CanInstall(const FItemInstanceData& Item) const;
UFUNCTION(BlueprintPure, Category = "World Slot")
FItemInstanceData GetInstalledItem() const { return InstalledItem.Item; }
/** Server: install an item. ProvidedVisual (e.g. a carried actor) is reused if given. */
bool InstallItem(const FItemInstanceData& Item, AActor* ProvidedVisual = nullptr);
/** Server: remove and return the installed item. */
bool RemoveItem(FItemInstanceData& OutItem);
protected:
UPROPERTY(ReplicatedUsing = OnRep_InstalledItem)
FWorldSlotInstalledState InstalledItem;
/** Server-spawned visual for the installed item (if not provided externally). */
UPROPERTY(Replicated)
TObjectPtr<AActor> InstalledVisual = nullptr;
UFUNCTION()
void OnRep_InstalledItem();
USceneComponent* GetAttachTarget() const;
bool HasAuthority() const;
};