// 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& 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 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& OutItems) const { for (const FRepInventoryEntry& E : ReplicatedItems.Entries) { if (E.ContainerId == ContainerId) { OutItems.Add(E.Item); } } } void UInventoryComponent::GetAllItems(TArray& 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 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& 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); }