Files
inventory-system-plugin/Source/ItemInventory/Private/InventoryComponent.cpp
Bonchellon 7f7e043a88 Initial commit: Item Interaction Ecosystem plugin (UE5.7)
Server-authoritative, data-driven item/inventory/interaction/carry/equipment/
world-slot/placement/crafting framework. 8 modules, compiles against UE 5.7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:49:56 +03:00

831 lines
21 KiB
C++

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