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,9 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "CoreMinimal.h"
#include "Logging/LogMacros.h"
/** Shared log categories for the Item Interaction Ecosystem (section 33). */
ITEMCORE_API DECLARE_LOG_CATEGORY_EXTERN(LogItemCore, Log, All);

View File

@ -0,0 +1,19 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
/**
* ItemCore module.
* Foundation of the Item Interaction Ecosystem: base item structs, ids,
* runtime properties, location/state enums and shared log categories.
* Contains zero gameplay logic - only data types every other module shares.
*/
class FItemCoreModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};

View File

@ -0,0 +1,95 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "CoreMinimal.h"
#include "ItemEnums.generated.h"
/**
* Where an item instance currently lives (section 5).
* Every state transition is validated server-side through the transaction layer.
*/
UENUM(BlueprintType)
enum class EItemLocationType : uint8
{
None,
World,
Inventory,
Container,
Held,
Equipped,
PlacementPreview,
Installed,
NestedContainer
};
/** Per-property replication scope (section 22). */
UENUM(BlueprintType)
enum class EItemPropertyReplicationPolicy : uint8
{
None,
OwnerOnly,
Everyone,
NearbyOnly,
ServerOnly
};
/** How an item is currently carried/equipped on a character (section 11). */
UENUM(BlueprintType)
enum class EHeldItemMode : uint8
{
None,
EquippedFromInventory,
CarryAttached,
CarryPhysicsHandle,
PlacementPreview
};
/** High level interaction mode resolved per option (section 8.5). */
UENUM(BlueprintType)
enum class EItemInteractionMode : uint8
{
None,
InstantPickup,
OpenLootUI,
PickupToInventory,
EquipFromWorld,
EquipFromInventory,
CarryAttached,
CarryPhysicsHandle,
DropHeldItem,
Inspect,
Use,
UseHeldOnTarget,
Combine,
StartPlacement,
ConfirmPlacement,
CancelPlacement,
InstallIntoWorldSlot,
RemoveFromWorldSlot,
TransferContainerItem,
SplitStack,
MergeStack
};
/** Container layout / role (section 7.2). */
UENUM(BlueprintType)
enum class EInventoryContainerType : uint8
{
SlotList,
Grid,
Equipment,
Hotbar,
VehicleCargo,
LootBox,
KeyRing,
Wallet,
CraftingInput,
CraftingOutput
};

View File

@ -0,0 +1,92 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "ItemEnums.h"
#include "ItemRuntimeProperties.h"
#include "ItemLockData.h"
#include "ItemInstanceData.generated.h"
/**
* Runtime representation of a single item instance (section 4.3).
*
* Deliberately tiny: it never copies the static FItemDefinitionRow - it only
* stores the stable ItemId plus this instance's mutable state. Resolve the
* definition through UItemDatabaseSubsystem when you need icons/meshes/flags.
*/
USTRUCT(BlueprintType)
struct ITEMCORE_API FItemInstanceData
{
GENERATED_BODY()
/** Globally unique handle for this concrete stack/instance. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
FGuid InstanceId;
/** Stable design-time id (e.g. "ammo_9mm"). Never a localized string. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
FName ItemId;
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
int32 Quantity = 1;
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
FGameplayTagContainer StateTags;
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
FItemRuntimeProperties RuntimeProperties;
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
FGuid CurrentContainerId;
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
int32 SlotIndex = INDEX_NONE;
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
EItemLocationType LocationType = EItemLocationType::None;
/** Bumped when the item schema changes; drives save migration (section 26.2). */
UPROPERTY(EditAnywhere, BlueprintReadOnly, SaveGame, Category = "Item")
int32 ItemDataVersion = 1;
/** Transient server-side lock; not saved (section 20). */
UPROPERTY(BlueprintReadOnly, Transient, Category = "Item")
FItemLockData Lock;
FItemInstanceData() = default;
explicit FItemInstanceData(FName InItemId, int32 InQuantity = 1)
: InstanceId(FGuid::NewGuid())
, ItemId(InItemId)
, Quantity(InQuantity)
{
}
bool IsValid() const { return !ItemId.IsNone() && InstanceId.IsValid() && Quantity > 0; }
void EnsureInstanceId()
{
if (!InstanceId.IsValid())
{
InstanceId = FGuid::NewGuid();
}
}
/**
* Two instances can merge into one stack only when they are the same item,
* carry identical per-instance state and neither is locked. MaxStack is
* enforced by the inventory layer which has access to the definition.
*/
bool CanStackWith(const FItemInstanceData& Other) const
{
return ItemId == Other.ItemId
&& !Lock.bLocked && !Other.Lock.bLocked
&& StateTags == Other.StateTags
&& RuntimeProperties == Other.RuntimeProperties;
}
bool operator==(const FItemInstanceData& Other) const { return InstanceId == Other.InstanceId; }
bool operator!=(const FItemInstanceData& Other) const { return InstanceId != Other.InstanceId; }
};

View File

@ -0,0 +1,42 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "ItemLockData.generated.h"
/**
* Server-side soft lock placed on an item while a transaction owns it (section 20).
* Prevents dupes / conflicts: a locked item cannot be moved, taken, dropped,
* crafted, deleted or stacked by anyone but the locking actor.
*/
USTRUCT(BlueprintType)
struct ITEMCORE_API FItemLockData
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Item|Lock")
bool bLocked = false;
/** Lock.Carrying / Lock.Using / Lock.Transferring / ... */
UPROPERTY(BlueprintReadOnly, Category = "Item|Lock")
FGameplayTag LockReason;
UPROPERTY(BlueprintReadOnly, Category = "Item|Lock")
TWeakObjectPtr<AActor> LockedBy;
/** Server world time (seconds) at which the lock auto-expires; 0 = no expiry. */
UPROPERTY(BlueprintReadOnly, Category = "Item|Lock")
float LockExpireTime = 0.f;
bool IsLockedBy(const AActor* Actor) const { return bLocked && LockedBy.Get() == Actor; }
void Clear()
{
bLocked = false;
LockReason = FGameplayTag();
LockedBy = nullptr;
LockExpireTime = 0.f;
}
};

View File

@ -0,0 +1,88 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "NativeGameplayTags.h"
/**
* Native gameplay tags shipped with the framework.
*
* These cover the built-in behaviour the C++ layer reasons about directly
* (lock reasons, the common runtime property tags, the default action verbs).
* Project / designer specific tags (item types, world sockets, custom features)
* are expected to be declared in the project's tag config or DataTables, not here.
*/
namespace ItemEcosystemTags
{
// --- Runtime property tags (section 4.4) ---
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Durability);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Freshness);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_RotProgress);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Temperature);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Wetness);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_FuelAmount);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_MaxFuel);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_BatteryCharge);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Contamination);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_NoiseLevel);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_SmellIntensity);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_BloodLevel);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Radiation);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Poison);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_CookedLevel);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_BurnLevel);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Quality);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_Charge);
/** Server world-time (seconds) of the last lazy feature update (section 21.4). */
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Property_LastUpdateTime);
// --- State tags (section 4.3 StateTags) ---
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(State_Broken);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(State_Rotten);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(State_Wet);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(State_Contaminated);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(State_Empty);
// --- Lock reasons (section 20) ---
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Lock_Carrying);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Lock_Using);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Lock_Transferring);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Lock_Crafting);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Lock_Installing);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Lock_Replicating);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Lock_Placement);
// --- Action verbs (section 6.4 / 8.x) ---
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Pickup_Instant);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Pickup);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Open);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Carry);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Drop);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Equip);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Inspect);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Use);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Install);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Remove);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_Place);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Action_UseHeldOnTarget);
// --- Feature tags (section 6.3) ---
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Consumable);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Rotting);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Durability);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Battery);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Fuel);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Wetness);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Temperature);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_NoiseOnDrop);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Smell);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Container);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Equippable);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Throwing);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Breakable);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Flammable);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Placeable);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Installable);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_QuestItem);
ITEMCORE_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Feature_Cursed);
}

View File

@ -0,0 +1,190 @@
// Copyright ExByte Studios. Item Interaction Ecosystem.
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "ItemRuntimeProperties.generated.h"
/**
* Tag->value pairs (section 4.4).
*
* The TЗ sketched these as TMaps, but UHT forbids TMap inside replicated structs,
* and runtime properties must replicate (they live in FItemInstanceData). So they
* are stored as small replicable arrays instead; the typed accessors below hide
* that and behave like a map. N is tiny per item, so linear lookup is fine.
*/
USTRUCT(BlueprintType)
struct FItemFloatProperty
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
FGameplayTag Tag;
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
float Value = 0.f;
bool operator==(const FItemFloatProperty& O) const { return Tag == O.Tag && Value == O.Value; }
};
USTRUCT(BlueprintType)
struct FItemIntProperty
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
FGameplayTag Tag;
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
int32 Value = 0;
bool operator==(const FItemIntProperty& O) const { return Tag == O.Tag && Value == O.Value; }
};
USTRUCT(BlueprintType)
struct FItemBoolProperty
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
FGameplayTag Tag;
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
bool Value = false;
bool operator==(const FItemBoolProperty& O) const { return Tag == O.Tag && Value == O.Value; }
};
USTRUCT(BlueprintType)
struct FItemStringProperty
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
FGameplayTag Tag;
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
FString Value;
bool operator==(const FItemStringProperty& O) const { return Tag == O.Tag && Value == O.Value; }
};
/**
* Dynamic, per-instance properties keyed by gameplay tag (section 4.4).
* Only data differing from the definition defaults should be stored, to keep
* replication/save payloads small. Typed accessors fall back when a key is absent.
*/
USTRUCT(BlueprintType)
struct ITEMCORE_API FItemRuntimeProperties
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
TArray<FItemFloatProperty> FloatProperties;
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
TArray<FItemIntProperty> IntProperties;
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
TArray<FItemBoolProperty> BoolProperties;
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category = "Item|Runtime")
TArray<FItemStringProperty> StringProperties;
bool IsEmpty() const
{
return FloatProperties.Num() == 0 && IntProperties.Num() == 0
&& BoolProperties.Num() == 0 && StringProperties.Num() == 0;
}
// --- Float ---
float GetFloat(const FGameplayTag& Tag, float Fallback = 0.f) const
{
for (const FItemFloatProperty& P : FloatProperties) { if (P.Tag == Tag) { return P.Value; } }
return Fallback;
}
bool HasFloat(const FGameplayTag& Tag) const
{
for (const FItemFloatProperty& P : FloatProperties) { if (P.Tag == Tag) { return true; } }
return false;
}
void SetFloat(const FGameplayTag& Tag, float Value)
{
for (FItemFloatProperty& P : FloatProperties) { if (P.Tag == Tag) { P.Value = Value; return; } }
FloatProperties.Add({ Tag, Value });
}
void RemoveFloat(const FGameplayTag& Tag)
{
FloatProperties.RemoveAll([&Tag](const FItemFloatProperty& P) { return P.Tag == Tag; });
}
// --- Int ---
int32 GetInt(const FGameplayTag& Tag, int32 Fallback = 0) const
{
for (const FItemIntProperty& P : IntProperties) { if (P.Tag == Tag) { return P.Value; } }
return Fallback;
}
bool HasInt(const FGameplayTag& Tag) const
{
for (const FItemIntProperty& P : IntProperties) { if (P.Tag == Tag) { return true; } }
return false;
}
void SetInt(const FGameplayTag& Tag, int32 Value)
{
for (FItemIntProperty& P : IntProperties) { if (P.Tag == Tag) { P.Value = Value; return; } }
IntProperties.Add({ Tag, Value });
}
// --- Bool ---
bool GetBool(const FGameplayTag& Tag, bool Fallback = false) const
{
for (const FItemBoolProperty& P : BoolProperties) { if (P.Tag == Tag) { return P.Value; } }
return Fallback;
}
bool HasBool(const FGameplayTag& Tag) const
{
for (const FItemBoolProperty& P : BoolProperties) { if (P.Tag == Tag) { return true; } }
return false;
}
void SetBool(const FGameplayTag& Tag, bool Value)
{
for (FItemBoolProperty& P : BoolProperties) { if (P.Tag == Tag) { P.Value = Value; return; } }
BoolProperties.Add({ Tag, Value });
}
// --- String ---
FString GetString(const FGameplayTag& Tag, const FString& Fallback = FString()) const
{
for (const FItemStringProperty& P : StringProperties) { if (P.Tag == Tag) { return P.Value; } }
return Fallback;
}
bool HasString(const FGameplayTag& Tag) const
{
for (const FItemStringProperty& P : StringProperties) { if (P.Tag == Tag) { return true; } }
return false;
}
void SetString(const FGameplayTag& Tag, const FString& Value)
{
for (FItemStringProperty& P : StringProperties) { if (P.Tag == Tag) { P.Value = Value; return; } }
StringProperties.Add({ Tag, Value });
}
bool operator==(const FItemRuntimeProperties& Other) const
{
return ArrayEqual(FloatProperties, Other.FloatProperties)
&& ArrayEqual(IntProperties, Other.IntProperties)
&& ArrayEqual(BoolProperties, Other.BoolProperties)
&& ArrayEqual(StringProperties, Other.StringProperties);
}
bool operator!=(const FItemRuntimeProperties& Other) const { return !(*this == Other); }
private:
template<typename T>
static bool ArrayEqual(const TArray<T>& A, const TArray<T>& B)
{
if (A.Num() != B.Num()) { return false; }
for (const T& Elem : A) { if (!B.Contains(Elem)) { return false; } }
return true;
}
};