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:
27
Source/ItemInventory/ItemInventory.Build.cs
Normal file
27
Source/ItemInventory/ItemInventory.Build.cs
Normal file
@ -0,0 +1,27 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class ItemInventory : ModuleRules
|
||||
{
|
||||
public ItemInventory(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
IWYUSupport = IWYUSupport.Full;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"GameplayTags",
|
||||
"NetCore",
|
||||
"ItemCore",
|
||||
"ItemDatabase"
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
});
|
||||
}
|
||||
}
|
||||
830
Source/ItemInventory/Private/InventoryComponent.cpp
Normal file
830
Source/ItemInventory/Private/InventoryComponent.cpp
Normal file
@ -0,0 +1,830 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "InventoryComponent.h"
|
||||
#include "ItemInventoryLog.h"
|
||||
#include "ItemDatabaseSubsystem.h"
|
||||
#include "ItemDefinitionRow.h"
|
||||
#include "Net/UnrealNetwork.h"
|
||||
#include "Engine/ActorChannel.h"
|
||||
|
||||
UInventoryComponent::UInventoryComponent()
|
||||
{
|
||||
PrimaryComponentTick.bCanEverTick = false;
|
||||
bWantsInitializeComponent = true;
|
||||
SetIsReplicatedByDefault(true);
|
||||
}
|
||||
|
||||
void UInventoryComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
|
||||
{
|
||||
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
|
||||
|
||||
if (bOwnerOnlyReplication)
|
||||
{
|
||||
DOREPLIFETIME_CONDITION(UInventoryComponent, ReplicatedItems, COND_OwnerOnly);
|
||||
DOREPLIFETIME_CONDITION(UInventoryComponent, Containers, COND_OwnerOnly);
|
||||
}
|
||||
else
|
||||
{
|
||||
DOREPLIFETIME(UInventoryComponent, ReplicatedItems);
|
||||
DOREPLIFETIME(UInventoryComponent, Containers);
|
||||
}
|
||||
}
|
||||
|
||||
void UInventoryComponent::OnRegister()
|
||||
{
|
||||
Super::OnRegister();
|
||||
ReplicatedItems.OwnerComponent = this;
|
||||
}
|
||||
|
||||
void UInventoryComponent::InitializeComponent()
|
||||
{
|
||||
Super::InitializeComponent();
|
||||
ReplicatedItems.OwnerComponent = this;
|
||||
|
||||
if (GetOwner() && GetOwner()->HasAuthority())
|
||||
{
|
||||
for (const FInventoryContainer& Template : DefaultContainers)
|
||||
{
|
||||
AddContainer(Template);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool UInventoryComponent::HasAuthorityChecked(const TCHAR* Op) const
|
||||
{
|
||||
if (GetOwner() && GetOwner()->HasAuthority())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
UE_LOG(LogItemInventory, Warning, TEXT("%s called without authority on %s - ignored."),
|
||||
Op, *GetNameSafe(GetOwner()));
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Containers ---
|
||||
|
||||
FGuid UInventoryComponent::AddContainer(FInventoryContainer Container)
|
||||
{
|
||||
if (!HasAuthorityChecked(TEXT("AddContainer")))
|
||||
{
|
||||
return FGuid();
|
||||
}
|
||||
if (!Container.ContainerId.IsValid())
|
||||
{
|
||||
Container.ContainerId = FGuid::NewGuid();
|
||||
}
|
||||
Containers.Add(Container);
|
||||
NotifyInventoryChanged();
|
||||
return Container.ContainerId;
|
||||
}
|
||||
|
||||
FGuid UInventoryComponent::AddContainerByTag(FGameplayTag ContainerTag)
|
||||
{
|
||||
if (!HasAuthorityChecked(TEXT("AddContainerByTag")))
|
||||
{
|
||||
return FGuid();
|
||||
}
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||
FContainerDefRow Def;
|
||||
if (!DB || !DB->FindContainerDef(ContainerTag, Def))
|
||||
{
|
||||
UE_LOG(LogItemInventory, Warning, TEXT("AddContainerByTag: no DT_Containers row for %s"), *ContainerTag.ToString());
|
||||
return FGuid();
|
||||
}
|
||||
FInventoryContainer Container;
|
||||
Container.ContainerTag = Def.ContainerTag;
|
||||
Container.Type = Def.Type;
|
||||
Container.Width = Def.Width;
|
||||
Container.Height = Def.Height;
|
||||
Container.MaxSlots = Def.MaxSlots;
|
||||
Container.MaxWeight = Def.MaxWeight;
|
||||
Container.AcceptedItemTags = Def.AcceptedItemTags;
|
||||
Container.BlockedItemTags = Def.BlockedItemTags;
|
||||
return AddContainer(Container);
|
||||
}
|
||||
|
||||
FGuid UInventoryComponent::AddNestedContainerForItem(FGuid OwnerItemInstanceId, FGameplayTag ContainerTag)
|
||||
{
|
||||
const FGuid NewId = AddContainerByTag(ContainerTag);
|
||||
if (NewId.IsValid())
|
||||
{
|
||||
if (FInventoryContainer* Container = GetContainerMutable(NewId))
|
||||
{
|
||||
Container->OwnerItemInstanceId = OwnerItemInstanceId;
|
||||
Container->Type = EInventoryContainerType::SlotList; // nested defaults
|
||||
NotifyInventoryChanged();
|
||||
}
|
||||
}
|
||||
return NewId;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::GetContainer(FGuid ContainerId, FInventoryContainer& OutContainer) const
|
||||
{
|
||||
if (const FInventoryContainer* Found = GetContainerConst(ContainerId))
|
||||
{
|
||||
OutContainer = *Found;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FGuid UInventoryComponent::FindContainerByTag(FGameplayTag ContainerTag) const
|
||||
{
|
||||
for (const FInventoryContainer& C : Containers)
|
||||
{
|
||||
if (C.ContainerTag == ContainerTag)
|
||||
{
|
||||
return C.ContainerId;
|
||||
}
|
||||
}
|
||||
return FGuid();
|
||||
}
|
||||
|
||||
FInventoryContainer* UInventoryComponent::GetContainerMutable(const FGuid& ContainerId)
|
||||
{
|
||||
return Containers.FindByPredicate([&ContainerId](const FInventoryContainer& C) { return C.ContainerId == ContainerId; });
|
||||
}
|
||||
|
||||
const FInventoryContainer* UInventoryComponent::GetContainerConst(const FGuid& ContainerId) const
|
||||
{
|
||||
return Containers.FindByPredicate([&ContainerId](const FInventoryContainer& C) { return C.ContainerId == ContainerId; });
|
||||
}
|
||||
|
||||
// --- Queries ---
|
||||
|
||||
bool UInventoryComponent::IsSlotOccupied(const FGuid& ContainerId, int32 SlotIndex) const
|
||||
{
|
||||
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||
{
|
||||
if (E.ContainerId == ContainerId && E.SlotIndex == SlotIndex)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int32 UInventoryComponent::FindFreeSlot(const FGuid& ContainerId) const
|
||||
{
|
||||
const FInventoryContainer* Container = GetContainerConst(ContainerId);
|
||||
if (!Container)
|
||||
{
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
const int32 SlotCount = Container->GetSlotCount();
|
||||
if (SlotCount > 0)
|
||||
{
|
||||
for (int32 Slot = 0; Slot < SlotCount; ++Slot)
|
||||
{
|
||||
if (!IsSlotOccupied(ContainerId, Slot))
|
||||
{
|
||||
return Slot;
|
||||
}
|
||||
}
|
||||
return INDEX_NONE; // full
|
||||
}
|
||||
|
||||
// Unlimited (weight-gated) list: append after the highest used index.
|
||||
int32 MaxUsed = INDEX_NONE;
|
||||
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||
{
|
||||
if (E.ContainerId == ContainerId)
|
||||
{
|
||||
MaxUsed = FMath::Max(MaxUsed, E.SlotIndex);
|
||||
}
|
||||
}
|
||||
return MaxUsed + 1;
|
||||
}
|
||||
|
||||
FIntPoint UInventoryComponent::GetItemGridSize(FName ItemId) const
|
||||
{
|
||||
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this))
|
||||
{
|
||||
if (const FItemDefinitionRow* Def = DB->GetItemDefinition(ItemId))
|
||||
{
|
||||
return FIntPoint(FMath::Max(1, Def->GridSize.X), FMath::Max(1, Def->GridSize.Y));
|
||||
}
|
||||
}
|
||||
return FIntPoint(1, 1);
|
||||
}
|
||||
|
||||
bool UInventoryComponent::IsGridRegionFree(const FInventoryContainer& Container, int32 TopLeft, FIntPoint Size, const FGuid& IgnoreInstance) const
|
||||
{
|
||||
const int32 W = FMath::Max(1, Container.Width);
|
||||
const int32 H = FMath::Max(1, Container.Height);
|
||||
const int32 X0 = TopLeft % W;
|
||||
const int32 Y0 = TopLeft / W;
|
||||
if (X0 + Size.X > W || Y0 + Size.Y > H)
|
||||
{
|
||||
return false; // would overflow the grid
|
||||
}
|
||||
|
||||
// Build an occupancy mask from existing entries' footprints.
|
||||
TArray<bool> Occupied;
|
||||
Occupied.Init(false, W * H);
|
||||
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||
{
|
||||
if (E.ContainerId != Container.ContainerId || E.Item.InstanceId == IgnoreInstance || E.SlotIndex < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const FIntPoint ES = GetItemGridSize(E.Item.ItemId);
|
||||
const int32 EX = E.SlotIndex % W;
|
||||
const int32 EY = E.SlotIndex / W;
|
||||
for (int32 dy = 0; dy < ES.Y; ++dy)
|
||||
{
|
||||
for (int32 dx = 0; dx < ES.X; ++dx)
|
||||
{
|
||||
const int32 CellX = EX + dx;
|
||||
const int32 CellY = EY + dy;
|
||||
if (CellX < W && CellY < H) { Occupied[CellY * W + CellX] = true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int32 dy = 0; dy < Size.Y; ++dy)
|
||||
{
|
||||
for (int32 dx = 0; dx < Size.X; ++dx)
|
||||
{
|
||||
if (Occupied[(Y0 + dy) * W + (X0 + dx)]) { return false; }
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int32 UInventoryComponent::FindFreeSlotForItem(const FGuid& ContainerId, const FItemInstanceData& Item) const
|
||||
{
|
||||
const FInventoryContainer* Container = GetContainerConst(ContainerId);
|
||||
if (!Container)
|
||||
{
|
||||
return INDEX_NONE;
|
||||
}
|
||||
if (Container->Type != EInventoryContainerType::Grid)
|
||||
{
|
||||
return FindFreeSlot(ContainerId);
|
||||
}
|
||||
|
||||
const int32 W = FMath::Max(1, Container->Width);
|
||||
const int32 H = FMath::Max(1, Container->Height);
|
||||
const FIntPoint Size = GetItemGridSize(Item.ItemId);
|
||||
for (int32 Cell = 0; Cell < W * H; ++Cell)
|
||||
{
|
||||
if (IsGridRegionFree(*Container, Cell, Size, FGuid()))
|
||||
{
|
||||
return Cell;
|
||||
}
|
||||
}
|
||||
return INDEX_NONE;
|
||||
}
|
||||
|
||||
float UInventoryComponent::GetContainerWeight(FGuid ContainerId) const
|
||||
{
|
||||
float Total = 0.f;
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||
{
|
||||
if (E.ContainerId != ContainerId)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (DB)
|
||||
{
|
||||
if (const FItemDefinitionRow* Def = DB->GetItemDefinition(E.Item.ItemId))
|
||||
{
|
||||
Total += Def->Weight * E.Item.Quantity;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Total;
|
||||
}
|
||||
|
||||
int32 UInventoryComponent::GetItemCount(FName ItemId) const
|
||||
{
|
||||
int32 Count = 0;
|
||||
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||
{
|
||||
if (E.Item.ItemId == ItemId)
|
||||
{
|
||||
Count += E.Item.Quantity;
|
||||
}
|
||||
}
|
||||
return Count;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::FindItem(FGuid InstanceId, FItemInstanceData& OutItem) const
|
||||
{
|
||||
if (const FRepInventoryEntry* Entry = ReplicatedItems.FindByInstanceId(InstanceId))
|
||||
{
|
||||
OutItem = Entry->Item;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void UInventoryComponent::GetContainerItems(FGuid ContainerId, TArray<FItemInstanceData>& OutItems) const
|
||||
{
|
||||
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||
{
|
||||
if (E.ContainerId == ContainerId)
|
||||
{
|
||||
OutItems.Add(E.Item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UInventoryComponent::GetAllItems(TArray<FItemInstanceData>& OutItems) const
|
||||
{
|
||||
OutItems.Reserve(ReplicatedItems.Entries.Num());
|
||||
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||
{
|
||||
OutItems.Add(E.Item);
|
||||
}
|
||||
}
|
||||
|
||||
bool UInventoryComponent::CanAcceptItem(const FItemInstanceData& Item, FGuid ContainerId) const
|
||||
{
|
||||
const FInventoryContainer* Container = GetContainerConst(ContainerId);
|
||||
if (!Container || !Item.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||
const FItemDefinitionRow* Def = DB ? DB->GetItemDefinition(Item.ItemId) : nullptr;
|
||||
if (!Def)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Capability: storage-style containers respect bCanStoreInInventory.
|
||||
const bool bStorageStyle =
|
||||
Container->Type == EInventoryContainerType::SlotList ||
|
||||
Container->Type == EInventoryContainerType::Grid ||
|
||||
Container->Type == EInventoryContainerType::Hotbar;
|
||||
if (bStorageStyle && !Def->bCanStoreInInventory)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tag gating.
|
||||
FGameplayTagContainer ItemTags = Def->ItemTags;
|
||||
ItemTags.AddTag(Def->ItemType);
|
||||
if (!Container->AcceptedItemTags.IsEmpty() && !ItemTags.HasAny(Container->AcceptedItemTags))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!Container->BlockedItemTags.IsEmpty() && ItemTags.HasAny(Container->BlockedItemTags))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Weight.
|
||||
if (Container->HasWeightLimit())
|
||||
{
|
||||
const float Projected = GetContainerWeight(ContainerId) + Def->Weight * Item.Quantity;
|
||||
if (Projected > Container->MaxWeight)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Space: a free slot, or an existing stack of the same item with headroom.
|
||||
if (Container->HasSlotLimit())
|
||||
{
|
||||
if (FindFreeSlotForItem(ContainerId, Item) != INDEX_NONE)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
const int32 MaxStack = FMath::Max(1, Def->MaxStack);
|
||||
for (const FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||
{
|
||||
if (E.ContainerId == ContainerId && E.Item.CanStackWith(Item) && E.Item.Quantity < MaxStack)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- Mutations ---
|
||||
|
||||
void UInventoryComponent::StackIntoExisting(const FGuid& ContainerId, FItemInstanceData& Item, int32& Remaining)
|
||||
{
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||
const int32 MaxStack = DB ? DB->GetMaxStack(Item.ItemId) : 1;
|
||||
if (MaxStack <= 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||
{
|
||||
if (Remaining <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (E.ContainerId == ContainerId && E.Item.CanStackWith(Item) && E.Item.Quantity < MaxStack)
|
||||
{
|
||||
const int32 Space = MaxStack - E.Item.Quantity;
|
||||
const int32 Moved = FMath::Min(Space, Remaining);
|
||||
E.Item.Quantity += Moved;
|
||||
Remaining -= Moved;
|
||||
ReplicatedItems.MarkItemDirty(E);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool UInventoryComponent::AddItem(FItemInstanceData& Item)
|
||||
{
|
||||
if (!HasAuthorityChecked(TEXT("AddItem")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (const FInventoryContainer& C : Containers)
|
||||
{
|
||||
if (CanAcceptItem(Item, C.ContainerId))
|
||||
{
|
||||
return AddItemToContainer(Item, C.ContainerId);
|
||||
}
|
||||
}
|
||||
UE_LOG(LogItemInventory, Verbose, TEXT("AddItem: no container accepts %s x%d"), *Item.ItemId.ToString(), Item.Quantity);
|
||||
return false;
|
||||
}
|
||||
|
||||
void UInventoryComponent::StripServerOnlyProperties(FItemInstanceData& Item)
|
||||
{
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||
if (!DB)
|
||||
{
|
||||
return;
|
||||
}
|
||||
TArray<FGameplayTag> ServerOnly;
|
||||
DB->GetServerOnlyProperties(Item.ItemId, ServerOnly);
|
||||
if (ServerOnly.Num() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
FItemRuntimeProperties& Side = ServerOnlyProps.FindOrAdd(Item.InstanceId);
|
||||
for (const FGameplayTag& Tag : ServerOnly)
|
||||
{
|
||||
if (Item.RuntimeProperties.HasFloat(Tag))
|
||||
{
|
||||
Side.SetFloat(Tag, Item.RuntimeProperties.GetFloat(Tag));
|
||||
Item.RuntimeProperties.RemoveFloat(Tag); // never replicated to clients
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float UInventoryComponent::GetServerOnlyFloat(FGuid InstanceId, FGameplayTag PropertyTag, float Fallback) const
|
||||
{
|
||||
if (const FItemRuntimeProperties* Side = ServerOnlyProps.Find(InstanceId))
|
||||
{
|
||||
return Side->GetFloat(PropertyTag, Fallback);
|
||||
}
|
||||
return Fallback;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::AddItemToContainer(FItemInstanceData& Item, FGuid ContainerId)
|
||||
{
|
||||
if (!HasAuthorityChecked(TEXT("AddItemToContainer")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const FInventoryContainer* Container = GetContainerConst(ContainerId);
|
||||
if (!Container || !Item.IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Item.EnsureInstanceId();
|
||||
StripServerOnlyProperties(Item);
|
||||
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||
const int32 MaxStack = DB ? DB->GetMaxStack(Item.ItemId) : 1;
|
||||
|
||||
int32 Remaining = Item.Quantity;
|
||||
|
||||
// 1) Top up compatible existing stacks.
|
||||
StackIntoExisting(ContainerId, Item, Remaining);
|
||||
|
||||
// 2) Place leftovers into fresh slots, one stack at a time.
|
||||
bool bFirstNewStack = true;
|
||||
while (Remaining > 0)
|
||||
{
|
||||
const int32 Slot = FindFreeSlotForItem(ContainerId, Item);
|
||||
if (Slot == INDEX_NONE)
|
||||
{
|
||||
break; // container full
|
||||
}
|
||||
|
||||
const int32 ThisStack = FMath::Min(MaxStack, Remaining);
|
||||
FItemInstanceData NewStack = Item;
|
||||
NewStack.Quantity = ThisStack;
|
||||
NewStack.LocationType = EItemLocationType::Inventory;
|
||||
// First fresh stack keeps the incoming instance id; subsequent stacks get new ids.
|
||||
NewStack.InstanceId = bFirstNewStack ? Item.InstanceId : FGuid::NewGuid();
|
||||
bFirstNewStack = false;
|
||||
|
||||
FRepInventoryEntry& Entry = ReplicatedItems.AddOrUpdate(ContainerId, Slot, NewStack);
|
||||
NotifyItemAdded(Entry.Item);
|
||||
|
||||
Remaining -= ThisStack;
|
||||
}
|
||||
|
||||
const int32 AddedCount = Item.Quantity - Remaining;
|
||||
Item.Quantity = Remaining; // report leftovers back to caller
|
||||
if (AddedCount > 0)
|
||||
{
|
||||
NotifyInventoryChanged();
|
||||
}
|
||||
return Remaining == 0;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::RemoveItem(FGuid InstanceId)
|
||||
{
|
||||
if (!HasAuthorityChecked(TEXT("RemoveItem")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FItemInstanceData Removed;
|
||||
const bool bFound = FindItem(InstanceId, Removed);
|
||||
if (bFound && Removed.Lock.bLocked)
|
||||
{
|
||||
UE_LOG(LogItemInventory, Verbose, TEXT("RemoveItem blocked: %s is locked."), *InstanceId.ToString());
|
||||
return false;
|
||||
}
|
||||
if (ReplicatedItems.RemoveByInstanceId(InstanceId))
|
||||
{
|
||||
NotifyItemRemoved(Removed);
|
||||
NotifyInventoryChanged();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::RemoveQuantity(FGuid InstanceId, int32 Amount)
|
||||
{
|
||||
if (!HasAuthorityChecked(TEXT("RemoveQuantity")) || Amount <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FRepInventoryEntry* Entry = ReplicatedItems.FindByInstanceId(InstanceId);
|
||||
if (!Entry || Entry->Item.Lock.bLocked)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Amount >= Entry->Item.Quantity)
|
||||
{
|
||||
return RemoveItem(InstanceId);
|
||||
}
|
||||
Entry->Item.Quantity -= Amount;
|
||||
ReplicatedItems.MarkItemDirty(*Entry);
|
||||
NotifyInventoryChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::MoveItem(FGuid InstanceId, FGuid FromContainerId, FGuid ToContainerId, int32 TargetSlot)
|
||||
{
|
||||
if (!HasAuthorityChecked(TEXT("MoveItem")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FRepInventoryEntry* Entry = ReplicatedItems.FindByInstanceId(InstanceId);
|
||||
if (!Entry || Entry->ContainerId != FromContainerId || Entry->Item.Lock.bLocked)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!CanAcceptItem(Entry->Item, ToContainerId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve target slot.
|
||||
if (TargetSlot == INDEX_NONE)
|
||||
{
|
||||
TargetSlot = FindFreeSlot(ToContainerId);
|
||||
if (TargetSlot == INDEX_NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Occupant handling: merge if stackable, else swap.
|
||||
FRepInventoryEntry* Occupant = nullptr;
|
||||
for (FRepInventoryEntry& E : ReplicatedItems.Entries)
|
||||
{
|
||||
if (E.ContainerId == ToContainerId && E.SlotIndex == TargetSlot && E.Item.InstanceId != InstanceId)
|
||||
{
|
||||
Occupant = &E;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (Occupant)
|
||||
{
|
||||
if (Occupant->Item.CanStackWith(Entry->Item))
|
||||
{
|
||||
return MergeStack(InstanceId, Occupant->Item.InstanceId);
|
||||
}
|
||||
// Swap positions.
|
||||
const FGuid FromC = Entry->ContainerId;
|
||||
const int32 FromS = Entry->SlotIndex;
|
||||
Occupant->ContainerId = FromC;
|
||||
Occupant->SlotIndex = FromS;
|
||||
Occupant->Item.CurrentContainerId = FromC;
|
||||
Occupant->Item.SlotIndex = FromS;
|
||||
ReplicatedItems.MarkItemDirty(*Occupant);
|
||||
}
|
||||
|
||||
Entry->ContainerId = ToContainerId;
|
||||
Entry->SlotIndex = TargetSlot;
|
||||
Entry->Item.CurrentContainerId = ToContainerId;
|
||||
Entry->Item.SlotIndex = TargetSlot;
|
||||
ReplicatedItems.MarkItemDirty(*Entry);
|
||||
NotifyInventoryChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::SplitStack(FGuid InstanceId, int32 Amount)
|
||||
{
|
||||
if (!HasAuthorityChecked(TEXT("SplitStack")) || Amount <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FRepInventoryEntry* Entry = ReplicatedItems.FindByInstanceId(InstanceId);
|
||||
if (!Entry || Entry->Item.Lock.bLocked || Amount >= Entry->Item.Quantity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const FGuid ContainerId = Entry->ContainerId;
|
||||
const int32 FreeSlot = FindFreeSlotForItem(ContainerId, Entry->Item);
|
||||
if (FreeSlot == INDEX_NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Entry->Item.Quantity -= Amount;
|
||||
ReplicatedItems.MarkItemDirty(*Entry);
|
||||
|
||||
FItemInstanceData NewStack = Entry->Item;
|
||||
NewStack.InstanceId = FGuid::NewGuid();
|
||||
NewStack.Quantity = Amount;
|
||||
FRepInventoryEntry& Added = ReplicatedItems.AddOrUpdate(ContainerId, FreeSlot, NewStack);
|
||||
NotifyItemAdded(Added.Item);
|
||||
NotifyInventoryChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::MergeStack(FGuid SourceInstanceId, FGuid TargetInstanceId)
|
||||
{
|
||||
if (!HasAuthorityChecked(TEXT("MergeStack")) || SourceInstanceId == TargetInstanceId)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FRepInventoryEntry* Source = ReplicatedItems.FindByInstanceId(SourceInstanceId);
|
||||
FRepInventoryEntry* Target = ReplicatedItems.FindByInstanceId(TargetInstanceId);
|
||||
if (!Source || !Target || !Target->Item.CanStackWith(Source->Item))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(this);
|
||||
const int32 MaxStack = DB ? DB->GetMaxStack(Target->Item.ItemId) : 1;
|
||||
const int32 Space = MaxStack - Target->Item.Quantity;
|
||||
if (Space <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int32 Moved = FMath::Min(Space, Source->Item.Quantity);
|
||||
Target->Item.Quantity += Moved;
|
||||
Source->Item.Quantity -= Moved;
|
||||
ReplicatedItems.MarkItemDirty(*Target);
|
||||
|
||||
if (Source->Item.Quantity <= 0)
|
||||
{
|
||||
const FItemInstanceData Removed = Source->Item;
|
||||
ReplicatedItems.RemoveByInstanceId(SourceInstanceId);
|
||||
NotifyItemRemoved(Removed);
|
||||
}
|
||||
else
|
||||
{
|
||||
ReplicatedItems.MarkItemDirty(*Source);
|
||||
}
|
||||
NotifyInventoryChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::UpdateItem(const FItemInstanceData& Item)
|
||||
{
|
||||
if (!HasAuthorityChecked(TEXT("UpdateItem")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FRepInventoryEntry* Entry = ReplicatedItems.FindByInstanceId(Item.InstanceId);
|
||||
if (!Entry)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FItemInstanceData Stored = Item;
|
||||
StripServerOnlyProperties(Stored);
|
||||
Entry->Item = Stored;
|
||||
Entry->Item.CurrentContainerId = Entry->ContainerId;
|
||||
Entry->Item.SlotIndex = Entry->SlotIndex;
|
||||
ReplicatedItems.MarkItemDirty(*Entry);
|
||||
NotifyInventoryChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::SetItemLock(FGuid InstanceId, FGameplayTag Reason, AActor* LockedBy, float ExpireWorldTime)
|
||||
{
|
||||
if (!HasAuthorityChecked(TEXT("SetItemLock")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FRepInventoryEntry* Entry = ReplicatedItems.FindByInstanceId(InstanceId);
|
||||
if (!Entry)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Entry->Item.Lock.bLocked = true;
|
||||
Entry->Item.Lock.LockReason = Reason;
|
||||
Entry->Item.Lock.LockedBy = LockedBy;
|
||||
Entry->Item.Lock.LockExpireTime = ExpireWorldTime;
|
||||
ReplicatedItems.MarkItemDirty(*Entry);
|
||||
NotifyInventoryChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UInventoryComponent::ClearItemLock(FGuid InstanceId)
|
||||
{
|
||||
if (!HasAuthorityChecked(TEXT("ClearItemLock")))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FRepInventoryEntry* Entry = ReplicatedItems.FindByInstanceId(InstanceId);
|
||||
if (!Entry)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Entry->Item.Lock.Clear();
|
||||
ReplicatedItems.MarkItemDirty(*Entry);
|
||||
NotifyInventoryChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
void UInventoryComponent::SetItemInSlot(const FItemInstanceData& Item, FGuid ContainerId, int32 SlotIndex)
|
||||
{
|
||||
if (!HasAuthorityChecked(TEXT("SetItemInSlot")))
|
||||
{
|
||||
return;
|
||||
}
|
||||
FItemInstanceData Copy = Item;
|
||||
Copy.LocationType = EItemLocationType::Inventory;
|
||||
StripServerOnlyProperties(Copy);
|
||||
ReplicatedItems.AddOrUpdate(ContainerId, SlotIndex, Copy);
|
||||
NotifyInventoryChanged();
|
||||
}
|
||||
|
||||
void UInventoryComponent::ClearAllItems()
|
||||
{
|
||||
if (!HasAuthorityChecked(TEXT("ClearAllItems")))
|
||||
{
|
||||
return;
|
||||
}
|
||||
ReplicatedItems.Entries.Reset();
|
||||
ReplicatedItems.MarkArrayDirty();
|
||||
NotifyInventoryChanged();
|
||||
}
|
||||
|
||||
void UInventoryComponent::SetContainers(const TArray<FInventoryContainer>& InContainers)
|
||||
{
|
||||
if (!HasAuthorityChecked(TEXT("SetContainers")))
|
||||
{
|
||||
return;
|
||||
}
|
||||
Containers = InContainers;
|
||||
NotifyInventoryChanged();
|
||||
}
|
||||
|
||||
// --- Notifications ---
|
||||
|
||||
void UInventoryComponent::NotifyInventoryChanged()
|
||||
{
|
||||
OnInventoryChanged.Broadcast();
|
||||
}
|
||||
|
||||
void UInventoryComponent::NotifyItemAdded(const FItemInstanceData& Item)
|
||||
{
|
||||
OnItemAdded.Broadcast(Item);
|
||||
}
|
||||
|
||||
void UInventoryComponent::NotifyItemRemoved(const FItemInstanceData& Item)
|
||||
{
|
||||
OnItemRemoved.Broadcast(Item);
|
||||
}
|
||||
98
Source/ItemInventory/Private/ItemCraftingLibrary.cpp
Normal file
98
Source/ItemInventory/Private/ItemCraftingLibrary.cpp
Normal file
@ -0,0 +1,98 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "ItemCraftingLibrary.h"
|
||||
#include "ItemInventoryLog.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "ItemDatabaseSubsystem.h"
|
||||
#include "ItemContentRows.h"
|
||||
|
||||
bool UItemCraftingLibrary::CanCraft(const UObject* WorldContext, UInventoryComponent* Inventory, FName RecipeId, FGameplayTag AvailableStation)
|
||||
{
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext);
|
||||
if (!DB || !Inventory)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FCraftingRecipeRow Recipe;
|
||||
if (!DB->FindRecipe(RecipeId, Recipe))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Recipe.StationTag.IsValid() && !AvailableStation.MatchesTag(Recipe.StationTag))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (const FCraftingIngredient& In : Recipe.Inputs)
|
||||
{
|
||||
if (Inventory->GetItemCount(In.ItemId) < In.Quantity)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UItemCraftingLibrary::Craft(const UObject* WorldContext, UInventoryComponent* Inventory, FName RecipeId, FGameplayTag AvailableStation)
|
||||
{
|
||||
if (!Inventory || !Inventory->GetOwner() || !Inventory->GetOwner()->HasAuthority())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!CanCraft(WorldContext, Inventory, RecipeId, AvailableStation))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext);
|
||||
FCraftingRecipeRow Recipe;
|
||||
DB->FindRecipe(RecipeId, Recipe);
|
||||
|
||||
// Consume inputs (already verified present).
|
||||
for (const FCraftingIngredient& In : Recipe.Inputs)
|
||||
{
|
||||
if (!ConsumeItem(Inventory, In.ItemId, In.Quantity))
|
||||
{
|
||||
UE_LOG(LogItemInventory, Error, TEXT("Craft '%s' failed mid-consume for %s."), *RecipeId.ToString(), *In.ItemId.ToString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
FItemInstanceData Output = DB->MakeItemInstance(Recipe.OutputItemId, FMath::Max(1, Recipe.OutputQuantity));
|
||||
Inventory->AddItem(Output);
|
||||
UE_LOG(LogItemInventory, Log, TEXT("[Crafting] crafted %s"), *Recipe.OutputItemId.ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
void UItemCraftingLibrary::RollLootIntoInventory(const UObject* WorldContext, UInventoryComponent* Inventory, FName TableId, int32 RollCount)
|
||||
{
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext);
|
||||
if (!DB || !Inventory || !Inventory->GetOwner() || !Inventory->GetOwner()->HasAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
TArray<FItemInstanceData> Loot;
|
||||
DB->RollLoot(TableId, RollCount, Loot);
|
||||
for (FItemInstanceData& Item : Loot)
|
||||
{
|
||||
Inventory->AddItem(Item);
|
||||
}
|
||||
}
|
||||
|
||||
bool UItemCraftingLibrary::ConsumeItem(UInventoryComponent* Inventory, FName ItemId, int32 Count)
|
||||
{
|
||||
int32 Remaining = Count;
|
||||
TArray<FItemInstanceData> Items;
|
||||
Inventory->GetAllItems(Items);
|
||||
for (const FItemInstanceData& Item : Items)
|
||||
{
|
||||
if (Remaining <= 0) { break; }
|
||||
if (Item.ItemId != ItemId || Item.Lock.bLocked) { continue; }
|
||||
|
||||
const int32 Take = FMath::Min(Remaining, Item.Quantity);
|
||||
if (Inventory->RemoveQuantity(Item.InstanceId, Take))
|
||||
{
|
||||
Remaining -= Take;
|
||||
}
|
||||
}
|
||||
return Remaining <= 0;
|
||||
}
|
||||
165
Source/ItemInventory/Private/ItemFeatureLibrary.cpp
Normal file
165
Source/ItemInventory/Private/ItemFeatureLibrary.cpp
Normal file
@ -0,0 +1,165 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "ItemFeatureLibrary.h"
|
||||
#include "ItemInventoryLog.h"
|
||||
#include "ItemNativeTags.h"
|
||||
#include "ItemDatabaseSubsystem.h"
|
||||
#include "ItemAuxiliaryRows.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "Engine/World.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
float GetWorldSeconds(const UObject* WorldContext)
|
||||
{
|
||||
const UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::ReturnNull) : nullptr;
|
||||
return World ? World->GetTimeSeconds() : 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
bool UItemFeatureLibrary::UpdateLazyFeatures(const UObject* WorldContext, FItemInstanceData& Item, FName& OutTransformItemId)
|
||||
{
|
||||
OutTransformItemId = NAME_None;
|
||||
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext);
|
||||
if (!DB)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TArray<FItemFeatureRow> RottingRows;
|
||||
DB->GetFeatureRows(Item.ItemId, ItemEcosystemTags::Feature_Rotting, RottingRows);
|
||||
if (RottingRows.Num() == 0)
|
||||
{
|
||||
return false; // not perishable
|
||||
}
|
||||
|
||||
float DecayPerHour = 0.f;
|
||||
FName RottenItemId = NAME_None;
|
||||
for (const FItemFeatureRow& Row : RottingRows)
|
||||
{
|
||||
if (Row.Param == FName("DecayRatePerHour")) { DecayPerHour = Row.GetFloat(); }
|
||||
else if (Row.Param == FName("RottenItemId")) { RottenItemId = FName(*Row.Value); }
|
||||
}
|
||||
if (DecayPerHour <= 0.f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const float Now = GetWorldSeconds(WorldContext);
|
||||
const float Last = Item.RuntimeProperties.GetFloat(ItemEcosystemTags::Property_LastUpdateTime, Now);
|
||||
float Freshness = Item.RuntimeProperties.GetFloat(ItemEcosystemTags::Property_Freshness, 1.f);
|
||||
|
||||
const float ElapsedHours = FMath::Max(0.f, Now - Last) / 3600.f;
|
||||
Freshness = FMath::Clamp(Freshness - DecayPerHour * ElapsedHours, 0.f, 1.f);
|
||||
|
||||
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_Freshness, Freshness);
|
||||
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_LastUpdateTime, Now);
|
||||
|
||||
if (Freshness <= 0.f)
|
||||
{
|
||||
Item.StateTags.AddTag(ItemEcosystemTags::State_Rotten);
|
||||
if (!RottenItemId.IsNone())
|
||||
{
|
||||
OutTransformItemId = RottenItemId;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void UItemFeatureLibrary::UpdateInventoryFeatures(const UObject* WorldContext, UInventoryComponent* Inventory)
|
||||
{
|
||||
if (!Inventory || !Inventory->GetOwner() || !Inventory->GetOwner()->HasAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<FItemInstanceData> Items;
|
||||
Inventory->GetAllItems(Items);
|
||||
for (FItemInstanceData& Item : Items)
|
||||
{
|
||||
FName TransformId;
|
||||
if (!UpdateLazyFeatures(WorldContext, Item, TransformId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!TransformId.IsNone())
|
||||
{
|
||||
// Transform into the rotten variant, preserving quantity.
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext);
|
||||
FItemInstanceData NewItem = DB ? DB->MakeItemInstance(TransformId, Item.Quantity) : FItemInstanceData(TransformId, Item.Quantity);
|
||||
Inventory->RemoveItem(Item.InstanceId);
|
||||
Inventory->AddItem(NewItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
Inventory->UpdateItem(Item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UItemFeatureLibrary::ForceRot(const UObject* WorldContext, FItemInstanceData& Item, FName& OutTransformItemId)
|
||||
{
|
||||
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_Freshness, 0.f);
|
||||
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_LastUpdateTime, GetWorldSeconds(WorldContext));
|
||||
Item.StateTags.AddTag(ItemEcosystemTags::State_Rotten);
|
||||
|
||||
OutTransformItemId = NAME_None;
|
||||
if (const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext))
|
||||
{
|
||||
TArray<FItemFeatureRow> Rows;
|
||||
DB->GetFeatureRows(Item.ItemId, ItemEcosystemTags::Feature_Rotting, Rows);
|
||||
for (const FItemFeatureRow& Row : Rows)
|
||||
{
|
||||
if (Row.Param == FName("RottenItemId") && !Row.Value.IsEmpty())
|
||||
{
|
||||
OutTransformItemId = FName(*Row.Value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UItemFeatureLibrary::BreakItem(FItemInstanceData& Item)
|
||||
{
|
||||
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_Durability, 0.f);
|
||||
Item.StateTags.AddTag(ItemEcosystemTags::State_Broken);
|
||||
}
|
||||
|
||||
void UItemFeatureLibrary::DrainBattery(FItemInstanceData& Item, float DrainPerSecond, float Seconds)
|
||||
{
|
||||
const float Charge = Item.RuntimeProperties.GetFloat(ItemEcosystemTags::Property_BatteryCharge, 1.f);
|
||||
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_BatteryCharge,
|
||||
FMath::Clamp(Charge - DrainPerSecond * Seconds, 0.f, 1.f));
|
||||
}
|
||||
|
||||
float UItemFeatureLibrary::AddFuel(FItemInstanceData& Item, float Amount, float MaxFuel)
|
||||
{
|
||||
const float Current = Item.RuntimeProperties.GetFloat(ItemEcosystemTags::Property_FuelAmount, 0.f);
|
||||
const float NewValue = FMath::Clamp(Current + Amount, 0.f, MaxFuel);
|
||||
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_FuelAmount, NewValue);
|
||||
return NewValue - Current;
|
||||
}
|
||||
|
||||
float UItemFeatureLibrary::TransferFuel(FItemInstanceData& From, FItemInstanceData& To, float Amount, float ToMaxFuel)
|
||||
{
|
||||
const float Available = From.RuntimeProperties.GetFloat(ItemEcosystemTags::Property_FuelAmount, 0.f);
|
||||
const float ToCurrent = To.RuntimeProperties.GetFloat(ItemEcosystemTags::Property_FuelAmount, 0.f);
|
||||
const float Room = FMath::Max(0.f, ToMaxFuel - ToCurrent);
|
||||
const float Moved = FMath::Min3(Amount, Available, Room);
|
||||
if (Moved <= 0.f)
|
||||
{
|
||||
return 0.f;
|
||||
}
|
||||
From.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_FuelAmount, Available - Moved);
|
||||
To.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_FuelAmount, ToCurrent + Moved);
|
||||
return Moved;
|
||||
}
|
||||
|
||||
void UItemFeatureLibrary::SetWetness(FItemInstanceData& Item, float Wetness)
|
||||
{
|
||||
const float Clamped = FMath::Clamp(Wetness, 0.f, 1.f);
|
||||
Item.RuntimeProperties.SetFloat(ItemEcosystemTags::Property_Wetness, Clamped);
|
||||
if (Clamped > 0.5f) { Item.StateTags.AddTag(ItemEcosystemTags::State_Wet); }
|
||||
else { Item.StateTags.RemoveTag(ItemEcosystemTags::State_Wet); }
|
||||
}
|
||||
8
Source/ItemInventory/Private/ItemInventoryModule.cpp
Normal file
8
Source/ItemInventory/Private/ItemInventoryModule.cpp
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "ItemInventoryLog.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogItemInventory);
|
||||
|
||||
IMPLEMENT_MODULE(FDefaultModuleImpl, ItemInventory)
|
||||
79
Source/ItemInventory/Private/ItemSaveLibrary.cpp
Normal file
79
Source/ItemInventory/Private/ItemSaveLibrary.cpp
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "ItemSaveLibrary.h"
|
||||
#include "ItemInventoryLog.h"
|
||||
#include "InventoryComponent.h"
|
||||
#include "ItemDatabaseSubsystem.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
|
||||
FInventorySaveData UItemSaveLibrary::CaptureInventory(UInventoryComponent* Inventory)
|
||||
{
|
||||
FInventorySaveData Data;
|
||||
if (!Inventory)
|
||||
{
|
||||
return Data;
|
||||
}
|
||||
Inventory->GetAllItems(Data.Items);
|
||||
for (FItemInstanceData& Item : Data.Items)
|
||||
{
|
||||
Item.Lock.Clear(); // never persist transient locks (section 26)
|
||||
}
|
||||
Inventory->GetContainers(Data.Containers);
|
||||
return Data;
|
||||
}
|
||||
|
||||
void UItemSaveLibrary::RestoreInventory(const UObject* WorldContext, UInventoryComponent* Inventory, const FInventorySaveData& Data)
|
||||
{
|
||||
if (!Inventory || !Inventory->GetOwner() || !Inventory->GetOwner()->HasAuthority())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Inventory->SetContainers(Data.Containers);
|
||||
Inventory->ClearAllItems();
|
||||
|
||||
const UItemDatabaseSubsystem* DB = UItemDatabaseSubsystem::Get(WorldContext);
|
||||
for (const FItemInstanceData& Saved : Data.Items)
|
||||
{
|
||||
FItemInstanceData Item = Saved;
|
||||
if (DB) { DB->MigrateLoadedItem(Item); }
|
||||
Inventory->SetItemInSlot(Item, Item.CurrentContainerId, Item.SlotIndex);
|
||||
}
|
||||
UE_LOG(LogItemInventory, Log, TEXT("[Save] restored %d items into %s"), Data.Items.Num(), *GetNameSafe(Inventory->GetOwner()));
|
||||
}
|
||||
|
||||
bool UItemSaveLibrary::SaveInventoryToSlot(UInventoryComponent* Inventory, const FString& SlotName, FName Key)
|
||||
{
|
||||
if (!Inventory)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UItemSaveGame* Save = Cast<UItemSaveGame>(UGameplayStatics::LoadGameFromSlot(SlotName, 0));
|
||||
if (!Save)
|
||||
{
|
||||
Save = Cast<UItemSaveGame>(UGameplayStatics::CreateSaveGameObject(UItemSaveGame::StaticClass()));
|
||||
}
|
||||
if (!Save)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Save->Inventories.Add(Key, CaptureInventory(Inventory));
|
||||
return UGameplayStatics::SaveGameToSlot(Save, SlotName, 0);
|
||||
}
|
||||
|
||||
bool UItemSaveLibrary::LoadInventoryFromSlot(const UObject* WorldContext, UInventoryComponent* Inventory, const FString& SlotName, FName Key)
|
||||
{
|
||||
UItemSaveGame* Save = Cast<UItemSaveGame>(UGameplayStatics::LoadGameFromSlot(SlotName, 0));
|
||||
if (!Save)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const FInventorySaveData* Data = Save->Inventories.Find(Key);
|
||||
if (!Data)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
RestoreInventory(WorldContext, Inventory, *Data);
|
||||
return true;
|
||||
}
|
||||
173
Source/ItemInventory/Private/ItemTransaction.cpp
Normal file
173
Source/ItemInventory/Private/ItemTransaction.cpp
Normal file
@ -0,0 +1,173 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "ItemTransaction.h"
|
||||
#include "ItemInventoryLog.h"
|
||||
#include "InventoryComponent.h"
|
||||
|
||||
FItemTransaction& FItemTransaction::Add(UInventoryComponent* Inventory, const FItemInstanceData& Item)
|
||||
{
|
||||
FItemTransactionStep Step;
|
||||
Step.Type = EItemTransactionStepType::AddItem;
|
||||
Step.Inventory = Inventory;
|
||||
Step.Item = Item;
|
||||
Step.Item.EnsureInstanceId();
|
||||
Step.InstanceId = Step.Item.InstanceId;
|
||||
Steps.Add(Step);
|
||||
return *this;
|
||||
}
|
||||
|
||||
FItemTransaction& FItemTransaction::Remove(UInventoryComponent* Inventory, const FGuid& InstanceId)
|
||||
{
|
||||
FItemTransactionStep Step;
|
||||
Step.Type = EItemTransactionStepType::RemoveItem;
|
||||
Step.Inventory = Inventory;
|
||||
Step.InstanceId = InstanceId;
|
||||
Steps.Add(Step);
|
||||
return *this;
|
||||
}
|
||||
|
||||
FItemTransaction& FItemTransaction::Update(UInventoryComponent* Inventory, const FItemInstanceData& Item)
|
||||
{
|
||||
FItemTransactionStep Step;
|
||||
Step.Type = EItemTransactionStepType::UpdateItem;
|
||||
Step.Inventory = Inventory;
|
||||
Step.Item = Item;
|
||||
Step.InstanceId = Item.InstanceId;
|
||||
Steps.Add(Step);
|
||||
return *this;
|
||||
}
|
||||
|
||||
FItemTransaction& FItemTransaction::Move(UInventoryComponent* Inventory, const FGuid& InstanceId, const FGuid& ToContainerId, int32 ToSlotIndex)
|
||||
{
|
||||
FItemTransactionStep Step;
|
||||
Step.Type = EItemTransactionStepType::MoveItem;
|
||||
Step.Inventory = Inventory;
|
||||
Step.InstanceId = InstanceId;
|
||||
Step.ToContainer = ToContainerId;
|
||||
Step.ToSlot = ToSlotIndex;
|
||||
Steps.Add(Step);
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool FItemTransaction::Validate() const
|
||||
{
|
||||
for (const FItemTransactionStep& Step : Steps)
|
||||
{
|
||||
UInventoryComponent* Inv = Step.Inventory.Get();
|
||||
if (!Inv)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
switch (Step.Type)
|
||||
{
|
||||
case EItemTransactionStepType::AddItem:
|
||||
{
|
||||
// At least one container must accept it.
|
||||
TArray<FInventoryContainer> Containers;
|
||||
Inv->GetContainers(Containers);
|
||||
bool bAccepts = false;
|
||||
for (const FInventoryContainer& C : Containers)
|
||||
{
|
||||
if (Inv->CanAcceptItem(Step.Item, C.ContainerId)) { bAccepts = true; break; }
|
||||
}
|
||||
if (!bAccepts) { return false; }
|
||||
break;
|
||||
}
|
||||
case EItemTransactionStepType::RemoveItem:
|
||||
case EItemTransactionStepType::UpdateItem:
|
||||
case EItemTransactionStepType::MoveItem:
|
||||
{
|
||||
FItemInstanceData Found;
|
||||
if (!Inv->FindItem(Step.InstanceId, Found) || Found.Lock.bLocked) { return false; }
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FItemTransaction::Apply()
|
||||
{
|
||||
if (!Validate())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (FItemTransactionStep& Step : Steps)
|
||||
{
|
||||
UInventoryComponent* Inv = Step.Inventory.Get();
|
||||
bool bOk = false;
|
||||
switch (Step.Type)
|
||||
{
|
||||
case EItemTransactionStepType::AddItem:
|
||||
{
|
||||
FItemInstanceData ToAdd = Step.Item;
|
||||
bOk = Inv->AddItem(ToAdd);
|
||||
break;
|
||||
}
|
||||
case EItemTransactionStepType::RemoveItem:
|
||||
{
|
||||
Inv->FindItem(Step.InstanceId, Step.UndoSnapshot); // capture for rollback
|
||||
bOk = Inv->RemoveItem(Step.InstanceId);
|
||||
break;
|
||||
}
|
||||
case EItemTransactionStepType::UpdateItem:
|
||||
{
|
||||
Inv->FindItem(Step.InstanceId, Step.UndoSnapshot);
|
||||
bOk = Inv->UpdateItem(Step.Item);
|
||||
break;
|
||||
}
|
||||
case EItemTransactionStepType::MoveItem:
|
||||
{
|
||||
Inv->FindItem(Step.InstanceId, Step.UndoSnapshot);
|
||||
Step.UndoFromContainer = Step.UndoSnapshot.CurrentContainerId;
|
||||
Step.UndoFromSlot = Step.UndoSnapshot.SlotIndex;
|
||||
bOk = Inv->MoveItem(Step.InstanceId, Step.UndoFromContainer, Step.ToContainer, Step.ToSlot);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bOk)
|
||||
{
|
||||
UE_LOG(LogItemInventory, Warning, TEXT("[Transaction %s] step failed; rolling back."), *TransactionId.ToString());
|
||||
Rollback();
|
||||
return false;
|
||||
}
|
||||
Step.bApplied = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void FItemTransaction::Rollback()
|
||||
{
|
||||
// Undo applied steps in reverse order.
|
||||
for (int32 i = Steps.Num() - 1; i >= 0; --i)
|
||||
{
|
||||
FItemTransactionStep& Step = Steps[i];
|
||||
if (!Step.bApplied)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
UInventoryComponent* Inv = Step.Inventory.Get();
|
||||
if (!Inv)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
switch (Step.Type)
|
||||
{
|
||||
case EItemTransactionStepType::AddItem:
|
||||
Inv->RemoveItem(Step.InstanceId);
|
||||
break;
|
||||
case EItemTransactionStepType::RemoveItem:
|
||||
if (Step.UndoSnapshot.IsValid()) { FItemInstanceData Restore = Step.UndoSnapshot; Inv->AddItem(Restore); }
|
||||
break;
|
||||
case EItemTransactionStepType::UpdateItem:
|
||||
if (Step.UndoSnapshot.IsValid()) { Inv->UpdateItem(Step.UndoSnapshot); }
|
||||
break;
|
||||
case EItemTransactionStepType::MoveItem:
|
||||
Inv->MoveItem(Step.InstanceId, Step.ToContainer, Step.UndoFromContainer, Step.UndoFromSlot);
|
||||
break;
|
||||
}
|
||||
Step.bApplied = false;
|
||||
}
|
||||
}
|
||||
103
Source/ItemInventory/Private/ReplicatedInventoryList.cpp
Normal file
103
Source/ItemInventory/Private/ReplicatedInventoryList.cpp
Normal file
@ -0,0 +1,103 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "ReplicatedInventoryList.h"
|
||||
#include "InventoryComponent.h"
|
||||
|
||||
void FReplicatedInventoryList::PostReplicatedAdd(const TArrayView<int32>& AddedIndices, int32 /*FinalSize*/)
|
||||
{
|
||||
if (!OwnerComponent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (int32 Index : AddedIndices)
|
||||
{
|
||||
if (Entries.IsValidIndex(Index))
|
||||
{
|
||||
OwnerComponent->NotifyItemAdded(Entries[Index].Item);
|
||||
}
|
||||
}
|
||||
OwnerComponent->NotifyInventoryChanged();
|
||||
}
|
||||
|
||||
void FReplicatedInventoryList::PostReplicatedChange(const TArrayView<int32>& /*ChangedIndices*/, int32 /*FinalSize*/)
|
||||
{
|
||||
if (OwnerComponent)
|
||||
{
|
||||
OwnerComponent->NotifyInventoryChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void FReplicatedInventoryList::PreReplicatedRemove(const TArrayView<int32>& RemovedIndices, int32 /*FinalSize*/)
|
||||
{
|
||||
if (!OwnerComponent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (int32 Index : RemovedIndices)
|
||||
{
|
||||
if (Entries.IsValidIndex(Index))
|
||||
{
|
||||
OwnerComponent->NotifyItemRemoved(Entries[Index].Item);
|
||||
}
|
||||
}
|
||||
OwnerComponent->NotifyInventoryChanged();
|
||||
}
|
||||
|
||||
FRepInventoryEntry& FReplicatedInventoryList::AddOrUpdate(const FGuid& Container, int32 Slot, const FItemInstanceData& Item)
|
||||
{
|
||||
if (FRepInventoryEntry* Existing = FindByInstanceId(Item.InstanceId))
|
||||
{
|
||||
Existing->ContainerId = Container;
|
||||
Existing->SlotIndex = Slot;
|
||||
Existing->Item = Item;
|
||||
Existing->Item.CurrentContainerId = Container;
|
||||
Existing->Item.SlotIndex = Slot;
|
||||
MarkItemDirty(*Existing);
|
||||
return *Existing;
|
||||
}
|
||||
|
||||
FRepInventoryEntry& New = Entries.Emplace_GetRef(Container, Slot, Item);
|
||||
New.Item.CurrentContainerId = Container;
|
||||
New.Item.SlotIndex = Slot;
|
||||
MarkItemDirty(New);
|
||||
return New;
|
||||
}
|
||||
|
||||
bool FReplicatedInventoryList::RemoveByInstanceId(const FGuid& InstanceId)
|
||||
{
|
||||
const int32 Index = Entries.IndexOfByPredicate([&InstanceId](const FRepInventoryEntry& E)
|
||||
{
|
||||
return E.Item.InstanceId == InstanceId;
|
||||
});
|
||||
if (Index == INDEX_NONE)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Entries.RemoveAt(Index);
|
||||
MarkArrayDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
void FReplicatedInventoryList::MarkEntryDirtyByInstanceId(const FGuid& InstanceId)
|
||||
{
|
||||
if (FRepInventoryEntry* Entry = FindByInstanceId(InstanceId))
|
||||
{
|
||||
MarkItemDirty(*Entry);
|
||||
}
|
||||
}
|
||||
|
||||
FRepInventoryEntry* FReplicatedInventoryList::FindByInstanceId(const FGuid& InstanceId)
|
||||
{
|
||||
return Entries.FindByPredicate([&InstanceId](const FRepInventoryEntry& E)
|
||||
{
|
||||
return E.Item.InstanceId == InstanceId;
|
||||
});
|
||||
}
|
||||
|
||||
const FRepInventoryEntry* FReplicatedInventoryList::FindByInstanceId(const FGuid& InstanceId) const
|
||||
{
|
||||
return Entries.FindByPredicate([&InstanceId](const FRepInventoryEntry& E)
|
||||
{
|
||||
return E.Item.InstanceId == InstanceId;
|
||||
});
|
||||
}
|
||||
195
Source/ItemInventory/Public/InventoryComponent.h
Normal file
195
Source/ItemInventory/Public/InventoryComponent.h
Normal file
@ -0,0 +1,195 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "ReplicatedInventoryList.h"
|
||||
#include "InventoryContainer.h"
|
||||
#include "InventoryComponent.generated.h"
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnInventoryChangedSignature);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInventoryItemEventSignature, const FItemInstanceData&, Item);
|
||||
|
||||
/**
|
||||
* Owns and replicates a set of containers full of items (section 7).
|
||||
*
|
||||
* Server-authoritative: every mutating method must run on the server (the
|
||||
* interaction/transaction layer is the only intended caller). Clients receive
|
||||
* state through the FastArray and react via OnInventoryChanged / OnRep. The UI
|
||||
* never mutates this directly (section 25).
|
||||
*/
|
||||
UCLASS(ClassGroup = (Inventory), meta = (BlueprintSpawnableComponent))
|
||||
class ITEMINVENTORY_API UInventoryComponent : public UActorComponent
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UInventoryComponent();
|
||||
|
||||
//~ UActorComponent
|
||||
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
|
||||
virtual void InitializeComponent() override;
|
||||
virtual void OnRegister() override;
|
||||
|
||||
// --- Events (UI binds here, never polls - section 32) ---
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Inventory")
|
||||
FOnInventoryChangedSignature OnInventoryChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Inventory")
|
||||
FOnInventoryItemEventSignature OnItemAdded;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Inventory")
|
||||
FOnInventoryItemEventSignature OnItemRemoved;
|
||||
|
||||
/**
|
||||
* When true (default) inventory replicates only to the owning client (player
|
||||
* inventory, section 25.9). Set false for shared world containers (loot chests,
|
||||
* vehicle cargo) so nearby clients can see the contents. Must be set before the
|
||||
* component registers replication (e.g. in the owning actor's constructor).
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory|Replication")
|
||||
bool bOwnerOnlyReplication = true;
|
||||
|
||||
// --- Container setup ---
|
||||
|
||||
/** Registers a container. Assigns a new ContainerId if unset. Server side. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||
FGuid AddContainer(FInventoryContainer Container);
|
||||
|
||||
/** Registers a container from a DT_Containers template (section 6). Server. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||
FGuid AddContainerByTag(FGameplayTag ContainerTag);
|
||||
|
||||
/** Creates a nested container owned by an item that has Feature.Container (section 7). */
|
||||
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||
FGuid AddNestedContainerForItem(FGuid OwnerItemInstanceId, FGameplayTag ContainerTag);
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||
bool GetContainer(FGuid ContainerId, FInventoryContainer& OutContainer) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||
void GetContainers(TArray<FInventoryContainer>& OutContainers) const { OutContainers = Containers; }
|
||||
|
||||
/** First container whose tag matches, or an invalid guid. */
|
||||
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||
FGuid FindContainerByTag(FGameplayTag ContainerTag) const;
|
||||
|
||||
// --- Core operations (section 7.1) ---
|
||||
|
||||
/** Adds an item, stacking into existing compatible stacks then free slots. Server. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||
bool AddItem(UPARAM(ref) FItemInstanceData& Item);
|
||||
|
||||
/** Adds an item into a specific container. Server. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||
bool AddItemToContainer(UPARAM(ref) FItemInstanceData& Item, FGuid ContainerId);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||
bool RemoveItem(FGuid InstanceId);
|
||||
|
||||
/** Removes up to Amount from a stack; removes the entry when it hits zero. Server. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||
bool RemoveQuantity(FGuid InstanceId, int32 Amount);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||
bool MoveItem(FGuid InstanceId, FGuid FromContainerId, FGuid ToContainerId, int32 TargetSlot);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||
bool SplitStack(FGuid InstanceId, int32 Amount);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||
bool MergeStack(FGuid SourceInstanceId, FGuid TargetInstanceId);
|
||||
|
||||
// --- Queries ---
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||
bool CanAcceptItem(const FItemInstanceData& Item, FGuid ContainerId) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||
bool FindItem(FGuid InstanceId, FItemInstanceData& OutItem) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||
void GetContainerItems(FGuid ContainerId, TArray<FItemInstanceData>& OutItems) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||
void GetAllItems(TArray<FItemInstanceData>& OutItems) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||
float GetContainerWeight(FGuid ContainerId) const;
|
||||
|
||||
/** Total quantity of a given ItemId across all containers. */
|
||||
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||
int32 GetItemCount(FName ItemId) const;
|
||||
|
||||
// --- Internal mutation used by the transaction layer ---
|
||||
|
||||
/** Writes a mutated copy back into the list and replicates it. Server. */
|
||||
bool UpdateItem(const FItemInstanceData& Item);
|
||||
|
||||
/** Server: place a soft lock on an item so it cannot be moved/taken (section 20). */
|
||||
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||
bool SetItemLock(FGuid InstanceId, FGameplayTag Reason, AActor* LockedBy, float ExpireWorldTime);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||
bool ClearItemLock(FGuid InstanceId);
|
||||
|
||||
/** Reads a ServerOnly property kept off the wire (section 22). Server only. */
|
||||
UFUNCTION(BlueprintPure, Category = "Inventory")
|
||||
float GetServerOnlyFloat(FGuid InstanceId, FGameplayTag PropertyTag, float Fallback = 0.f) const;
|
||||
|
||||
/** Direct, unchecked write of an item into a slot (used by transactions). Server. */
|
||||
void SetItemInSlot(const FItemInstanceData& Item, FGuid ContainerId, int32 SlotIndex);
|
||||
|
||||
/** Server: remove every item (save load / debug clear). */
|
||||
UFUNCTION(BlueprintCallable, Category = "Inventory")
|
||||
void ClearAllItems();
|
||||
|
||||
/** Server: replace the container set (save load). */
|
||||
void SetContainers(const TArray<FInventoryContainer>& InContainers);
|
||||
|
||||
/** Broadcasts OnInventoryChanged. Called by the FastArray on clients too. */
|
||||
void NotifyInventoryChanged();
|
||||
void NotifyItemAdded(const FItemInstanceData& Item);
|
||||
void NotifyItemRemoved(const FItemInstanceData& Item);
|
||||
|
||||
protected:
|
||||
UPROPERTY(Replicated)
|
||||
FReplicatedInventoryList ReplicatedItems;
|
||||
|
||||
UPROPERTY(Replicated)
|
||||
TArray<FInventoryContainer> Containers;
|
||||
|
||||
/** Default containers spawned in InitializeComponent (designer-configurable). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory|Setup")
|
||||
TArray<FInventoryContainer> DefaultContainers;
|
||||
|
||||
private:
|
||||
bool HasAuthorityChecked(const TCHAR* Op) const;
|
||||
|
||||
FInventoryContainer* GetContainerMutable(const FGuid& ContainerId);
|
||||
const FInventoryContainer* GetContainerConst(const FGuid& ContainerId) const;
|
||||
|
||||
/** Lowest free slot index in a slot-limited container, or INDEX_NONE if full/unlimited-ok. */
|
||||
int32 FindFreeSlot(const FGuid& ContainerId) const;
|
||||
|
||||
/** Grid-aware free slot for a specific item (uses its GridSize); falls back to FindFreeSlot. */
|
||||
int32 FindFreeSlotForItem(const FGuid& ContainerId, const FItemInstanceData& Item) const;
|
||||
|
||||
FIntPoint GetItemGridSize(FName ItemId) const;
|
||||
|
||||
/** True if the WxH rectangle at TopLeft is inside the grid and unoccupied (ignoring IgnoreInstance). */
|
||||
bool IsGridRegionFree(const FInventoryContainer& Container, int32 TopLeft, FIntPoint Size, const FGuid& IgnoreInstance) const;
|
||||
|
||||
bool IsSlotOccupied(const FGuid& ContainerId, int32 SlotIndex) const;
|
||||
|
||||
/** Tries to top up existing stacks of the same item; reduces Remaining accordingly. */
|
||||
void StackIntoExisting(const FGuid& ContainerId, FItemInstanceData& Item, int32& Remaining);
|
||||
|
||||
/** Moves an item's ServerOnly properties into the server-side side channel (section 22). */
|
||||
void StripServerOnlyProperties(FItemInstanceData& Item);
|
||||
|
||||
/** Server-only properties never replicated to clients, keyed by instance id. */
|
||||
TMap<FGuid, FItemRuntimeProperties> ServerOnlyProps;
|
||||
};
|
||||
74
Source/ItemInventory/Public/InventoryContainer.h
Normal file
74
Source/ItemInventory/Public/InventoryContainer.h
Normal file
@ -0,0 +1,74 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "ItemEnums.h"
|
||||
#include "InventoryContainer.generated.h"
|
||||
|
||||
/**
|
||||
* Metadata describing one container owned by an inventory (section 7.2).
|
||||
*
|
||||
* NOTE (design deviation from the TЗ sketch): the actual item entries do NOT live
|
||||
* inside the container struct. They live in the component's single
|
||||
* FReplicatedInventoryList, tagged with this ContainerId. A FastArraySerializer
|
||||
* only delta-replicates correctly as a direct UPROPERTY, so nesting one inside a
|
||||
* replicated TArray<FInventoryContainer> would silently fall back to full-array
|
||||
* replication. Keeping containers as lightweight metadata fixes that (section 32).
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct ITEMINVENTORY_API FInventoryContainer
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
FGuid ContainerId;
|
||||
|
||||
/** Logical identity, e.g. Container.Backpack / Container.Hotbar / Container.Vehicle.Cargo. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
FGameplayTag ContainerTag;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
EInventoryContainerType Type = EInventoryContainerType::SlotList;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
int32 Width = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
int32 Height = 0;
|
||||
|
||||
/** Used by SlotList/Equipment/Hotbar; for Grid types capacity is Width*Height. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
int32 MaxSlots = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
float MaxWeight = 0.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
FGameplayTagContainer AcceptedItemTags;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
FGameplayTagContainer BlockedItemTags;
|
||||
|
||||
/** Set when this container lives inside another item (a backpack/pouch item) - section 7. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Container")
|
||||
FGuid OwnerItemInstanceId;
|
||||
|
||||
FInventoryContainer() = default;
|
||||
|
||||
bool IsNested() const { return OwnerItemInstanceId.IsValid(); }
|
||||
|
||||
/** Number of addressable slots in this container (0 = unlimited weight-gated list). */
|
||||
int32 GetSlotCount() const
|
||||
{
|
||||
if (Type == EInventoryContainerType::Grid)
|
||||
{
|
||||
return FMath::Max(0, Width) * FMath::Max(0, Height);
|
||||
}
|
||||
return FMath::Max(0, MaxSlots);
|
||||
}
|
||||
|
||||
bool HasSlotLimit() const { return GetSlotCount() > 0; }
|
||||
bool HasWeightLimit() const { return MaxWeight > 0.f; }
|
||||
};
|
||||
38
Source/ItemInventory/Public/ItemCraftingLibrary.h
Normal file
38
Source/ItemInventory/Public/ItemCraftingLibrary.h
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "ItemCraftingLibrary.generated.h"
|
||||
|
||||
class UInventoryComponent;
|
||||
|
||||
/**
|
||||
* Crafting + loot helpers (section 6). All server-authoritative: crafting consumes
|
||||
* inputs and produces the output through the inventory's validated operations;
|
||||
* loot rolls fresh instances from a loot table into a container.
|
||||
*/
|
||||
UCLASS()
|
||||
class ITEMINVENTORY_API UItemCraftingLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** True if Inventory holds every ingredient (and the station requirement is met). */
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Crafting", meta = (WorldContext = "WorldContext"))
|
||||
static bool CanCraft(const UObject* WorldContext, UInventoryComponent* Inventory, FName RecipeId, FGameplayTag AvailableStation);
|
||||
|
||||
/** Server: consume the recipe inputs and add its output. Returns true on success. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Crafting", meta = (WorldContext = "WorldContext"))
|
||||
static bool Craft(const UObject* WorldContext, UInventoryComponent* Inventory, FName RecipeId, FGameplayTag AvailableStation);
|
||||
|
||||
/** Server: roll a loot table and add the results into an inventory. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Loot", meta = (WorldContext = "WorldContext"))
|
||||
static void RollLootIntoInventory(const UObject* WorldContext, UInventoryComponent* Inventory, FName TableId, int32 RollCount);
|
||||
|
||||
private:
|
||||
/** Removes Count of ItemId across stacks. Returns true if the full amount was removed. */
|
||||
static bool ConsumeItem(UInventoryComponent* Inventory, FName ItemId, int32 Count);
|
||||
};
|
||||
56
Source/ItemInventory/Public/ItemFeatureLibrary.h
Normal file
56
Source/ItemInventory/Public/ItemFeatureLibrary.h
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "ItemInstanceData.h"
|
||||
#include "ItemFeatureLibrary.generated.h"
|
||||
|
||||
class UInventoryComponent;
|
||||
|
||||
/**
|
||||
* Feature processing (section 21). Everything is LAZY: nothing ticks per item.
|
||||
* Call these on events - container open, save load, interaction, world-time pulse.
|
||||
* State (freshness, last-update time, charge, fuel) lives in the item's runtime
|
||||
* properties so it travels with the instance.
|
||||
*/
|
||||
UCLASS()
|
||||
class ITEMINVENTORY_API UItemFeatureLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/**
|
||||
* Recomputes time-based features (rotting) for one item up to "now".
|
||||
* Returns true if the item changed. If the item should transform (e.g. raw_meat
|
||||
* -> rotten_meat) OutTransformItemId is set to the replacement id.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Features", meta = (WorldContext = "WorldContext"))
|
||||
static bool UpdateLazyFeatures(const UObject* WorldContext, UPARAM(ref) FItemInstanceData& Item, FName& OutTransformItemId);
|
||||
|
||||
/** Server: run lazy updates across a whole inventory, applying any transforms. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Features", meta = (WorldContext = "WorldContext"))
|
||||
static void UpdateInventoryFeatures(const UObject* WorldContext, UInventoryComponent* Inventory);
|
||||
|
||||
// --- Direct feature operations (debug tools, gameplay) ---
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Features", meta = (WorldContext = "WorldContext"))
|
||||
static void ForceRot(const UObject* WorldContext, UPARAM(ref) FItemInstanceData& Item, FName& OutTransformItemId);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Features")
|
||||
static void BreakItem(UPARAM(ref) FItemInstanceData& Item);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Features")
|
||||
static void DrainBattery(UPARAM(ref) FItemInstanceData& Item, float DrainPerSecond, float Seconds);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Features")
|
||||
static float AddFuel(UPARAM(ref) FItemInstanceData& Item, float Amount, float MaxFuel);
|
||||
|
||||
/** Moves up to Amount of fuel from one item to another, respecting ToMaxFuel. Returns moved. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Features")
|
||||
static float TransferFuel(UPARAM(ref) FItemInstanceData& From, UPARAM(ref) FItemInstanceData& To, float Amount, float ToMaxFuel);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Features")
|
||||
static void SetWetness(UPARAM(ref) FItemInstanceData& Item, float Wetness);
|
||||
};
|
||||
8
Source/ItemInventory/Public/ItemInventoryLog.h
Normal file
8
Source/ItemInventory/Public/ItemInventoryLog.h
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Logging/LogMacros.h"
|
||||
|
||||
ITEMINVENTORY_API DECLARE_LOG_CATEGORY_EXTERN(LogItemInventory, Log, All);
|
||||
34
Source/ItemInventory/Public/ItemSaveGame.h
Normal file
34
Source/ItemInventory/Public/ItemSaveGame.h
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/SaveGame.h"
|
||||
#include "ItemInstanceData.h"
|
||||
#include "InventoryContainer.h"
|
||||
#include "ItemSaveGame.generated.h"
|
||||
|
||||
/** Saved snapshot of one inventory (section 26). Only ids/state are kept. */
|
||||
USTRUCT(BlueprintType)
|
||||
struct ITEMINVENTORY_API FInventorySaveData
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(SaveGame)
|
||||
TArray<FItemInstanceData> Items;
|
||||
|
||||
UPROPERTY(SaveGame)
|
||||
TArray<FInventoryContainer> Containers;
|
||||
};
|
||||
|
||||
/** SaveGame object holding any number of named inventory snapshots. */
|
||||
UCLASS()
|
||||
class ITEMINVENTORY_API UItemSaveGame : public USaveGame
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Keyed by a stable inventory id chosen by the game (e.g. "Player0", a chest guid). */
|
||||
UPROPERTY(SaveGame)
|
||||
TMap<FName, FInventorySaveData> Inventories;
|
||||
};
|
||||
38
Source/ItemInventory/Public/ItemSaveLibrary.h
Normal file
38
Source/ItemInventory/Public/ItemSaveLibrary.h
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Kismet/BlueprintFunctionLibrary.h"
|
||||
#include "ItemSaveGame.h"
|
||||
#include "ItemSaveLibrary.generated.h"
|
||||
|
||||
class UInventoryComponent;
|
||||
|
||||
/**
|
||||
* Save / load of inventories with redirect + version migration (section 26).
|
||||
* Stores only ItemId/InstanceId/Quantity/RuntimeProperties/StateTags/container/slot
|
||||
* - never icons, meshes, names or actor pointers (section 26 "do not save").
|
||||
*/
|
||||
UCLASS()
|
||||
class ITEMINVENTORY_API UItemSaveLibrary : public UBlueprintFunctionLibrary
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Snapshots an inventory's items + containers (drops transient locks). */
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Save")
|
||||
static FInventorySaveData CaptureInventory(UInventoryComponent* Inventory);
|
||||
|
||||
/** Server: rebuilds an inventory from a snapshot, migrating each item. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Save", meta = (WorldContext = "WorldContext"))
|
||||
static void RestoreInventory(const UObject* WorldContext, UInventoryComponent* Inventory, const FInventorySaveData& Data);
|
||||
|
||||
/** Writes one inventory under Key into a save slot (loads/creates the slot file). */
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Save")
|
||||
static bool SaveInventoryToSlot(UInventoryComponent* Inventory, const FString& SlotName, FName Key);
|
||||
|
||||
/** Server: loads one inventory under Key from a save slot. */
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Save", meta = (WorldContext = "WorldContext"))
|
||||
static bool LoadInventoryFromSlot(const UObject* WorldContext, UInventoryComponent* Inventory, const FString& SlotName, FName Key);
|
||||
};
|
||||
101
Source/ItemInventory/Public/ItemTransaction.h
Normal file
101
Source/ItemInventory/Public/ItemTransaction.h
Normal file
@ -0,0 +1,101 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "ItemInstanceData.h"
|
||||
#include "ItemTransaction.generated.h"
|
||||
|
||||
class UInventoryComponent;
|
||||
|
||||
/** Kind of mutation a transaction step performs (section 10). */
|
||||
UENUM(BlueprintType)
|
||||
enum class EItemTransactionStepType : uint8
|
||||
{
|
||||
AddItem,
|
||||
RemoveItem,
|
||||
UpdateItem,
|
||||
MoveItem
|
||||
};
|
||||
|
||||
/** One atomic step. Carries enough to apply AND to roll back. */
|
||||
USTRUCT(BlueprintType)
|
||||
struct ITEMINVENTORY_API FItemTransactionStep
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY()
|
||||
EItemTransactionStepType Type = EItemTransactionStepType::AddItem;
|
||||
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<UInventoryComponent> Inventory;
|
||||
|
||||
/** Item payload for AddItem / UpdateItem. */
|
||||
UPROPERTY()
|
||||
FItemInstanceData Item;
|
||||
|
||||
UPROPERTY()
|
||||
FGuid InstanceId;
|
||||
|
||||
UPROPERTY()
|
||||
FGuid ToContainer;
|
||||
|
||||
UPROPERTY()
|
||||
int32 ToSlot = INDEX_NONE;
|
||||
|
||||
// --- runtime (rollback bookkeeping) ---
|
||||
UPROPERTY()
|
||||
bool bApplied = false;
|
||||
|
||||
UPROPERTY()
|
||||
FItemInstanceData UndoSnapshot;
|
||||
|
||||
UPROPERTY()
|
||||
FGuid UndoFromContainer;
|
||||
|
||||
UPROPERTY()
|
||||
int32 UndoFromSlot = INDEX_NONE;
|
||||
};
|
||||
|
||||
/**
|
||||
* Atomic multi-step item operation (section 10). Build steps, then Apply():
|
||||
* if any step fails, every applied step is rolled back so the inventories are
|
||||
* left untouched. The single correct way to do multi-inventory moves.
|
||||
*
|
||||
* Server-only. Operates through UInventoryComponent's authoritative methods.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct ITEMINVENTORY_API FItemTransaction
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY()
|
||||
FGuid TransactionId;
|
||||
|
||||
UPROPERTY()
|
||||
FGameplayTag TransactionType;
|
||||
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<AActor> Instigator;
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FItemTransactionStep> Steps;
|
||||
|
||||
FItemTransaction() { TransactionId = FGuid::NewGuid(); }
|
||||
|
||||
// --- Builders ---
|
||||
FItemTransaction& Add(UInventoryComponent* Inventory, const FItemInstanceData& Item);
|
||||
FItemTransaction& Remove(UInventoryComponent* Inventory, const FGuid& InstanceId);
|
||||
FItemTransaction& Update(UInventoryComponent* Inventory, const FItemInstanceData& Item);
|
||||
FItemTransaction& Move(UInventoryComponent* Inventory, const FGuid& InstanceId, const FGuid& ToContainerId, int32 ToSlotIndex);
|
||||
|
||||
/** Non-mutating feasibility check of every step. */
|
||||
bool Validate() const;
|
||||
|
||||
/** Applies all steps; on any failure rolls back and returns false. */
|
||||
bool Apply();
|
||||
|
||||
/** Undoes every applied step in reverse order. */
|
||||
void Rollback();
|
||||
};
|
||||
84
Source/ItemInventory/Public/ReplicatedInventoryList.h
Normal file
84
Source/ItemInventory/Public/ReplicatedInventoryList.h
Normal file
@ -0,0 +1,84 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Net/Serialization/FastArraySerializer.h"
|
||||
#include "ItemInstanceData.h"
|
||||
#include "ReplicatedInventoryList.generated.h"
|
||||
|
||||
class UInventoryComponent;
|
||||
|
||||
/**
|
||||
* One replicated item slot (section 7.3). The item carries its own ContainerId /
|
||||
* SlotIndex inside FItemInstanceData, but they are mirrored here so the FastArray
|
||||
* delta only needs the entry, and so clients can route OnRep callbacks per slot.
|
||||
*/
|
||||
USTRUCT()
|
||||
struct ITEMINVENTORY_API FRepInventoryEntry : public FFastArraySerializerItem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY()
|
||||
FGuid ContainerId;
|
||||
|
||||
UPROPERTY()
|
||||
int32 SlotIndex = INDEX_NONE;
|
||||
|
||||
UPROPERTY()
|
||||
FItemInstanceData Item;
|
||||
|
||||
FRepInventoryEntry() = default;
|
||||
FRepInventoryEntry(const FGuid& InContainer, int32 InSlot, const FItemInstanceData& InItem)
|
||||
: ContainerId(InContainer), SlotIndex(InSlot), Item(InItem) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* FastArraySerializer carrying every item the inventory owns across all containers
|
||||
* (section 7.3 / 32). One list per component delta-replicates only changed entries.
|
||||
*/
|
||||
USTRUCT()
|
||||
struct ITEMINVENTORY_API FReplicatedInventoryList : public FFastArraySerializer
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY()
|
||||
TArray<FRepInventoryEntry> Entries;
|
||||
|
||||
/** Not replicated - set by the owning component so callbacks can broadcast. */
|
||||
UPROPERTY(NotReplicated)
|
||||
TObjectPtr<UInventoryComponent> OwnerComponent = nullptr;
|
||||
|
||||
bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms)
|
||||
{
|
||||
return FFastArraySerializer::FastArrayDeltaSerialize<FRepInventoryEntry, FReplicatedInventoryList>(Entries, DeltaParms, *this);
|
||||
}
|
||||
|
||||
//~ FFastArraySerializer client callbacks
|
||||
void PostReplicatedAdd(const TArrayView<int32>& AddedIndices, int32 FinalSize);
|
||||
void PostReplicatedChange(const TArrayView<int32>& ChangedIndices, int32 FinalSize);
|
||||
void PreReplicatedRemove(const TArrayView<int32>& RemovedIndices, int32 FinalSize);
|
||||
|
||||
// --- Server-side mutation helpers (mark dirty for delta replication) ---
|
||||
|
||||
/** Adds or replaces the entry occupying (Container, Slot). Returns the live entry. */
|
||||
FRepInventoryEntry& AddOrUpdate(const FGuid& Container, int32 Slot, const FItemInstanceData& Item);
|
||||
|
||||
/** Removes the entry with the given instance id. Returns true if one was removed. */
|
||||
bool RemoveByInstanceId(const FGuid& InstanceId);
|
||||
|
||||
/** Marks an existing entry dirty after its Item was mutated in place. */
|
||||
void MarkEntryDirtyByInstanceId(const FGuid& InstanceId);
|
||||
|
||||
FRepInventoryEntry* FindByInstanceId(const FGuid& InstanceId);
|
||||
const FRepInventoryEntry* FindByInstanceId(const FGuid& InstanceId) const;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct TStructOpsTypeTraits<FReplicatedInventoryList> : public TStructOpsTypeTraitsBase2<FReplicatedInventoryList>
|
||||
{
|
||||
enum
|
||||
{
|
||||
WithNetDeltaSerializer = true
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user