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>
831 lines
21 KiB
C++
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);
|
|
}
|