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:
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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user