第一次提交

This commit is contained in:
不明不惑
2026-03-03 01:23:02 +08:00
commit 3e434877e8
1053 changed files with 102411 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Async/GIS_AsyncAction_Wait.h"
#include "TimerManager.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_AsyncAction_Wait)
UGIS_AsyncAction_Wait::UGIS_AsyncAction_Wait()
{
}
bool UGIS_AsyncAction_Wait::ShouldBroadcastDelegates() const
{
return Super::ShouldBroadcastDelegates() && IsValid(GetActor());
}
void UGIS_AsyncAction_Wait::StopWaiting()
{
const UWorld* World = GetWorld();
if (TimerHandle.IsValid() && IsValid(World))
{
FTimerManager& TimerManager = World->GetTimerManager();
TimerManager.ClearTimer(TimerHandle);
}
}
void UGIS_AsyncAction_Wait::Cleanup()
{
AActor* Actor = GetActor();
if (IsValid(Actor))
{
Actor->OnDestroyed.RemoveDynamic(this, &ThisClass::OnTargetDestroyed);
}
StopWaiting();
}
void UGIS_AsyncAction_Wait::Activate()
{
const UWorld* World = GetWorld();
AActor* Actor = GetActor();
if (IsValid(World) && IsValid(Actor))
{
FTimerManager& TimerManager = World->GetTimerManager();
TimerManager.SetTimer(TimerHandle, this, &ThisClass::OnTimer, WaitInterval, true, 0);
Actor->OnDestroyed.AddDynamic(this, &ThisClass::OnTargetDestroyed);
}
else
{
Cancel();
}
}
UWorld* UGIS_AsyncAction_Wait::GetWorld() const
{
if (WorldPtr.IsValid() && WorldPtr->IsValidLowLevelFast())
{
return WorldPtr.Get();
}
return nullptr;
}
AActor* UGIS_AsyncAction_Wait::GetActor() const
{
if (TargetActorPtr.IsValid() && TargetActorPtr->IsValidLowLevelFast())
{
return TargetActorPtr.Get();
}
return nullptr;
}
void UGIS_AsyncAction_Wait::Cancel()
{
Super::Cancel();
Cleanup();
}
void UGIS_AsyncAction_Wait::OnTargetDestroyed(AActor* DestroyedActor)
{
Cancel();
}
void UGIS_AsyncAction_Wait::SetWorld(UWorld* NewWorld)
{
WorldPtr = NewWorld;
}
void UGIS_AsyncAction_Wait::SetTargetActor(AActor* NewTargetActor)
{
TargetActorPtr = NewTargetActor;
}
void UGIS_AsyncAction_Wait::SetWaitInterval(float NewWaitInterval)
{
WaitInterval = NewWaitInterval;
}
void UGIS_AsyncAction_Wait::SetMaxWaitTimes(int32 NewMaxWaitTimes)
{
MaxWaitTimes = NewMaxWaitTimes;
}
void UGIS_AsyncAction_Wait::OnTimer()
{
AActor* Actor = GetActor();
if (!IsValid(Actor))
{
Cancel();
return;
}
OnExecutionAction();
if (MaxWaitTimes > 0)
{
WaitTimes++;
if (WaitTimes > MaxWaitTimes)
{
Cancel();
}
}
}
void UGIS_AsyncAction_Wait::OnExecutionAction()
{
}
void UGIS_AsyncAction_Wait::Complete()
{
Super::Cancel();
OnCompleted.Broadcast();
Cleanup();
}

View File

@@ -0,0 +1,66 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Async/GIS_AsyncAction_WaitEquipmentSystem.h"
#include "GIS_EquipmentSystemComponent.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_AsyncAction_WaitEquipmentSystem)
UGIS_AsyncAction_WaitEquipmentSystem* UGIS_AsyncAction_WaitEquipmentSystem::WaitEquipmentSystem(UObject* WorldContext, AActor* TargetActor)
{
return CreateWaitAction<UGIS_AsyncAction_WaitEquipmentSystem>(WorldContext, TargetActor, 0.5, -1);
}
void UGIS_AsyncAction_WaitEquipmentSystem::OnExecutionAction()
{
AActor* Actor = GetActor();
if (UGIS_EquipmentSystemComponent* EquipmentSystem = UGIS_EquipmentSystemComponent::GetEquipmentSystemComponent(Actor))
{
Complete();
}
}
UGIS_AsyncAction_WaitEquipmentSystem* UGIS_AsyncAction_WaitEquipmentSystemInitialized::WaitEquipmentSystemInitialized(UObject* WorldContext, AActor* TargetActor)
{
return CreateWaitAction<UGIS_AsyncAction_WaitEquipmentSystemInitialized>(WorldContext, TargetActor, 0.5, -1);
}
void UGIS_AsyncAction_WaitEquipmentSystemInitialized::OnExecutionAction()
{
// Already found.
UGIS_EquipmentSystemComponent* ExistingOne = EquipmentSystemPtr.IsValid() ? EquipmentSystemPtr.Get() : nullptr;
if (IsValid(ExistingOne))
{
return;
}
AActor* Actor = GetActor();
if (UGIS_EquipmentSystemComponent* EquipmentSys = UGIS_EquipmentSystemComponent::GetEquipmentSystemComponent(Actor))
{
if (EquipmentSys->IsEquipmentSystemInitialized())
{
Complete();
return;
}
EquipmentSystemPtr = EquipmentSys;
EquipmentSystemPtr->OnEquipmentSystemInitializedEvent.AddDynamic(this, &ThisClass::OnSystemInitialized);
}
}
void UGIS_AsyncAction_WaitEquipmentSystemInitialized::Cleanup()
{
Super::Cleanup();
UGIS_EquipmentSystemComponent* ExistingOne = EquipmentSystemPtr.IsValid() ? EquipmentSystemPtr.Get() : nullptr;
if (IsValid(ExistingOne))
{
ExistingOne->OnEquipmentSystemInitializedEvent.RemoveAll(this);
}
}
void UGIS_AsyncAction_WaitEquipmentSystemInitialized::OnSystemInitialized()
{
Complete();
}

View File

@@ -0,0 +1,66 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Async/GIS_AsyncAction_WaitInventorySystem.h"
#include "GIS_InventorySystemComponent.h"
#include "GameFramework/PlayerState.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_AsyncAction_WaitInventorySystem)
UGIS_AsyncAction_WaitInventorySystem* UGIS_AsyncAction_WaitInventorySystem::WaitInventorySystem(UObject* WorldContext, AActor* TargetActor)
{
return CreateWaitAction<UGIS_AsyncAction_WaitInventorySystem>(WorldContext, TargetActor, 0.5, -1);
}
void UGIS_AsyncAction_WaitInventorySystem::OnExecutionAction()
{
AActor* Actor = GetActor();
if (UGIS_InventorySystemComponent* Inventory = UGIS_InventorySystemComponent::GetInventorySystemComponent(Actor))
{
Complete();
}
}
UGIS_AsyncAction_WaitInventorySystem* UGIS_AsyncAction_WaitInventorySystemInitialized::WaitInventorySystemInitialized(UObject* WorldContext, AActor* TargetActor)
{
return CreateWaitAction<UGIS_AsyncAction_WaitInventorySystemInitialized>(WorldContext, TargetActor, 0.5, -1);
}
void UGIS_AsyncAction_WaitInventorySystemInitialized::OnExecutionAction()
{
// Already found.
UGIS_InventorySystemComponent* ExistingOne = InventorySysPtr.IsValid() ? InventorySysPtr.Get() : nullptr;
if (IsValid(ExistingOne))
{
return;
}
AActor* Actor = GetActor();
if (UGIS_InventorySystemComponent* InventorySys = UGIS_InventorySystemComponent::GetInventorySystemComponent(Actor))
{
if (InventorySys->IsInventoryInitialized())
{
Complete();
return;
}
InventorySysPtr = InventorySys;
InventorySysPtr->OnInventorySystemInitializedEvent.AddDynamic(this, &ThisClass::OnSystemInitialized);
}
}
void UGIS_AsyncAction_WaitInventorySystemInitialized::Cleanup()
{
Super::Cleanup();
UGIS_InventorySystemComponent* ExistingOne = InventorySysPtr.IsValid() ? InventorySysPtr.Get() : nullptr;
if (IsValid(ExistingOne))
{
ExistingOne->OnInventorySystemInitializedEvent.RemoveAll(this);
}
}
void UGIS_AsyncAction_WaitInventorySystemInitialized::OnSystemInitialized()
{
Complete();
}

View File

@@ -0,0 +1,81 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Async/GIS_AsyncAction_WaitItemFragmentDataChanged.h"
#include "Engine/Engine.h"
#include "UObject/Object.h"
#include "GIS_ItemFragment.h"
#include "GIS_ItemInstance.h"
#include "GIS_LogChannels.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_AsyncAction_WaitItemFragmentDataChanged)
UGIS_AsyncAction_WaitItemFragmentDataChanged* UGIS_AsyncAction_WaitItemFragmentDataChanged::WaitItemFragmentStateChanged(UObject* WorldContext, UGIS_ItemInstance* ItemInstance,
TSoftClassPtr<UGIS_ItemFragment> FragmentClass)
{
if (!IsValid(WorldContext))
{
GIS_LOG(Warning, "invalid world context!")
return nullptr;
}
UWorld* World = GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::LogAndReturnNull);
if (!IsValid(World))
{
GIS_LOG(Warning, "can't get world from context:%s", *GetNameSafe(WorldContext));
return nullptr;
}
if (!IsValid(ItemInstance))
{
GIS_LOG(Warning, "invalid item instance");
return nullptr;
}
TSubclassOf<UGIS_ItemFragment> Class = FragmentClass.LoadSynchronous();
if (Class == nullptr)
{
GIS_LOG(Warning, "invalid fragment class");
return nullptr;
}
UGIS_AsyncAction_WaitItemFragmentDataChanged* NewAction = NewObject<UGIS_AsyncAction_WaitItemFragmentDataChanged>(GetTransientPackage(), StaticClass());
NewAction->ItemInstance = ItemInstance;
NewAction->FragmentClass = Class;
NewAction->RegisterWithGameInstance(World->GetGameInstance());
return NewAction;
}
void UGIS_AsyncAction_WaitItemFragmentDataChanged::Activate()
{
UGIS_ItemInstance* Item = ItemInstance.IsValid() ? ItemInstance.Get() : nullptr;
if (IsValid(Item))
{
Item->OnFragmentStateAddedEvent.AddDynamic(this, &ThisClass::OnFragmentStateChanged);
Item->OnFragmentStateUpdatedEvent.AddDynamic(this, &ThisClass::OnFragmentStateChanged);
}
}
void UGIS_AsyncAction_WaitItemFragmentDataChanged::Cancel()
{
Super::Cancel();
UGIS_ItemInstance* Item = ItemInstance.IsValid() ? ItemInstance.Get() : nullptr;
if (IsValid(Item))
{
Item->OnFragmentStateAddedEvent.RemoveAll(this);
Item->OnFragmentStateUpdatedEvent.RemoveAll(this);
ItemInstance.Reset();
FragmentClass = nullptr;
}
}
void UGIS_AsyncAction_WaitItemFragmentDataChanged::OnFragmentStateChanged(const UGIS_ItemFragment* Fragment, const FInstancedStruct& State)
{
if (ShouldBroadcastDelegates())
{
if (Fragment != nullptr && Fragment->GetClass() == FragmentClass)
{
OnStateChanged.Broadcast(Fragment, State);
}
}
}

View File

@@ -0,0 +1,187 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Attributes/GIS_GameplayTagFloat.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_GameplayTagFloat)
FString FGIS_GameplayTagFloat::GetDebugString() const
{
return FString::Printf(TEXT("%sx%f"), *Tag.ToString(), Value);
}
void FGIS_GameplayTagFloatContainer::AddItem(FGameplayTag Tag, float Value)
{
if (!Tag.IsValid())
{
FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to AddItem"), ELogVerbosity::Warning);
return;
}
if (Value > 0)
{
for (FGIS_GameplayTagFloat& Item : Items)
{
// handle adding to existing value.
if (Item.Tag == Tag)
{
const float OldValue = Item.Value;
const float NewValue = Item.Value + Value;
Item.Value = NewValue;
TagToValueMap[Tag] = NewValue;
MarkItemDirty(Item);
if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast<IGIS_GameplayTagFloatContainerOwner>(ContainerOwner))
{
Interface->OnTagFloatUpdate(Tag, OldValue, NewValue);
}
return;
}
}
// handle adding new item.
FGIS_GameplayTagFloat& NewItem = Items.Emplace_GetRef(Tag, Value);
TagToValueMap.Add(Tag, Value);
MarkItemDirty(NewItem);
if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast<IGIS_GameplayTagFloatContainerOwner>(ContainerOwner))
{
Interface->OnTagFloatUpdate(Tag, 0, Value);
}
}
}
void FGIS_GameplayTagFloatContainer::SetItem(FGameplayTag Tag, float Value)
{
if (!Tag.IsValid())
{
FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to SetItem"), ELogVerbosity::Warning);
return;
}
for (FGIS_GameplayTagFloat& Item : Items)
{
if (Item.Tag == Tag)
{
const float OldValue = Item.Value;
Item.Value = Value;
TagToValueMap[Tag] = Value;
MarkItemDirty(Item);
if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast<IGIS_GameplayTagFloatContainerOwner>(ContainerOwner))
{
Interface->OnTagFloatUpdate(Tag, OldValue, Value);
}
return;
}
}
FGIS_GameplayTagFloat& NewItem = Items.Emplace_GetRef(Tag, Value);
MarkItemDirty(NewItem);
TagToValueMap.Add(Tag, Value);
if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast<IGIS_GameplayTagFloatContainerOwner>(ContainerOwner))
{
Interface->OnTagFloatUpdate(Tag, 0, Value);
}
}
void FGIS_GameplayTagFloatContainer::SetItems(const TArray<FGIS_GameplayTagFloat>& NewItems)
{
Items = NewItems;
TagToValueMap.Empty();
for (const FGIS_GameplayTagFloat& NewItem : NewItems)
{
TagToValueMap.Add(NewItem.Tag, NewItem.Value);
}
MarkArrayDirty();
}
void FGIS_GameplayTagFloatContainer::EmptyItems()
{
Items.Empty();
TagToValueMap.Empty();
MarkArrayDirty();
}
void FGIS_GameplayTagFloatContainer::RemoveItem(FGameplayTag Tag, float Value)
{
if (!Tag.IsValid())
{
FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to RemoveItem"), ELogVerbosity::Warning);
return;
}
//@TODO: Should we error if you try to remove a Item that doesn't exist or has a smaller count?
if (Value > 0)
{
for (auto It = Items.CreateIterator(); It; ++It)
{
FGIS_GameplayTagFloat& Item = *It;
if (Item.Tag == Tag)
{
if (Item.Value <= Value)
{
const float OldValue = Item.Value;
It.RemoveCurrent();
TagToValueMap.Remove(Tag);
MarkArrayDirty();
if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast<IGIS_GameplayTagFloatContainerOwner>(ContainerOwner))
{
Interface->OnTagFloatUpdate(Tag, OldValue, 0);
}
}
else
{
const float OldValue = Item.Value;
const float NewValue = Item.Value - Value;
Item.Value = NewValue;
TagToValueMap[Tag] = NewValue;
MarkItemDirty(Item);
if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast<IGIS_GameplayTagFloatContainerOwner>(ContainerOwner))
{
Interface->OnTagFloatUpdate(Tag, OldValue, NewValue);
}
}
return;
}
}
}
}
void FGIS_GameplayTagFloatContainer::PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize)
{
for (int32 Index : RemovedIndices)
{
FGIS_GameplayTagFloat& Item = Items[Index];
TagToValueMap.Remove(Item.Tag);
if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast<IGIS_GameplayTagFloatContainerOwner>(ContainerOwner))
{
Interface->OnTagFloatUpdate(Item.Tag, Item.PrevValue, 0);
}
Item.PrevValue = 0;
}
}
void FGIS_GameplayTagFloatContainer::PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize)
{
for (int32 Index : AddedIndices)
{
FGIS_GameplayTagFloat& Item = Items[Index];
TagToValueMap.Add(Item.Tag, Item.Value);
if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast<IGIS_GameplayTagFloatContainerOwner>(ContainerOwner))
{
Interface->OnTagFloatUpdate(Item.Tag, 0, Item.Value);
}
Item.PrevValue = Item.Value;
}
}
void FGIS_GameplayTagFloatContainer::PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize)
{
for (int32 Index : ChangedIndices)
{
FGIS_GameplayTagFloat& Item = Items[Index];
TagToValueMap[Item.Tag] = Item.Value;
if (IGIS_GameplayTagFloatContainerOwner* Interface = Cast<IGIS_GameplayTagFloatContainerOwner>(ContainerOwner))
{
Interface->OnTagFloatUpdate(Item.Tag, Item.PrevValue, Item.Value);
}
Item.PrevValue = Item.Value;
}
}

View File

@@ -0,0 +1,167 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Attributes/GIS_GameplayTagInteger.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_GameplayTagInteger)
FString FGIS_GameplayTagInteger::GetDebugString() const
{
return FString::Printf(TEXT("%sx%d"), *Tag.ToString(), Value);
}
void FGIS_GameplayTagIntegerContainer::AddItem(FGameplayTag Tag, int32 Value)
{
if (!Tag.IsValid())
{
FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to AddItem"), ELogVerbosity::Warning);
return;
}
if (Value > 0)
{
for (FGIS_GameplayTagInteger& Item : Items)
{
if (Item.Tag == Tag)
{
const int32 OldValue = Item.Value;
const int32 NewValue = Item.Value + Value;
Item.Value = NewValue;
TagToValueMap[Tag] = NewValue;
MarkItemDirty(Item);
if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast<IGIS_GameplayTagIntegerContainerOwner>(ContainerOwner))
{
Interface->OnTagIntegerUpdate(Tag, OldValue, NewValue);
}
return;
}
}
FGIS_GameplayTagInteger& NewItem = Items.Emplace_GetRef(Tag, Value);
TagToValueMap.Add(Tag, Value);
MarkItemDirty(NewItem);
if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast<IGIS_GameplayTagIntegerContainerOwner>(ContainerOwner))
{
Interface->OnTagIntegerUpdate(Tag, 0, Value);
}
}
}
void FGIS_GameplayTagIntegerContainer::SetItem(FGameplayTag Tag, int32 Value)
{
if (!Tag.IsValid())
{
FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to SetItem"), ELogVerbosity::Warning);
return;
}
for (FGIS_GameplayTagInteger& Item : Items)
{
if (Item.Tag == Tag)
{
int32 OldValue = Item.Value;
Item.Value = Value;
TagToValueMap[Tag] = Value;
MarkItemDirty(Item);
if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast<IGIS_GameplayTagIntegerContainerOwner>(ContainerOwner))
{
Interface->OnTagIntegerUpdate(Tag, OldValue, Value);
}
return;
}
}
FGIS_GameplayTagInteger& NewItem = Items.Emplace_GetRef(Tag, Value);
MarkItemDirty(NewItem);
TagToValueMap.Add(Tag, Value);
if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast<IGIS_GameplayTagIntegerContainerOwner>(ContainerOwner))
{
Interface->OnTagIntegerUpdate(Tag, 0, Value);
}
}
void FGIS_GameplayTagIntegerContainer::RemoveItem(FGameplayTag Tag, int32 Value)
{
if (!Tag.IsValid())
{
FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to RemoveItem"), ELogVerbosity::Warning);
return;
}
//@TODO: Should we error if you try to remove a Item that doesn't exist or has a smaller count?
if (Value > 0)
{
for (auto It = Items.CreateIterator(); It; ++It)
{
FGIS_GameplayTagInteger& Item = *It;
if (Item.Tag == Tag)
{
if (Item.Value <= Value)
{
const int32 OldValue = Item.Value;
It.RemoveCurrent();
TagToValueMap.Remove(Tag);
MarkArrayDirty();
if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast<IGIS_GameplayTagIntegerContainerOwner>(ContainerOwner))
{
Interface->OnTagIntegerUpdate(Tag, OldValue, 0);
}
}
else
{
const int32 OldValue = Item.Value;
const int32 NewValue = Item.Value - Value;
Item.Value = NewValue;
TagToValueMap[Tag] = NewValue;
MarkItemDirty(Item);
if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast<IGIS_GameplayTagIntegerContainerOwner>(ContainerOwner))
{
Interface->OnTagIntegerUpdate(Tag, OldValue, NewValue);
}
}
return;
}
}
}
}
void FGIS_GameplayTagIntegerContainer::PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize)
{
for (int32 Index : RemovedIndices)
{
FGIS_GameplayTagInteger& Item = Items[Index];
TagToValueMap.Remove(Item.Tag);
if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast<IGIS_GameplayTagIntegerContainerOwner>(ContainerOwner))
{
Interface->OnTagIntegerUpdate(Item.Tag, Item.PrevValue, 0);
}
Item.PrevValue = 0;
}
}
void FGIS_GameplayTagIntegerContainer::PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize)
{
for (int32 Index : AddedIndices)
{
FGIS_GameplayTagInteger& Item = Items[Index];
TagToValueMap.Add(Item.Tag, Item.Value);
if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast<IGIS_GameplayTagIntegerContainerOwner>(ContainerOwner))
{
Interface->OnTagIntegerUpdate(Item.Tag, 0, Item.Value);
}
Item.PrevValue = Item.Value;
}
}
void FGIS_GameplayTagIntegerContainer::PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize)
{
for (int32 Index : ChangedIndices)
{
FGIS_GameplayTagInteger& Item = Items[Index];
TagToValueMap[Item.Tag] = Item.Value;
if (IGIS_GameplayTagIntegerContainerOwner* Interface = Cast<IGIS_GameplayTagIntegerContainerOwner>(ContainerOwner))
{
Interface->OnTagIntegerUpdate(Item.Tag, Item.PrevValue, Item.Value);
}
Item.PrevValue = Item.Value;
}
}

View File

@@ -0,0 +1,76 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_CollectionContainer.h"
#include "GIS_InventorySystemComponent.h"
#include "GIS_ItemCollection.h"
#include "GIS_LogChannels.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CollectionContainer)
bool FGIS_CollectionEntry::IsValidEntry() const
{
return Id.IsValid() && IsValid(Instance) && IsValid(Definition);
}
FGIS_CollectionContainer::FGIS_CollectionContainer(UGIS_InventorySystemComponent* InInventory)
{
OwningComponent = InInventory;
}
void FGIS_CollectionContainer::PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize)
{
for (int32 Index : RemovedIndices)
{
const FGIS_CollectionEntry& Entry = Entries[Index];
// already in the list.
if (Entry.IsValidEntry() && OwningComponent->CollectionIdToInstanceMap.Contains(Entry.Instance->GetCollectionId()))
{
OwningComponent->OnCollectionRemoved(Entry);
}
else if (OwningComponent->PendingCollections.Contains(Entry.Id))
{
GIS_OWNED_CLOG(OwningComponent, Warning, "Discard pending collection(%s).", *GetNameSafe(OwningComponent->PendingCollections[Entry.Id].Definition))
OwningComponent->PendingCollections.Remove(Entry.Id);
}
}
}
void FGIS_CollectionContainer::PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize)
{
for (int32 Index : AddedIndices)
{
const FGIS_CollectionEntry& Entry = Entries[Index];
if (OwningComponent->GetOwner() && Entry.IsValidEntry())
{
OwningComponent->OnCollectionAdded(Entry);
}
else
{
OwningComponent->PendingCollections.Add(Entry.Id, Entry);
}
}
}
void FGIS_CollectionContainer::PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize)
{
for (int32 Index : ChangedIndices)
{
const FGIS_CollectionEntry& Entry = Entries[Index];
if (Entry.IsValidEntry() && OwningComponent->CollectionIdToInstanceMap.Contains(Entry.Instance->GetCollectionId())) //Already Added.
{
OwningComponent->OnCollectionUpdated(Entry);
}
else if (OwningComponent->PendingCollections.Contains(Entry.Id)) //In pending list.
{
OwningComponent->PendingCollections.Emplace(Entry.Id, Entry); // Updated to pending.
}
else
{
OwningComponent->PendingCollections.Add(Entry.Id, Entry); //Add to pending list.
}
}
}

View File

@@ -0,0 +1,861 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_ItemCollection.h"
#include "GIS_InventorySystemComponent.h"
#include "GIS_InventoryMeesages.h"
#include "GIS_InventorySubsystem.h"
#include "GIS_InventoryTags.h"
#include "Items/GIS_ItemInstance.h"
#include "Items/GIS_ItemDefinition.h"
#include "Engine/ActorChannel.h"
#include "Net/UnrealNetwork.h"
#include "Engine/Engine.h"
#include "Engine/NetDriver.h"
#include "Engine/BlueprintGeneratedClass.h"
#include "GIS_ItemRestriction.h"
#include "GIS_LogChannels.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemCollection)
bool UGIS_ItemCollectionDefinition::IsSupportedForNetworking() const
{
return true;
}
TSubclassOf<UGIS_ItemCollection> UGIS_ItemCollectionDefinition::GetCollectionInstanceClass() const
{
return UGIS_ItemCollection::StaticClass();
}
UGIS_ItemCollection::GIS_CollectionNotifyLocker::GIS_CollectionNotifyLocker(UGIS_ItemCollection& InItemCollection): ItemCollection(InItemCollection)
{
ItemCollection.NotifyLocker++;
}
UGIS_ItemCollection::GIS_CollectionNotifyLocker::~GIS_CollectionNotifyLocker()
{
ItemCollection.NotifyLocker--;
}
UGIS_ItemCollection::UGIS_ItemCollection(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer), Container(this)
{
}
void UGIS_ItemCollection::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ThisClass, Container);
//fix: https://forums.unrealengine.com/t/subobject-replication-for-blueprint-child-class/106205/4
UBlueprintGeneratedClass* bpClass = Cast<UBlueprintGeneratedClass>(this->GetClass());
if (bpClass != nullptr)
{
bpClass->GetLifetimeBlueprintReplicationList(OutLifetimeProps);
}
}
bool UGIS_ItemCollection::CallRemoteFunction(UFunction* Function, void* Parms, FOutParmRec* OutParms, FFrame* Stack)
{
check(!HasAnyFlags(RF_ClassDefaultObject));
check(GetOuter() != nullptr);
AActor* Owner = CastChecked<AActor>(GetOuter());
bool bProcessed = false;
FWorldContext* const Context = GEngine->GetWorldContextFromWorld(GetWorld());
if (Context != nullptr)
{
for (FNamedNetDriver& Driver : Context->ActiveNetDrivers)
{
if (Driver.NetDriver != nullptr && Driver.NetDriver->ShouldReplicateFunction(Owner, Function))
{
Driver.NetDriver->ProcessRemoteFunction(Owner, Function, Parms, OutParms, Stack, this);
bProcessed = true;
}
}
}
return bProcessed;
}
int32 UGIS_ItemCollection::GetFunctionCallspace(UFunction* Function, FFrame* Stack)
{
if (HasAnyFlags(RF_ClassDefaultObject) || !IsSupportedForNetworking())
{
// This handles absorbing authority/cosmetic
return GEngine->GetGlobalFunctionCallspace(Function, this, Stack);
}
check(GetOuter() != nullptr);
return GetOuter()->GetFunctionCallspace(Function, Stack);
}
bool UGIS_ItemCollection::IsInitialized() const
{
return bInitialized;
}
FString UGIS_ItemCollection::GetCollectionName() const
{
if (Definition)
{
if (Definition->CollectionTag.IsValid())
{
TArray<FName> TagNames;
UGameplayTagsManager::Get().SplitGameplayTagFName(Definition->CollectionTag, TagNames);
if (!TagNames.IsEmpty())
{
return FString::Format(TEXT("{0} Collection"), {TagNames.Last().ToString()});
}
}
return FString::Format(TEXT("{0}"), {GetNameSafe(this)});
}
return TEXT("Invalid Collection!!!");
}
FString UGIS_ItemCollection::GetDebugString() const
{
return FString::Format(TEXT("{0}({1})"), {GetCollectionName(), GetNameSafe(OwningInventory->GetOwner())});
}
bool UGIS_ItemCollection::HasItem(const UGIS_ItemInstance* Item, int32 Amount, bool SimilarItem) const
{
if (Item == nullptr) { return false; }
return GetItemAmount(Item, SimilarItem) >= Amount;
}
FGIS_ItemInfo UGIS_ItemCollection::AddItem(const FGIS_ItemInfo& ItemInfo)
{
//The actually added item info; 实际添加的道具信息
FGIS_ItemInfo ItemInfoAdded = FGIS_ItemInfo(ItemInfo.Item, 0, this);
FGIS_ItemInfo CanAddItemInfo;
if (CanAddItem(ItemInfo, CanAddItemInfo))
{
//非唯一道具或数量为一,直接加.
if (!CanAddItemInfo.Item->IsUnique() || CanAddItemInfo.Amount <= 1)
{
//要添加的信息
FGIS_ItemInfo ItemInfoToAdd(CanAddItemInfo.Item, CanAddItemInfo.Amount, ItemInfo);
ItemInfoAdded = AddInternal(ItemInfoToAdd);
}
else //unique item can stack.
{
//先加1个
FGIS_ItemInfo OriginalResult = AddItem(FGIS_ItemInfo(CanAddItemInfo.Item, 1, ItemInfo));
// 遍历加入数量为1的道具, 每个都有不同的GUID
for (int32 i = 1; i < CanAddItemInfo.Amount; i++)
{
UGIS_ItemInstance* DuplicatedItem = UGIS_InventorySubsystem::Get(GetWorld())->DuplicateItem(OwningInventory->GetOwner(), CanAddItemInfo.Item);
check(DuplicatedItem && DuplicatedItem->IsItemValid())
AddItem(FGIS_ItemInfo(DuplicatedItem, 1, ItemInfo));
}
ItemInfoAdded = FGIS_ItemInfo(CanAddItemInfo.Amount, OriginalResult);
}
}
// overflow
if (ItemInfoAdded.Amount < ItemInfo.Amount)
{
HandleItemOverflow(ItemInfo, ItemInfoAdded);
}
return ItemInfoAdded;
}
int32 UGIS_ItemCollection::AddItems(const TArray<FGIS_ItemInfo>& ItemInfos)
{
int32 TotalAdded = 0;
for (int32 i = 0; i < ItemInfos.Num(); i++)
{
TotalAdded += AddItem(ItemInfos[i]).Amount;
}
return TotalAdded;
}
FGIS_ItemInfo UGIS_ItemCollection::AddItem(UGIS_ItemInstance* Item, int32 Amount)
{
return AddItem(FGIS_ItemInfo(Item, Amount));
}
bool UGIS_ItemCollection::CanAddItem(const FGIS_ItemInfo& Input, FGIS_ItemInfo& Output)
{
if (!bInitialized || Input.Item == nullptr || !Input.Item->IsItemValid() || Input.Amount < 1)
{
return false;
}
FGIS_ItemInfo ModifiedInput = Input;
/*
* Documentation: For none-unique item(means stackable), if you want to add 2 apples with id x, there are following case:
* 1.There are already 10 apples with id x in your inventory,this will turn your "add 2 apples of id x" request into "add 2 apples of id y", resulting 12 apples with id y in your inventory.
* 2.There's no any apples in your inventory, resulting in 2 apples with id x in your inventory.
* 文档: 道具不唯一. 举例: 你要加2个苹果(ID是X)到库存,且苹果定义上它不是唯一的(即可叠加)。
* 如果1库存里如果已经有10个苹果(ID是Y)。就把 “你要加2个ID为X的苹果” 变成“你要加2个ID为Y的苹果”.
* 如果2库存里如果没有任何苹果。那么就不变其结果就是往库存里加2个ID是X的苹果.
*/
if (!ModifiedInput.Item->IsUnique())
{
FGIS_ItemInfo SimilarItemInfo;
if (GetItemInfo(ModifiedInput.Item, SimilarItemInfo))
{
//修改要加入的道具信息为相似道具、但保留Amount.
ModifiedInput = FGIS_ItemInfo(Input.Amount, SimilarItemInfo);
}
}
if (ModifiedInput.Item->GetOwningCollection() != nullptr && ModifiedInput.Item->GetOwningCollection() != this)
{
UGIS_ItemInstance* DuplicatedItem = UGIS_InventorySubsystem::Get(GetWorld())->DuplicateItem(OwningInventory->GetOwner(), ModifiedInput.Item);
check(DuplicatedItem && DuplicatedItem->IsItemValid())
GIS_LOG(Verbose, "The Item(%s) was duplicated, and the duplicated one was added because the item is already part of another collection on actor %s.", *Input.GetDebugString(),
*GetNameSafe(Input.Item->GetOuter()))
ModifiedInput = FGIS_ItemInfo(DuplicatedItem, Input.Amount, Input);
}
if (ModifiedInput.Item->GetOuter() != nullptr && ModifiedInput.Item->GetOuter() != OwningInventory->GetOwner())
{
UGIS_ItemInstance* DuplicatedItem = UGIS_InventorySubsystem::Get(GetWorld())->DuplicateItem(OwningInventory->GetOwner(), ModifiedInput.Item, false);
check(DuplicatedItem && DuplicatedItem->IsItemValid())
GIS_LOG(Verbose, "The Item(%s) was duplicated, and the duplicated one was added because the item was created by another actor %s.", *Input.GetDebugString(),
*GetNameSafe(Input.Item->GetOuter()))
ModifiedInput = FGIS_ItemInfo(DuplicatedItem, Input.Amount, Input);
}
for (int32 i = 0; i < Definition->Restrictions.Num(); i++)
{
auto Restriction = Definition->Restrictions[i];
if (Restriction && !Restriction->CanAddItem(ModifiedInput, this))
{
GIS_CLOG(Warning, "can't add item(%s) due to restriction:%s", *Input.GetDebugString(), *Restriction->GetClass()->GetName())
return false;
}
}
Output = ModifiedInput;
return true;
}
bool UGIS_ItemCollection::CanItemStack(const FGIS_ItemInfo& ItemInfo, const FGIS_ItemStack& ItemStack) const
{
return !ItemInfo.Item->IsUnique() && ItemStack.Item->StackableEquivalentTo(ItemInfo.Item);
}
bool UGIS_ItemCollection::RemoveItemCondition(const FGIS_ItemInfo& ItemInfo, FGIS_ItemInfo& OutItemInfo)
{
if (!ItemInfo.Item || !ItemInfo.Item->IsItemValid() || ItemInfo.Amount < 1)
{
return false;
}
OutItemInfo = ItemInfo;
if (!ItemInfo.Item->IsUnique())
{
FGIS_ItemInfo SimilarItemInfo;
if (GetItemInfo(ItemInfo.Item, SimilarItemInfo))
{
OutItemInfo = FGIS_ItemInfo(SimilarItemInfo.Item, ItemInfo.Amount);
}
}
// make sure is this collection.
if (OutItemInfo.ItemCollection != this)
{
OutItemInfo.ItemCollection = this;
}
for (int32 i = 0; i < Definition->Restrictions.Num(); i++)
{
// Check against src item info in restriction.
if (!Definition->Restrictions[i]->CanRemoveItem(OutItemInfo))
{
return false;
}
}
return true;
}
FGIS_ItemInfo UGIS_ItemCollection::RemoveItem(const FGIS_ItemInfo& ItemInfo)
{
FGIS_ItemInfo ItemInfoToRemove;
if (!RemoveItemCondition(ItemInfo, ItemInfoToRemove))
{
return FGIS_ItemInfo(ItemInfo.Item, 0, this);
}
return RemoveInternal(ItemInfoToRemove);
}
void UGIS_ItemCollection::RemoveAll()
{
for (int32 i = Container.Stacks.Num() - 1; i >= 0; i--)
{
FGIS_ItemStack& ItemStack = Container.Stacks[i];
RemoveItem(FGIS_ItemInfo(ItemStack.Item, ItemStack.Amount));
}
}
bool UGIS_ItemCollection::GetItemInfoByStackId(FGuid InStackId, FGIS_ItemInfo& OutItemInfo) const
{
if (const FGIS_ItemStack* Stack = Container.Stacks.FindByKey(InStackId))
{
OutItemInfo = *Stack;
return true;
}
return false;
}
bool UGIS_ItemCollection::FindItemInfoByStackId(FGuid InStackId, FGIS_ItemInfo& OutItemInfo) const
{
return GetItemInfoByStackId(InStackId, OutItemInfo);
}
bool UGIS_ItemCollection::GetItemInfo(const UGIS_ItemInstance* Item, FGIS_ItemInfo& OutItemInfo) const
{
if (Item == nullptr)
{
return false;
}
bool bFoundSimilar = false;
FGIS_ItemInfo SimilarItemInfo;
for (int32 i = 0; i < Container.Stacks.Num(); i++)
{
const FGIS_ItemStack& ItemStack = Container.Stacks[i];
//Is not unique but has same definition.
if (!Item->IsUnique() && ItemStack.Item->GetDefinition() == Item->GetDefinition())
{
SimilarItemInfo = FGIS_ItemInfo(ItemStack.Item, ItemStack.Amount, ItemStack.Collection);
bFoundSimilar = true;
}
if (ItemStack.Item->GetItemId() != Item->GetItemId())
{
continue;
}
//Found unique item with same id.
OutItemInfo = FGIS_ItemInfo(ItemStack.Item, ItemStack.Amount, ItemStack.Collection);
return true;
}
if (bFoundSimilar)
{
OutItemInfo = SimilarItemInfo;
}
return bFoundSimilar;
}
bool UGIS_ItemCollection::GetItemInfoByDefinition(const TSoftObjectPtr<UGIS_ItemDefinition>& ItemDefinition, FGIS_ItemInfo& OutItemInfo)
{
if (ItemDefinition.IsNull())
{
return false;
}
UGIS_ItemDefinition* LoadedItemDefinition = ItemDefinition.LoadSynchronous();
if (LoadedItemDefinition == nullptr)
{
return false;
}
bool bFoundSimilar = false;
FGIS_ItemInfo SimilarItemInfo;
for (int32 i = 0; i < Container.Stacks.Num(); i++)
{
FGIS_ItemStack& ItemStack = Container.Stacks[i];
if (ItemStack.Item->GetDefinition() == LoadedItemDefinition)
{
SimilarItemInfo = FGIS_ItemInfo(ItemStack.Item, ItemStack.Amount, ItemStack.Collection);
bFoundSimilar = true;
}
}
if (bFoundSimilar && SimilarItemInfo.IsValid())
{
OutItemInfo = SimilarItemInfo;
return true;
}
return false;
}
bool UGIS_ItemCollection::GetItemInfosByDefinition(const TSoftObjectPtr<UGIS_ItemDefinition>& ItemDefinition, TArray<FGIS_ItemInfo>& OutItemInfos)
{
if (ItemDefinition.IsNull())
{
return false;
}
UGIS_ItemDefinition* LoadedItemDefinition = ItemDefinition.LoadSynchronous();
if (LoadedItemDefinition == nullptr)
{
return false;
}
for (int32 i = 0; i < Container.Stacks.Num(); i++)
{
FGIS_ItemStack& ItemStack = Container.Stacks[i];
if (ItemStack.Item->GetDefinition() == LoadedItemDefinition)
{
FGIS_ItemInfo SimilarItemInfo = FGIS_ItemInfo(ItemStack.Item, ItemStack.Amount, ItemStack.Collection);
OutItemInfos.Add(SimilarItemInfo);
}
}
return OutItemInfos.Num() > 0;
}
int32 UGIS_ItemCollection::GetItemAmount(const UGIS_ItemInstance* Item, bool SimilarItem) const
{
if (Item == nullptr) { return 0; }
int32 Count = 0;
for (int i = 0; i < Container.Stacks.Num(); i++)
{
if (Container.Stacks[i].Item && Container.Stacks[i].Item->SimilarTo(Item))
{
Count += Container.Stacks[i].Amount;
}
}
return Count;
}
int32 UGIS_ItemCollection::GetItemAmount(TSoftObjectPtr<UGIS_ItemDefinition> ItemDefinition, bool CountStacks) const
{
if (ItemDefinition.IsNull())
{
return 0;
}
UGIS_ItemDefinition* LoadedDefinition = ItemDefinition.LoadSynchronous();
if (LoadedDefinition == nullptr)
{
return 0;
}
int32 Count = 0;
for (int32 i = 0; i < Container.Stacks.Num(); i++)
{
if (Container.Stacks[i].Item == nullptr)
{
continue;
}
if (LoadedDefinition != Container.Stacks[i].Item->GetDefinition())
{
continue;
}
Count += CountStacks ? 1 : Container.Stacks[i].Amount;
}
return Count;
}
TArray<FGIS_ItemInfo> UGIS_ItemCollection::GetAllItemInfos() const
{
TArray<FGIS_ItemInfo> Infos;
for (int32 i = 0; i < Container.Stacks.Num(); i++)
{
const FGIS_ItemStack& ItemStack = Container.Stacks[i];
Infos.Add(FGIS_ItemInfo(ItemStack));
}
return Infos;
}
TArray<UGIS_ItemInstance*> UGIS_ItemCollection::GetAllItems() const
{
TArray<UGIS_ItemInstance*> Rets;
for (int32 i = 0; i < Container.Stacks.Num(); i++)
{
if (Container.Stacks[i].IsValidStack())
{
Rets.AddUnique(Container.Stacks[i].Item);
}
}
return Rets;
}
const TArray<FGIS_ItemStack>& UGIS_ItemCollection::GetAllItemStacks() const
{
return Container.Stacks;
}
int32 UGIS_ItemCollection::GetItemStacksNum() const
{
return Container.Stacks.Num();
}
void UGIS_ItemCollection::AddItemStack(const FGIS_ItemStack& Stack)
{
check(Stack.IsValidStack())
int32 Idx = Container.Stacks.AddDefaulted();
Container.Stacks[Idx] = Stack;
FGIS_ItemStack& AddedStack = Container.Stacks[Idx];
OnPreItemStackAdded(Stack, Idx);
OnItemStackAdded(AddedStack);
Container.MarkItemDirty(AddedStack);
}
void UGIS_ItemCollection::RemoveItemStackAtIndex(int32 Idx, bool bRemoveFromCollection)
{
check(Container.Stacks.IsValidIndex(Idx))
const FGIS_ItemStack RemovedStack = Container.Stacks[Idx];
OnItemStackRemoved(RemovedStack);
Container.Stacks.RemoveAt(Idx);
if (bRemoveFromCollection)
{
RemovedStack.Item->UnassignCollection(this);
if (OwningInventory->IsReplicatedSubObjectRegistered(RemovedStack.Item))
{
OwningInventory->RemoveReplicatedSubObject(RemovedStack.Item);
}
}
Container.MarkArrayDirty();
}
void UGIS_ItemCollection::UpdateItemStackAmountAtIndex(int32 Idx, int32 NewAmount)
{
check(Container.Stacks.IsValidIndex(Idx))
FGIS_ItemStack& StackToUpdate = Container.Stacks[Idx];
StackToUpdate.Amount = NewAmount;
OnItemStackUpdated(StackToUpdate);
Container.MarkItemDirty(StackToUpdate);
}
void UGIS_ItemCollection::OnPreItemStackAdded(const FGIS_ItemStack& Stack, int32 Idx)
{
if (IsValid(Stack.Item))
{
if (Stack.Item->GetOwningCollection() != this)
{
Stack.Item->AssignCollection(this);
}
if (!OwningInventory->IsReplicatedSubObjectRegistered(Stack.Item))
{
OwningInventory->AddReplicatedSubObject(Stack.Item);
}
}
}
void UGIS_ItemCollection::OnItemStackAdded(const FGIS_ItemStack& Stack)
{
check(Stack.IsValidStack())
if (OwningInventory)
{
StackToIdxMap.Add(Stack.Id, Stack.Index);
FGIS_InventoryStackUpdateMessage Message;
Message.Inventory = OwningInventory;
Message.ChangeType = EGIS_ItemStackChangeType::WasAdded;
Message.CollectionId = CollectionId;
Message.Instance = Stack.Item;
Message.StackId = Stack.Id;
Message.NewCount = Stack.Amount;
Message.Delta = Stack.Amount;
OwningInventory->OnInventoryStackUpdate.Broadcast(Message);
GIS_CLOG(Verbose, "added item stack:item(%s),amount(%d)", *Stack.Item->GetDefinition()->GetName(), Stack.Amount)
}
}
void UGIS_ItemCollection::OnItemStackRemoved(const FGIS_ItemStack& Stack)
{
if (OwningInventory)
{
StackToIdxMap.Remove(Stack.Id);
FGIS_InventoryStackUpdateMessage Message;
Message.Inventory = OwningInventory;
Message.ChangeType = EGIS_ItemStackChangeType::WasRemoved;
Message.CollectionId = CollectionId;
Message.Instance = Stack.Item;
Message.StackId = Stack.Id;
Message.NewCount = 0;
Message.Delta = -Stack.LastObservedAmount;
OwningInventory->OnInventoryStackUpdate.Broadcast(Message);
GIS_CLOG(Verbose, "removed item stack:item(%s),amount(%d)", *Stack.Item->GetDefinition()->GetName(), Stack.Amount)
}
}
void UGIS_ItemCollection::OnItemStackUpdated(const FGIS_ItemStack& Stack)
{
if (OwningInventory)
{
if (StackToIdxMap.Contains(Stack.Id))
{
StackToIdxMap[Stack.Id] = Stack.Index;
}
FGIS_InventoryStackUpdateMessage Message;
Message.Inventory = OwningInventory;
Message.ChangeType = EGIS_ItemStackChangeType::Changed;
Message.CollectionId = CollectionId;
Message.Instance = Stack.Item;
Message.StackId = Stack.Id;
Message.NewCount = Stack.Amount;
Message.Delta = Stack.Amount - Stack.LastObservedAmount;
OwningInventory->OnInventoryStackUpdate.Broadcast(Message);
GIS_CLOG(Verbose, "updated item stack:item(%s),amount(%d)", *Stack.Item->GetDefinition()->GetName(), Stack.Amount)
}
}
void UGIS_ItemCollection::ProcessPendingItemStacks()
{
if (bInitialized)
{
TArray<FGuid> Added;
for (const TPair<FGuid, FGIS_ItemStack>& Pending : PendingItemStacks)
{
if (Pending.Value.IsValidStack())
{
Added.AddUnique(Pending.Key);
}
}
for (int32 i = 0; i < Added.Num(); i++)
{
FGuid AddedStackId = Added[i];
const FGIS_ItemStack& AddedStack = PendingItemStacks[AddedStackId];
OnItemStackAdded(AddedStack);
GIS_CLOG(Verbose, "added item stack:item(%s),amount(%d) from pending list.", *AddedStack.Item->GetDefinition()->GetName(), AddedStack.Amount)
PendingItemStacks.Remove(AddedStackId);
}
}
}
void UGIS_ItemCollection::SetDefinition(const UGIS_ItemCollectionDefinition* NewDefinition)
{
check(OwningInventory != nullptr);
check(NewDefinition != nullptr);
if (!bInitialized)
{
Definition = NewDefinition;
CollectionTag = Definition->CollectionTag;
bInitialized = true;
}
}
void UGIS_ItemCollection::SetCollectionTag(FGameplayTag NewTag)
{
CollectionTag = NewTag;
}
void UGIS_ItemCollection::SetCollectionId(FGuid NewId)
{
if (!CollectionId.IsValid())
{
CollectionId = NewId;
}
}
FGIS_ItemInfo UGIS_ItemCollection::AddInternal(const FGIS_ItemInfo& ItemInfo)
{
bool bFound = false;
FGIS_ItemStack AddedItemStack;
if (!ItemInfo.Item->IsUnique())
{
// First, trying to add to existing stack.
int32 TargetStackIdx = Container.IndexOfById(ItemInfo.StackId);
if (TargetStackIdx != INDEX_NONE)
{
check(Container.Stacks.IsValidIndex(TargetStackIdx))
const FGIS_ItemStack& ExistingStack = Container.Stacks[TargetStackIdx];
if (ExistingStack.Collection == this && ExistingStack.Item == ItemInfo.Item)
{
UpdateItemStackAmountAtIndex(TargetStackIdx, ItemInfo.Amount + ExistingStack.Amount);
AddedItemStack = ExistingStack;
bFound = true;
}
}
if (!bFound)
{
for (int32 i = 0; i < Container.Stacks.Num(); i++)
{
if (CanItemStack(ItemInfo, Container.Stacks[i]) == false)
{
continue;
}
UpdateItemStackAmountAtIndex(i, ItemInfo.Amount + Container.Stacks[i].Amount);
AddedItemStack = Container.Stacks[i];
bFound = true;
break;
}
}
}
//新增.
if (!bFound)
{
AddedItemStack.Initialize(FGuid::NewGuid(), ItemInfo.Item, ItemInfo.Amount, this, ItemInfo.Index);
AddItemStack(AddedItemStack);
}
FGIS_ItemInfo AddedItemInfo = FGIS_ItemInfo(ItemInfo.Item, ItemInfo.Amount, this, AddedItemStack.Id);
return AddedItemInfo;
}
void UGIS_ItemCollection::HandleItemOverflow(const FGIS_ItemInfo& OriginalItemInfo, const FGIS_ItemInfo& ItemInfoAdded)
{
FGIS_ItemInfo RejectedItemInfo = FGIS_ItemInfo(OriginalItemInfo.Amount - ItemInfoAdded.Amount, OriginalItemInfo);
FGIS_ItemInfo ReturnedItemInfo;
if (Definition->OverflowOptions.bReturnOverflow)
{
if (OriginalItemInfo.ItemCollection != nullptr && OriginalItemInfo.ItemCollection != this)
{
ReturnedItemInfo = OriginalItemInfo.ItemCollection->AddItem(RejectedItemInfo);
}
}
if (Definition->OverflowOptions.bSendRejectedMessage)
{
FGIS_InventoryAddItemInfoRejectedMessage Message;
Message.Inventory = OwningInventory;
Message.Collection = this;
Message.OriginalItemInfo = OriginalItemInfo;
Message.ItemInfoAdded = ItemInfoAdded;
Message.RejectedItemInfo = RejectedItemInfo;
Message.ReturnedItemInfo = ReturnedItemInfo;
OwningInventory->OnInventoryAddItemInfo_Rejected.Broadcast(Message);
}
GIS_CLOG(Warning, "try add %s, added %d, rejected %d, returned:%d", *OriginalItemInfo.GetDebugString(), ItemInfoAdded.Amount, RejectedItemInfo.Amount, ReturnedItemInfo.Amount);
}
FGIS_ItemInfo UGIS_ItemCollection::RemoveInternal(const FGIS_ItemInfo& ItemInfo)
{
int32 Removed = 0;
FGIS_ItemStack ItemStackToRemove;
{
int32 Idx = Container.IndexOfByIds(ItemInfo.StackId, ItemInfo.Item->GetItemId());
if (Idx != INDEX_NONE)
{
ItemStackToRemove = SimpleInternalItemRemove(ItemInfo, Removed, Idx);
}
}
//如果已经移除的数量未达到指定移除的数量,就继续移除。
if (Removed < ItemInfo.Amount)
{
TArray<FGIS_ItemStack> TempStacks = Container.Stacks;
for (int32 i = TempStacks.Num() - 1; i >= 0; i--)
{
if (TempStacks[i].Item == nullptr || TempStacks[i].Item->GetItemId() != ItemInfo.Item->GetItemId())
{
continue;
}
ItemStackToRemove = SimpleInternalItemRemove(ItemInfo, Removed, i);
if (Removed >= ItemInfo.Amount)
{
break;
}
}
}
const FGIS_ItemInfo RemovedItemInfo = FGIS_ItemInfo(ItemInfo.Item, Removed, this, ItemStackToRemove.Id);
if (Removed == 0)
{
return RemovedItemInfo;
}
return RemovedItemInfo;
}
FGIS_ItemStack UGIS_ItemCollection::SimpleInternalItemRemove(const FGIS_ItemInfo& ItemInfo, int32& AlreadyRemoved, int32 StackIndex)
{
check(Container.Stacks.IsValidIndex(StackIndex));
const FGIS_ItemStack FoundStack = Container.Stacks[StackIndex];
int32 RemainingToRemove = ItemInfo.Amount - AlreadyRemoved; //我要减10个已经减了4个还要减6个
int32 NewAmount = FoundStack.Amount - RemainingToRemove;
if (NewAmount <= 0) //如果栈里还有3个不够减那么已经减去的数量就成了7个然后栈被移除。
{
AlreadyRemoved += FoundStack.Amount;
RemoveItemStackAtIndex(StackIndex);
}
else //如果栈里还有7个够减那么已经减去的数量为10,栈还剩下7-6=1个。
{
AlreadyRemoved += RemainingToRemove;
UpdateItemStackAmountAtIndex(StackIndex, NewAmount);
}
return FoundStack;
}
FGIS_ItemInfo UGIS_ItemCollection::GiveItem(const FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ItemCollection)
{
if (OwningInventory->GetOwnerRole() != ROLE_Authority)
{
GIS_CLOG(Warning, "Has no authority")
return FGIS_ItemInfo::None;
}
if (!ItemInfo.IsValid())
{
GIS_CLOG(Warning, "invalid ItemInfo to give.")
return FGIS_ItemInfo::None;
}
if (ItemInfo.Item->GetOwningInventory() == ItemCollection->GetOwningInventory())
{
GIS_CLOG(Warning, "Item:%s already belongs to this inventory.", *ItemInfo.GetDebugString());
return FGIS_ItemInfo::None;
}
FGIS_ItemInfo RemovedItemInfo = RemoveItem(ItemInfo);
FGIS_ItemInfo ItemInfoToAdd = FGIS_ItemInfo(ItemInfo.Item, RemovedItemInfo.Amount, this);
FGIS_ItemInfo GivenItemInfo = ItemCollection->AddItem(ItemInfoToAdd);
if (GivenItemInfo.Amount != RemovedItemInfo.Amount)
{
// Failed to add so add it back to the previous collection.
//ItemInfoToAdd.Amount = ItemInfoToAdd.Amount - GivenItemInfo.Amount;
//if (ItemInfoToAdd.IsValid())
//{
// AddItem(FGIS_ItemInfo(ItemInfoToAdd));
//}
}
return GivenItemInfo;
}
void UGIS_ItemCollection::ServerGiveItem_Implementation(const FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ItemCollection)
{
GiveItem(ItemInfo, ItemCollection);
}
void UGIS_ItemCollection::GiveAllItems(UGIS_ItemCollection* OtherItemCollection)
{
for (int i = Container.Stacks.Num() - 1; i >= 0; i--)
{
auto& itemStack = Container.Stacks[i];
GiveItem(FGIS_ItemInfo(itemStack), OtherItemCollection);
}
}
void UGIS_ItemCollection::ServerGiveAllItems_Implementation(UGIS_ItemCollection* OtherItemCollection)
{
GiveAllItems(OtherItemCollection);
}
int32 UGIS_ItemCollection::GetItemAmountFittingInLimitedAdditionalStacks(const FGIS_ItemInfo& ItemInfo, int32 AvailableAdditionalStacks) const
{
if (ItemInfo.Item->IsUnique())
{
//预计还有5个而要添加10个那么就只能5个。
//预计还有10个而要添加6个那么就能放6个。
return FMath::Min(ItemInfo.Amount, AvailableAdditionalStacks);
}
//满了且没有已经存在的Stack可以叠上去。
if (AvailableAdditionalStacks == 0 && !HasItem(ItemInfo.Item, 1))
{
return 0;
}
return ItemInfo.Amount;
}

View File

@@ -0,0 +1,329 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_ItemMultiStackCollection.h"
#include "GIS_InventorySystemComponent.h"
#include "GIS_InventoryTags.h"
#include "GIS_ItemDefinition.h"
#include "Items/GIS_ItemInstance.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemMultiStackCollection)
UGIS_ItemMultiStackCollectionDefinition::UGIS_ItemMultiStackCollectionDefinition()
{
StackSizeLimitAttribute = GIS_AttributeTags::StackSizeLimit;
}
TSubclassOf<UGIS_ItemCollection> UGIS_ItemMultiStackCollectionDefinition::GetCollectionInstanceClass() const
{
return UGIS_ItemMultiStackCollection::StaticClass();
}
UGIS_ItemMultiStackCollection::UGIS_ItemMultiStackCollection(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer)
{
}
bool UGIS_ItemMultiStackCollection::GetItemInfo(const UGIS_ItemInstance* Item, FGIS_ItemInfo& OutItemInfo) const
{
if (Item == nullptr)
{
return false;
}
bool bFoundSimilar = false;
FGIS_ItemInfo SimilarItemInfo;
for (int32 i = Container.Stacks.Num() - 1; i >= 0; i--)
{
const FGIS_ItemStack& ItemStack = Container.Stacks[i];
if (!ItemStack.IsValidStack())
{
continue;
}
if (!Item->IsUnique() && ItemStack.Item->GetDefinition() == Item->GetDefinition())
{
SimilarItemInfo = FGIS_ItemInfo(ItemStack);
bFoundSimilar = true;
}
if (Container.Stacks[i].Item->GetItemId() != Item->GetItemId())
{
continue;
}
OutItemInfo = FGIS_ItemInfo(Container.Stacks[i]);
return true;
}
if (bFoundSimilar)
{
OutItemInfo = SimilarItemInfo;
}
return bFoundSimilar;
}
int32 UGIS_ItemMultiStackCollection::GetItemAmountFittingInLimitedAdditionalStacks(const FGIS_ItemInfo& ItemInfo, int32 AvailableAdditionalStacks) const
{
int32 AmountToAdd = ItemInfo.Amount;
int32 MaxStackSize = GetMaxStackSize(ItemInfo.Item);
for (int32 i = 0; i < Container.Stacks.Num(); i++)
{
const FGIS_ItemStack& itemStack = Container.Stacks[i];
if (CanItemStack(ItemInfo, itemStack) == false) { continue; }
if (itemStack.Amount == MaxStackSize) { continue; }
int32 TotalToSet = itemStack.Amount + AmountToAdd;
int32 SizeDifference = TotalToSet - MaxStackSize;
if (SizeDifference <= 0)
{
AmountToAdd = 0;
break;
}
AmountToAdd = SizeDifference;
}
int32 StacksToAdd = AmountToAdd / MaxStackSize;
int32 RemainderStack = AmountToAdd % MaxStackSize;
if (AvailableAdditionalStacks > StacksToAdd)
{
return ItemInfo.Amount;
}
if (AvailableAdditionalStacks == StacksToAdd)
{
return ItemInfo.Amount - RemainderStack;
}
return ItemInfo.Amount - RemainderStack - MaxStackSize * (StacksToAdd - AvailableAdditionalStacks);
}
FGIS_ItemInfo UGIS_ItemMultiStackCollection::AddInternal(const FGIS_ItemInfo& ItemInfo)
{
// total amounts of item need to add.
int32 AmountToAdd = ItemInfo.Amount;
int32 MaxStackSize = GetMaxStackSize(ItemInfo.Item);
// temp struct to record added stack.
FGIS_ItemStack AddedItemStack;
{
//try adding item amount on top of the existing stack. 尝试在指定栈上新增数量。
int32 TargetStackIdx = Container.IndexOfById(ItemInfo.StackId);
if (TargetStackIdx != INDEX_NONE)
{
check(Container.Stacks.IsValidIndex(TargetStackIdx))
const FGIS_ItemStack& TargetStack = Container.Stacks[TargetStackIdx];
//Make sure the target stack is valid and can be stacked with new item. 确保指定栈有效且可与请求的道具信息堆叠。
if (TargetStack.IsValidStack() && CanItemStack(ItemInfo, TargetStack))
{
AddedItemStack = TargetStack;
IncreaseStackAmount(TargetStackIdx, MaxStackSize, AmountToAdd);
}
}
}
//尝试在已经存在的兼容栈上新增数量。
for (int32 i = 0; i < Container.Stacks.Num(); i++)
{
const FGIS_ItemStack& ItemStack = Container.Stacks[i];
if (CanItemStack(ItemInfo, ItemStack) == false)
{
continue;
}
AddedItemStack = ItemStack;
int32 AmountAdded = IncreaseStackAmount(i, MaxStackSize, AmountToAdd);
}
/**
* 30/10=3,30%10=0 need 3 stacks.
* 25/10=2,25%10=1 need 3 stacks.
* 77/21=3,77%21=1 need 4 stacks. (4*21 = 84) > 77
*/
int32 StacksToAdd = AmountToAdd / MaxStackSize;
int32 RemainderStack = AmountToAdd % MaxStackSize;
for (int32 i = 0; i < StacksToAdd; i++)
{
FGIS_ItemStack NewItemStack;
NewItemStack.Initialize(FGuid::NewGuid(), ItemInfo.Item, MaxStackSize, this);
AddedItemStack = NewItemStack;
AddItemStack(NewItemStack);
}
if (RemainderStack != 0)
{
FGIS_ItemStack NewItemStack;
NewItemStack.Initialize(FGuid::NewGuid(), ItemInfo.Item, RemainderStack, this);
AddedItemStack = NewItemStack;
AddItemStack(NewItemStack);
}
return FGIS_ItemInfo(ItemInfo.Item, AddedItemStack.Amount, this);
}
int32 UGIS_ItemMultiStackCollection::GetMaxStackSize(UGIS_ItemInstance* Item) const
{
const UGIS_ItemMultiStackCollectionDefinition* MyDefinition = CastChecked<UGIS_ItemMultiStackCollectionDefinition>(Definition);
if (!Item->GetDefinition()->HasIntegerAttribute(MyDefinition->StackSizeLimitAttribute))
{
return MyDefinition->DefaultStackSizeLimit;
}
return Item->GetDefinition()->GetIntegerAttribute(MyDefinition->StackSizeLimitAttribute);
// if (!Item->HasIntegerAttribute(MyDefinition->StackSizeLimitAttribute))
// {
// return MyDefinition->DefaultStackSizeLimit;
// }
// return Item->GetIntegerAttribute(MyDefinition->StackSizeLimitAttribute);
}
/**
* 案例: 每个栈最多放10个现在有35个苹果占用4个栈这两个栈的ID分别是ABCD。它们的分布是[(A:10),(B:10)(C:10)(D:5)]
* 假设传入的ItemInfo是栈A里删除11个苹果那么先从栈A删除10个这时候栈A空了还需要在栈B里移除1个。最后的栈分布是[(B:9),(C:10),(D:5)]
* 假设传入的ItemInfo是栈A里删除34个苹果那么先从栈ABC都删除10个ABC清空然后剩下栈D里删除4个最后的栈分布是[(D:1)]
*/
FGIS_ItemInfo UGIS_ItemMultiStackCollection::RemoveInternal(const FGIS_ItemInfo& ItemInfo)
{
int32 AlreadyRemoved = 0;
int32 AmountToRemove = ItemInfo.Amount;
//上一次移除的StackIndex
int32 PreviousStackIndexWithSameItem = INDEX_NONE;
int32 MaxStackSize = GetMaxStackSize(ItemInfo.Item);
FGIS_ItemStack RemovedItemStack;
{
// Try remove from existing stack first.
int32 StackIdx = Container.IndexOfById(ItemInfo.StackId);
if (StackIdx != INDEX_NONE)
{
check(Container.Stacks.IsValidIndex(StackIdx))
RemovedItemStack = Container.Stacks[StackIdx];
PreviousStackIndexWithSameItem = RemoveItemFromStack(StackIdx, PreviousStackIndexWithSameItem, MaxStackSize, AmountToRemove, AlreadyRemoved);
}
}
// 继续从其他栈中移除这个道具
TArray<FGIS_ItemStack> TempStacks = Container.Stacks;
for (int i = TempStacks.Num() - 1; i >= 0; i--)
{
if (AmountToRemove <= 0) { break; }
if (TempStacks[i].Item == nullptr || TempStacks[i].Item->GetItemId() != ItemInfo.Item->GetItemId()) { continue; }
//忽略前面移除的Stack.
if (RemovedItemStack == TempStacks[i]) { continue; }
RemovedItemStack = TempStacks[i];
PreviousStackIndexWithSameItem = RemoveItemFromStack(i, PreviousStackIndexWithSameItem, MaxStackSize, AmountToRemove, AlreadyRemoved);
}
//No any stacks contain this item instance; 无任何包含此道具实例的道具栈,因此可以从该集合完全移除。
if (PreviousStackIndexWithSameItem == INDEX_NONE)
{
ItemInfo.Item->UnassignCollection(this);
if (OwningInventory->IsReplicatedSubObjectRegistered(ItemInfo.Item))
{
OwningInventory->RemoveReplicatedSubObject(ItemInfo.Item);
}
}
if (AlreadyRemoved == 0)
{
return FGIS_ItemInfo(ItemInfo.Item, AlreadyRemoved, this);
}
return FGIS_ItemInfo(ItemInfo.Item, AlreadyRemoved, this);
}
int32 UGIS_ItemMultiStackCollection::RemoveItemFromStack(int32 Index, int32 PrevStackIndexWithSameItem, int32 MaxStackSize, int32& AmountToRemove, int32& AlreadyRemoved)
{
check(Container.Stacks.IsValidIndex(Index));
int32 AmountInStack = Container.Stacks[Index].Amount;
if (PrevStackIndexWithSameItem != INDEX_NONE)
{
check(Container.Stacks.IsValidIndex(PrevStackIndexWithSameItem))
int32 MergedAmount = AmountInStack + Container.Stacks[PrevStackIndexWithSameItem].Amount;
if (MergedAmount > MaxStackSize) //假设每个栈最大100当前的栈有70之前的栈有60即一共130就超了30.
{
UpdateItemStackAmountAtIndex(PrevStackIndexWithSameItem, MaxStackSize); //让之前的栈满即60+40=100
AmountInStack = MergedAmount - MaxStackSize; //再让当前栈变成 130-100=30(即少了70-40=30)
}
else //the total size of 2 stacks doesn't reach max stack size. 假设每个栈最大100当前的栈有70之前的栈有10即一共80没有超过100.
{
// merge current stack's amount into prev stack. //让之前栈为10+70=80.
UpdateItemStackAmountAtIndex(PrevStackIndexWithSameItem, MergedAmount);
// and empty this one. 当前栈归0.
AmountInStack = 0;
}
}
if (AmountToRemove == 0) { return PrevStackIndexWithSameItem; }
int32 NewAmount = AmountInStack - AmountToRemove;
if (NewAmount <= 0)
{
AmountToRemove = -NewAmount;
AlreadyRemoved += AmountInStack;
//Item can be stored within multiple stacks, we don't need to remove it from this collection now. Item在此集合可以属于多个栈不在这里移除。
RemoveItemStackAtIndex(Index, false);
}
else
{
AlreadyRemoved += AmountToRemove;
AmountToRemove = 0;
UpdateItemStackAmountAtIndex(Index, NewAmount);
PrevStackIndexWithSameItem = Index;
}
return PrevStackIndexWithSameItem;
}
/**
* 代入: Stack(item,70);MaxStackSize(100),AmountToAdd(50) 结果Stack(item,100), AmountToAdd(20), 返回实际添加30
* 代入: Stack(item,40);MaxStackSize(100),AmountToAdd(50) 结果Stack(item,90), AmountToAdd(0), 返回实际添加50
*/
int32 UGIS_ItemMultiStackCollection::IncreaseStackAmount(int32 StackIdx, int32 MaxStackSize, int32& AmountToAdd)
{
check(Container.Stacks.IsValidIndex(StackIdx))
const FGIS_ItemStack& ItemStack = Container.Stacks[StackIdx];
if (ItemStack.Amount == MaxStackSize)
{
return 0;
}
int32 OriginAmountToAdd = AmountToAdd;
int32 NewAmount = ItemStack.Amount + AmountToAdd;
int32 OverflowedAmount = NewAmount - MaxStackSize; // <=0 no overflow >0 means overflow.
// This stack can hold the new amount. 这个栈装得下。
if (OverflowedAmount <= 0)
{
AmountToAdd = 0; //all amounts have been added. 无需再添加。
}
else //This stack overflow. 这个栈溢出了。
{
NewAmount = MaxStackSize;
//Still need to add overflowed amount. 超过最大尺寸后的剩余要增加的数量
AmountToAdd = OverflowedAmount;
}
UpdateItemStackAmountAtIndex(StackIdx, NewAmount);
//实际增加的数量
return OriginAmountToAdd - AmountToAdd;
}

View File

@@ -0,0 +1,26 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_ItemRestriction.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemRestriction)
bool UGIS_ItemRestriction::CanAddItem(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const
{
return CanAddItemInternal(ItemInfo, ReceivingCollection);
}
bool UGIS_ItemRestriction::CanRemoveItem(FGIS_ItemInfo& ItemInfo) const
{
return CanRemoveItemInternal(ItemInfo);
}
bool UGIS_ItemRestriction::CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const
{
return true;
}
bool UGIS_ItemRestriction::CanRemoveItemInternal_Implementation(FGIS_ItemInfo& ItemInfo) const
{
return true;
}

View File

@@ -0,0 +1,57 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_ItemRestriction_StackSizeLimit.h"
#include "GIS_ItemCollection.h"
#include "GIS_ItemDefinition.h"
#include "Items/GIS_ItemInstance.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemRestriction_StackSizeLimit)
UGIS_ItemRestriction_StackSizeLimit::UGIS_ItemRestriction_StackSizeLimit()
{
DefaultStackSizeLimit = 99;
}
bool UGIS_ItemRestriction_StackSizeLimit::CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const
{
int32 MaxStackSize = GetStackSizeLimit(ItemInfo.Item);
FGIS_ItemInfo ExistingItemInfoResult;
if (!ReceivingCollection->GetItemInfo(ItemInfo.Item, ExistingItemInfoResult))
{
ItemInfo.Amount = FMath::Min(ItemInfo.Amount, MaxStackSize);
return true;
}
int32 ItemAmountsThatFit = FMath::Min(MaxStackSize - ExistingItemInfoResult.Amount, ItemInfo.Amount);
ItemAmountsThatFit = FMath::Max(0, ItemAmountsThatFit);
if (ItemAmountsThatFit == 0)
{
return false;
}
ItemInfo.Amount = ItemAmountsThatFit;
return true;
}
int32 UGIS_ItemRestriction_StackSizeLimit::GetStackSizeLimit(const UGIS_ItemInstance* Item) const
{
if (Item == nullptr)
{
return 0;
}
if (StackSizeLimitAttributeTag.IsValid() && Item->GetDefinition()->HasIntegerAttribute(StackSizeLimitAttributeTag))
{
return Item->GetDefinition()->GetIntegerAttribute(StackSizeLimitAttributeTag);
}
// if (StackSizeLimitAttributeTag.IsValid() && Item->HasIntegerAttribute(StackSizeLimitAttributeTag))
// {
// return Item->GetIntegerAttribute(StackSizeLimitAttributeTag);
// }
return DefaultStackSizeLimit;
}

View File

@@ -0,0 +1,26 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_ItemRestriction_StacksNumLimit.h"
#include "GIS_InventorySystemComponent.h"
#include "GIS_ItemCollection.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemRestriction_StacksNumLimit)
bool UGIS_ItemRestriction_StacksNumLimit::CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const
{
int32 ExistingNum = ReceivingCollection->GetItemStacksNum();
int32 AvailableAdditionalStacks = MaxStacksNum - ExistingNum;
int32 ItemAmountsThatFit = ReceivingCollection->GetItemAmountFittingInLimitedAdditionalStacks(ItemInfo, AvailableAdditionalStacks);
if (ItemAmountsThatFit == 0)
{
return false;
}
ItemInfo = FGIS_ItemInfo(ItemAmountsThatFit, ItemInfo);
return true;
}

View File

@@ -0,0 +1,18 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_ItemRestriction_TagRequirements.h"
#include "Items/GIS_ItemInstance.h"
#include "GIS_ItemCollection.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemRestriction_TagRequirements)
bool UGIS_ItemRestriction_TagRequirements::CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const
{
if (TagQuery.IsEmpty())
{
return true;
}
return TagQuery.Matches(ItemInfo.Item->GetItemTags());
}

View File

@@ -0,0 +1,16 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_ItemRestriction_UniqueOnly.h"
#include "Items/GIS_ItemInstance.h"
#include "GIS_ItemCollection.h"
#include "Items/GIS_ItemDefinition.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemRestriction_UniqueOnly)
bool UGIS_ItemRestriction_UniqueOnly::CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const
{
// this restriction will not modify the item info will be added, so just set it to OutItemInfo
return ItemInfo.Item->IsUnique();
}

View File

@@ -0,0 +1,583 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_ItemSlotCollection.h"
#include "GameFramework/Actor.h"
#include "GIS_InventorySystemComponent.h"
#include "Items/GIS_ItemInstance.h"
#include "GIS_LogChannels.h"
#include "Misc/DataValidation.h"
#include "UObject/ObjectSaveContext.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemSlotCollection)
TSubclassOf<UGIS_ItemCollection> UGIS_ItemSlotCollectionDefinition::GetCollectionInstanceClass() const
{
return UGIS_ItemSlotCollection::StaticClass();
}
bool UGIS_ItemSlotCollectionDefinition::IsValidSlotIndex(int32 SlotIndex) const
{
return SlotDefinitions.IsValidIndex(SlotIndex);
}
int32 UGIS_ItemSlotCollectionDefinition::GetIndexOfSlot(const FGameplayTag& SlotName) const
{
return TagToIndexMap.Contains(SlotName) ? TagToIndexMap[SlotName] : INDEX_NONE;
}
FGameplayTag UGIS_ItemSlotCollectionDefinition::GetSlotOfIndex(int32 SlotIndex) const
{
return SlotDefinitions.IsValidIndex(SlotIndex) ? SlotDefinitions[SlotIndex].Tag : FGameplayTag::EmptyTag;
}
const TArray<FGIS_ItemSlotDefinition>& UGIS_ItemSlotCollectionDefinition::GetSlotDefinitions() const
{
return SlotDefinitions;
}
bool UGIS_ItemSlotCollectionDefinition::GetSlotDefinition(int32 SlotIndex, FGIS_ItemSlotDefinition& OutDefinition) const
{
if (SlotDefinitions.IsValidIndex(SlotIndex))
{
OutDefinition = SlotDefinitions[SlotIndex];
return true;
}
return false;
}
bool UGIS_ItemSlotCollectionDefinition::GetSlotDefinition(const FGameplayTag& SlotName, FGIS_ItemSlotDefinition& OutDefinition) const
{
if (TagToIndexMap.Contains(SlotName))
{
check(SlotDefinitions.IsValidIndex(TagToIndexMap[SlotName]));
OutDefinition = SlotDefinitions[TagToIndexMap[SlotName]];
return true;
}
return false;
}
int32 UGIS_ItemSlotCollectionDefinition::GetSlotIndexWithinGroup(FGameplayTag GroupTag, FGameplayTag SlotTag) const
{
if (SlotGroupMap.Contains(GroupTag))
{
if (SlotGroupMap[GroupTag].SlotToIndexMap.Contains(SlotTag))
{
return SlotGroupMap[GroupTag].SlotToIndexMap[SlotTag];
}
}
return INDEX_NONE;
}
UGIS_ItemSlotCollection::UGIS_ItemSlotCollection(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
}
void UGIS_ItemSlotCollection::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// DOREPLIFETIME(ThisClass, ItemBySlots)
}
const UGIS_ItemSlotCollectionDefinition* UGIS_ItemSlotCollection::GetMyDefinition() const
{
return MyDefinition;
}
FGIS_ItemInfo UGIS_ItemSlotCollection::AddItem(const FGIS_ItemInfo& ItemInfo)
{
if (!ItemInfo.IsValid())
{
return FGIS_ItemInfo(ItemInfo.Item, 0, this);
}
return AddItem(ItemInfo, GetTargetSlotIndex(ItemInfo.Item));
}
FGIS_ItemInfo UGIS_ItemSlotCollection::AddItem(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex)
{
FGIS_ItemInfo ItemInfoAdded = AddItemInternal(ItemInfo, SlotIndex);
if (ItemInfoAdded.Amount < ItemInfo.Amount)
{
HandleItemOverflow(ItemInfo, ItemInfoAdded);
}
return ItemInfoAdded;
}
void UGIS_ItemSlotCollection::ServerAddItem_Implementation(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex)
{
AddItem(ItemInfo, SlotIndex);
}
FGIS_ItemInfo UGIS_ItemSlotCollection::AddItemBySlotName(const FGIS_ItemInfo& ItemInfo, FGameplayTag SlotName)
{
int32 Index = MyDefinition->GetIndexOfSlot(SlotName);
if (Index <= INDEX_NONE)
{
return FGIS_ItemInfo(ItemInfo.Item, 0, this);
}
return AddItem(ItemInfo, Index);
}
void UGIS_ItemSlotCollection::ServerAddItemBySlotName_Implementation(const FGIS_ItemInfo& ItemInfo, FGameplayTag SlotName)
{
AddItemBySlotName(ItemInfo, SlotName);
}
FGIS_ItemInfo UGIS_ItemSlotCollection::RemoveItem(const FGIS_ItemInfo& ItemInfo)
{
int32 SlotIndex = GetItemSlotIndex(ItemInfo.Item);
if (SlotIndex == INDEX_NONE)
{
return FGIS_ItemInfo(ItemInfo.Item, 0, this);
}
return RemoveItem(SlotIndex, ItemInfo.Amount);
}
FGIS_ItemInfo UGIS_ItemSlotCollection::RemoveItem(int32 SlotIndex, int32 Amount)
{
if (!SlotToStackMap.Contains(SlotIndex))
{
return FGIS_ItemInfo(nullptr, 0, this);
}
const FGIS_ItemStack* ItemToRemove = Container.FindById(SlotToStackMap[SlotIndex]);
if (ItemToRemove == nullptr)
{
return FGIS_ItemInfo(nullptr, 0, this);
}
int32 AmountToRemove = Amount > 0 ? Amount : ItemToRemove->Amount;
FGIS_ItemInfo Removed = RemoveInternal(FGIS_ItemInfo(ItemToRemove->Item, AmountToRemove, this, ItemToRemove->Id));
return Removed;
}
bool UGIS_ItemSlotCollection::IsItemFitWithSlot(const UGIS_ItemInstance* Item, int32 SlotIndex) const
{
if (!IsValid(Item))
{
GIS_CLOG(Error, "invalid item")
return false;
}
const TArray<FGIS_ItemSlotDefinition>& SlotDefinitions = MyDefinition->SlotDefinitions;
if (SlotDefinitions.IsEmpty() || !SlotDefinitions.IsValidIndex(SlotIndex))
{
GIS_CLOG(Error, "has empty slots!")
return false;
}
const FGIS_ItemSlotDefinition& SlotDefinition = SlotDefinitions[SlotIndex];
// doesn't match definition.
if (!SlotDefinition.MatchItem(Item))
{
return false;
}
// slot is empty.
if (!SlotToStackMap.Contains(SlotIndex))
{
return true;
}
// slot is not empty, check if it can stack.
if (!Item->IsUnique())
{
if (const FGIS_ItemStack* ExistingStack = Container.FindById(SlotToStackMap[SlotIndex]))
{
if (ExistingStack->Item->StackableEquivalentTo(Item))
{
return true;
}
}
}
// can't stack, check new item can replace existing item.
return MyDefinition->bNewItemPriority;
}
int32 UGIS_ItemSlotCollection::GetTargetSlotIndex(const UGIS_ItemInstance* Item) const
{
const TArray<FGIS_ItemSlotDefinition>& SlotDefinitions = MyDefinition->SlotDefinitions;
if (SlotDefinitions.IsEmpty())
{
GIS_CLOG(Error, "has empty slots!")
return INDEX_NONE;
}
int32 FoundUsedSlot = INDEX_NONE;
int32 FoundEmptySlot = INDEX_NONE;
for (int32 i = 0; i < SlotDefinitions.Num(); i++)
{
const FGIS_ItemSlotDefinition& SlotDefinition = SlotDefinitions[i];
if (!SlotDefinition.MatchItem(Item))
{
continue;
}
FoundUsedSlot = i;
// i对应的槽为空
if (!SlotToStackMap.Contains(i))
{
if (FoundEmptySlot != INDEX_NONE)
{
continue;
}
FoundEmptySlot = i;
continue;
}
//非唯一且可堆叠.
if (!Item->IsUnique())
{
if (const FGIS_ItemStack* ExistingStack = Container.FindById(SlotToStackMap[i]))
{
if (ExistingStack->Item->StackableEquivalentTo(Item))
{
return i;
}
}
}
}
if (FoundEmptySlot != INDEX_NONE)
{
return FoundEmptySlot;
}
return FoundUsedSlot;
}
bool UGIS_ItemSlotCollection::GetItemInfoAtSlot(FGameplayTag SlotTag, FGIS_ItemInfo& OutItemInfo) const
{
int32 Index = MyDefinition->GetIndexOfSlot(SlotTag);
if (Index != INDEX_NONE)
{
OutItemInfo = GetItemInfoAtSlot(Index);
return OutItemInfo.IsValid();
}
return false;
}
bool UGIS_ItemSlotCollection::FindItemInfoAtSlot(FGameplayTag SlotTag, FGIS_ItemInfo& OutItemInfo) const
{
return GetItemInfoAtSlot(SlotTag, OutItemInfo);
}
bool UGIS_ItemSlotCollection::GetItemStackAtSlot(FGameplayTag SlotTag, FGIS_ItemStack& OutItemStack) const
{
int32 Index = MyDefinition->GetIndexOfSlot(SlotTag);
if (Index != INDEX_NONE)
{
OutItemStack = GetItemStackAtSlot(Index);
return OutItemStack.IsValidStack();
}
return false;
}
bool UGIS_ItemSlotCollection::FindItemStackAtSlot(FGameplayTag SlotTag, FGIS_ItemStack& OutItemStack) const
{
return GetItemStackAtSlot(SlotTag, OutItemStack);
}
FGIS_ItemInfo UGIS_ItemSlotCollection::GetItemInfoAtSlot(int32 SlotIndex) const
{
if (SlotIndex < 0) { return FGIS_ItemInfo::None; }
FGIS_ItemStack ItemStack = GetItemStackAtSlot(SlotIndex);
if (ItemStack.IsValidStack())
{
return FGIS_ItemInfo(ItemStack.Item, ItemStack.Amount, ItemStack.Collection);
}
return FGIS_ItemInfo::None;
}
FGIS_ItemStack UGIS_ItemSlotCollection::GetItemStackAtSlot(int32 SlotIndex) const
{
if (SlotToStackMap.Contains(SlotIndex))
{
if (const FGIS_ItemStack* Stack = Container.FindById(SlotToStackMap[SlotIndex]))
{
return *Stack;
}
}
return FGIS_ItemStack();
}
FGameplayTag UGIS_ItemSlotCollection::GetItemSlotName(const UGIS_ItemInstance* Item) const
{
int32 SlotIndex = GetItemSlotIndex(Item);
return MyDefinition->GetSlotOfIndex(SlotIndex);
}
int32 UGIS_ItemSlotCollection::GetItemSlotIndex(const UGIS_ItemInstance* Item) const
{
const TArray<FGIS_ItemSlotDefinition>& SlotDefinitions = MyDefinition->SlotDefinitions;
int32 StackableEquivalentItemIndex = INDEX_NONE;
for (int i = 0; i < SlotDefinitions.Num(); i++)
{
const FGIS_ItemSlotDefinition& SlotDefinition = SlotDefinitions[i];
if (!SlotDefinition.MatchItem(Item))
{
continue;
}
if (!SlotToStackMap.Contains(i) || !SlotToStackMap[i].IsValid())
{
continue;
}
if (const FGIS_ItemStack* Stack = Container.FindById(SlotToStackMap[i]))
{
if (Stack->Item == Item)
{
return i;
}
if (Stack->Item->StackableEquivalentTo(Item))
{
StackableEquivalentItemIndex = i;
}
}
}
return StackableEquivalentItemIndex;
}
FGIS_ItemInfo UGIS_ItemSlotCollection::AddItemInternal(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex)
{
FGIS_ItemInfo CanAddItemInfo;
const bool CanAddResult = CanAddItem(ItemInfo, CanAddItemInfo);
if (!CanAddResult)
{
return FGIS_ItemInfo(ItemInfo.Item, 0, this);
}
if (SlotIndex == INDEX_NONE)
{
GIS_CLOG(Warning, "invalid valid target slot(%d) to put item:%s", SlotIndex, *ItemInfo.GetDebugString())
return FGIS_ItemInfo(ItemInfo.Item, 0, this);
}
FGIS_ItemSlotDefinition SlotDefinition;
if (!MyDefinition->GetSlotDefinition(SlotIndex, SlotDefinition))
{
GIS_CLOG(Verbose, "No slot definition found for slot index:%d", SlotIndex)
return FGIS_ItemInfo(ItemInfo.Item, 0, this);
}
if (!MyDefinition->bNewItemPriority && CanAddItemInfo.Item->IsUnique() && SlotToStackMap.Contains(SlotIndex) && SlotToStackMap[SlotIndex].IsValid())
{
GIS_CLOG(Verbose, "Can't add item info because the target slot:%s was occupied. ItemInfo:%s.", *SlotDefinition.Tag.ToString(), *ItemInfo.GetDebugString())
return FGIS_ItemInfo(ItemInfo.Item, 0, this);
}
// similar item amount for this item.
int32 CurrentAmount = GetItemAmount(CanAddItemInfo.Item.Get());
FGIS_ItemInfo SetItemInfo;
if (SetItemAmount(FGIS_ItemInfo(CanAddItemInfo.Amount + CurrentAmount, CanAddItemInfo), SlotIndex, true, SetItemInfo))
{
return SetItemInfo;
}
return FGIS_ItemInfo(ItemInfo.Item, 0, this);
}
int32 UGIS_ItemSlotCollection::StackIdToSlotIndex(FGuid InStackId) const
{
if (!InStackId.IsValid())
{
return INDEX_NONE;
}
if (StackToIdxMap.Contains(InStackId))
{
return StackToIdxMap[InStackId];
}
return INDEX_NONE;
}
int32 UGIS_ItemSlotCollection::SlotIndexToStackIndex(int32 InSlotIndex) const
{
if (SlotToStackMap.Contains(InSlotIndex))
{
return Container.IndexOfById(SlotToStackMap[InSlotIndex]);
}
return INDEX_NONE;
}
FGuid UGIS_ItemSlotCollection::SlotIndexToStackId(int32 InSlotIndex) const
{
return SlotToStackMap.Contains(InSlotIndex) && SlotToStackMap[InSlotIndex].IsValid() ? SlotToStackMap[InSlotIndex] : FGuid();
}
void UGIS_ItemSlotCollection::OnRep_ItemsBySlot()
{
}
void UGIS_ItemSlotCollection::SetDefinition(const UGIS_ItemCollectionDefinition* NewDefinition)
{
Super::SetDefinition(NewDefinition);
check(OwningInventory != nullptr)
check(Definition != nullptr)
if (bInitialized)
{
MyDefinition = CastChecked<UGIS_ItemSlotCollectionDefinition>(Definition);
if (OwningInventory->GetOwnerRole() >= ROLE_Authority)
{
if (MyDefinition->SlotDefinitions.Num() == 0)
{
GIS_CLOG(Error, "has empty slots")
}
}
}
}
void UGIS_ItemSlotCollection::OnPreItemStackAdded(const FGIS_ItemStack& Stack, int32 Idx)
{
Super::OnPreItemStackAdded(Stack, Idx);
}
void UGIS_ItemSlotCollection::OnItemStackAdded(const FGIS_ItemStack& Stack)
{
check(Stack.IsValidStack())
if (OwningInventory)
{
SlotToStackMap.Add(Stack.Index, Stack.Id);
}
Super::OnItemStackAdded(Stack);
}
void UGIS_ItemSlotCollection::OnItemStackRemoved(const FGIS_ItemStack& Stack)
{
if (OwningInventory)
{
SlotToStackMap.Remove(Stack.Index);
}
Super::OnItemStackRemoved(Stack);
}
bool UGIS_ItemSlotCollection::SetItemAmount(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex, bool RemovePreviousItem, FGIS_ItemInfo& ItemInfoAdded)
{
if (!OwningInventory->GetOwner()->HasAuthority())
{
GIS_CLOG(Warning, "has no authority!");
return false;
}
int32 Amount = ItemInfo.Amount;
int32 StackIdx = SlotIndexToStackIndex(SlotIndex);
// Found valid stack
if (StackIdx != INDEX_NONE)
{
const FGIS_ItemStack& CurrentStack = Container.Stacks[StackIdx];
if (ItemInfo.Item->StackableEquivalentTo(CurrentStack.Item))
{
// reduce existing amount to get the amount needed to add.
Amount -= CurrentStack.Amount;
}
else if (RemovePreviousItem)
{
FGIS_ItemInfo RemovedItem = RemoveItem(FGIS_ItemInfo(CurrentStack));
if (RemovedItem.Amount > 0)
{
FGIS_ItemInfo CanAddItemInfo;
if (MyDefinition->bTryGivePrevItemToNewItemCollection && ItemInfo.ItemCollection != nullptr && ItemInfo.ItemCollection->CanAddItem(RemovedItem, CanAddItemInfo))
{
GIS_CLOG(Verbose, "An existing item has been replaced by a new one, and the old one has been added to the collection where the new item coming from. prev:%s new:%s",
*RemovedItem.GetDebugString(), *ItemInfo.GetDebugString())
ItemInfo.ItemCollection->AddItem(CanAddItemInfo);
}
else
{
GIS_CLOG(Verbose, "An item has been replaced by a new one, and the old one has gone forever! prev: %s new:%s",
*RemovedItem.GetDebugString(), *ItemInfo.GetDebugString())
}
}
}
else
{
return false;
}
}
// Not found, add new item.
ItemInfoAdded = AddInternal(FGIS_ItemInfo(Amount, SlotIndex, ItemInfo));
return true;
}
#if WITH_EDITOR
void UGIS_ItemSlotCollectionDefinition::PreSave(FObjectPreSaveContext SaveContext)
{
// remove repeated slot by name.
{
TArray<FGameplayTag> SlotNames;
TArray<FGIS_ItemSlotDefinition> Slots;
for (int32 i = 0; i < SlotDefinitions.Num(); ++i)
{
if (!SlotNames.Contains(SlotDefinitions[i].Tag))
{
Slots.Add(SlotDefinitions[i]);
SlotNames.Add(SlotDefinitions[i].Tag);
}
}
SlotDefinitions = Slots;
}
IndexToTagMap.Empty();
TagToIndexMap.Empty();
for (int32 i = 0; i < SlotDefinitions.Num(); ++i)
{
if (!SlotDefinitions[i].Tag.IsValid())
{
continue;
}
if (!IndexToTagMap.Contains(i))
{
IndexToTagMap.Add(i, SlotDefinitions[i].Tag);
}
if (!TagToIndexMap.Contains(SlotDefinitions[i].Tag))
{
TagToIndexMap.Add(SlotDefinitions[i].Tag, i);
}
}
TArray<FGIS_ItemSlotGroup> Groups;
SlotGroupMap.Empty();
for (int32 i = 0; i < SlotGroups.Num(); i++)
{
FGIS_ItemSlotGroup Group;
int32 Idx = 0;
for (int32 j = 0; j < SlotDefinitions.Num(); j++)
{
if (SlotDefinitions[j].Tag.MatchesTag(SlotGroups[i]))
{
Group.IndexToSlotMap.Add(Idx, SlotDefinitions[j].Tag);
Group.SlotToIndexMap.Add(SlotDefinitions[j].Tag, Idx);
Idx++;
}
}
if (!Group.IndexToSlotMap.IsEmpty())
{
SlotGroupMap.Add(SlotGroups[i], Group);
}
}
Super::PreSave(SaveContext);
}
#endif

View File

@@ -0,0 +1,48 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Fragments/GIS_ItemFragment.h"
#include "Items/GIS_ItemStack.h"
#include "Misc/DataValidation.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemFragment)
bool UGIS_ItemFragment::IsMixinDataSerializable() const
{
return IsStateSerializable();
}
TObjectPtr<const UScriptStruct> UGIS_ItemFragment::GetCompatibleMixinDataType() const
{
return GetCompatibleStateType();
}
bool UGIS_ItemFragment::MakeDefaultMixinData(FInstancedStruct& DefaultState) const
{
return MakeDefaultState(DefaultState);
}
bool UGIS_ItemFragment::MakeDefaultState_Implementation(FInstancedStruct& DefaultState) const
{
return false;
}
const UScriptStruct* UGIS_ItemFragment::GetCompatibleStateType_Implementation() const
{
return nullptr;
}
bool UGIS_ItemFragment::IsStateSerializable_Implementation() const
{
return false;
}
#if WITH_EDITOR
bool UGIS_ItemFragment::FragmentDataValidation_Implementation(FText& OutMessage) const
{
return true;
}
#endif

View File

@@ -0,0 +1,66 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_ItemFragment_DynamicAttributes.h"
#include "Items/GIS_ItemInstance.h"
#include "UObject/ObjectSaveContext.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemFragment_DynamicAttributes)
void UGIS_ItemFragment_DynamicAttributes::OnInstanceCreated(UGIS_ItemInstance* Instance) const
{
for (int32 i = 0; i < InitialFloatAttributes.Num(); i++)
{
if (InitialFloatAttributes[i].Tag.IsValid())
{
Instance->SetFloatAttribute(InitialFloatAttributes[i].Tag, InitialFloatAttributes[i].Value);
}
}
for (int32 i = 0; i < InitialIntegerAttributes.Num(); i++)
{
if (InitialIntegerAttributes[i].Tag.IsValid())
{
Instance->SetIntegerAttribute(InitialIntegerAttributes[i].Tag, InitialIntegerAttributes[i].Value);
}
}
// for (int32 i=0;i<InitialBoolAttributes.Num();i++)
// {
// Instance->SetBoolAttribute(InitialBoolAttributes[i].Tag,InitialBoolAttributes[i].Value);
// }
}
float UGIS_ItemFragment_DynamicAttributes::GetFloatAttributeDefault(FGameplayTag AttributeTag) const
{
return FloatAttributeMap.Contains(AttributeTag) ? FloatAttributeMap[AttributeTag] : 0;
}
int32 UGIS_ItemFragment_DynamicAttributes::GetIntegerAttributeDefault(FGameplayTag AttributeTag) const
{
return IntegerAttributeMap.Contains(AttributeTag) ? IntegerAttributeMap[AttributeTag] : 0;
}
#if WITH_EDITOR
void UGIS_ItemFragment_DynamicAttributes::PreSave(FObjectPreSaveContext SaveContext)
{
FloatAttributeMap.Empty();
for (const FGIS_GameplayTagFloat& Attribute : InitialFloatAttributes)
{
if (FloatAttributeMap.Contains(Attribute.Tag))
{
FloatAttributeMap.Add(Attribute.Tag, Attribute.Value);
}
}
IntegerAttributeMap.Empty();
for (const FGIS_GameplayTagInteger& Attribute : InitialIntegerAttributes)
{
if (IntegerAttributeMap.Contains(Attribute.Tag))
{
IntegerAttributeMap.Add(Attribute.Tag, Attribute.Value);
}
}
Super::PreSave(SaveContext);
}
#endif

View File

@@ -0,0 +1,34 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_ItemFragment_Equippable.h"
#include "GIS_EquipmentInstance.h"
#include "UObject/ObjectSaveContext.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemFragment_Equippable)
UGIS_ItemFragment_Equippable::UGIS_ItemFragment_Equippable()
{
InstanceType = UGIS_EquipmentInstance::StaticClass();
}
#if WITH_EDITOR
void UGIS_ItemFragment_Equippable::PreSave(FObjectPreSaveContext SaveContext)
{
if (InstanceType.IsNull())
{
bActorBased = false;
}
else
{
UClass* InstanceClass = InstanceType.LoadSynchronous();
if (InstanceClass != nullptr)
{
bActorBased = InstanceClass->IsChildOf(AActor::StaticClass());
}
}
Super::PreSave(SaveContext);
}
#endif

View File

@@ -0,0 +1,12 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_ItemFragment_Shoppable.h"
#include "GIS_InventoryTags.h"
#include "Items/GIS_ItemInstance.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemFragment_Shoppable)
void UGIS_ItemFragment_Shoppable::OnInstanceCreated(UGIS_ItemInstance* Instance) const
{
}

View File

@@ -0,0 +1,42 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_CoreStructLibray.h"
#include "Items/GIS_ItemDefinition.h"
#include "Items/GIS_ItemInstance.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CoreStructLibray)
FGIS_ItemDefinitionAmount::FGIS_ItemDefinitionAmount()
{
Definition = nullptr;
Amount = 1;
}
FGIS_ItemDefinitionAmount::FGIS_ItemDefinitionAmount(TSoftObjectPtr<UGIS_ItemDefinition> InDefinition, int32 InAmount)
{
Definition = InDefinition;
Amount = InAmount;
}
bool FGIS_ItemSlotDefinition::MatchItem(const UGIS_ItemInstance* Item) const
{
if (!IsValid(Item))
{
return false;
}
if (TagQuery.IsEmpty())
{
return false;
}
if (TagQuery.Matches(Item->GetDefinition()->ItemTags))
{
return true;
}
return false;
}

View File

@@ -0,0 +1,385 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_MixinContainer.h"
#include "Engine/World.h"
#include "AssetRegistry/AssetData.h"
#include "GIS_ItemFragment.h"
#include "GIS_LogChannels.h"
#include "GIS_MixinOwnerInterface.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Items/GIS_ItemInstance.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_MixinContainer)
uint32 GetTypeHash(const FGIS_Mixin& Entry)
{
return HashCombine(GetTypeHash(Entry.Target), Entry.Timestamp);
}
int32 FGIS_MixinContainer::IndexOfTarget(const UObject* Target) const
{
if (!IsValid(Target))
{
return INDEX_NONE;
}
return AcceleratedMap.Contains(Target->GetClass()) ? AcceleratedMap[Target->GetClass()] : INDEX_NONE;
}
int32 FGIS_MixinContainer::IndexOfTargetByClass(const TSubclassOf<UObject>& TargetClass) const
{
check(IsValid(TargetClass));
const int32* Idx = AcceleratedMap.Find(TargetClass);
if (Idx != nullptr)
{
return *Idx;
}
return INDEX_NONE;
}
// bool FGIS_MixinContainer::GetDataByTargetClass(const TSubclassOf<UObject>& TargetClass, FInstancedStruct& OutData) const
// {
// if (AcceleratedMap.Contains(TargetClass) && Mixins.IsValidIndex(AcceleratedMap[TargetClass]))
// {
// OutData = Mixins[AcceleratedMap[TargetClass]].Data;
// return true;
// }
// return false;
// }
bool FGIS_MixinRecord::operator==(const FGIS_MixinRecord& Other) const
{
return TargetPath == Other.TargetPath && Data.GetScriptStruct() == Other.Data.GetScriptStruct();
}
bool FGIS_MixinRecord::IsValid() const
{
return !TargetPath.IsEmpty() && Data.IsValid();
}
bool FGIS_MixinContainer::GetDataByTarget(const UObject* Target, FInstancedStruct& OutData) const
{
if (AcceleratedMap.Contains(Target) && Mixins.IsValidIndex(AcceleratedMap[Target]))
{
OutData = Mixins[AcceleratedMap[Target]].Data;
return true;
}
return false;
}
int32 FGIS_MixinContainer::SetDataForTarget(const TObjectPtr<const UObject>& Target, const FInstancedStruct& Data)
{
if (Target == nullptr || !Data.IsValid())
{
return INDEX_NONE;
}
if (!IsObjectLoadedFromDisk(Target))
{
return INDEX_NONE;
}
if (AcceleratedMap.Contains(Target))
{
return UpdateDataAt(AcceleratedMap[Target], Data);
}
// is not valid class -> data pair.
if (!CheckCompatibility(Target, Data))
{
return INDEX_NONE;
}
int32 Idx = Mixins.AddDefaulted();
FGIS_Mixin& NewMixin = Mixins[Idx];
NewMixin.Target = Target;
NewMixin.Data = Data;
NewMixin.Timestamp = OwningObject->GetWorld()->GetTimeSeconds();
if (IGIS_MixinOwnerInterface* MixinOwner = Cast<IGIS_MixinOwnerInterface>(OwningObject))
{
MixinOwner->OnMixinDataAdded(NewMixin.Target, NewMixin.Data);
}
MarkItemDirty(NewMixin);
CacheMixins();
return Idx;
}
bool FGIS_MixinContainer::IsObjectLoadedFromDisk(const UObject* Object) const
{
if (!IsValid(Object))
{
return false;
}
// 获取资源路径
FSoftObjectPath AssetPath(Object);
if (AssetPath.IsNull())
{
return false;
}
// 检查资产注册表
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
FAssetData AssetData = AssetRegistry.GetAssetByObjectPath(AssetPath);
return AssetData.IsValid();
}
int32 FGIS_MixinContainer::UpdateDataByTargetClass(const TSubclassOf<UObject>& TargetClass, const FInstancedStruct& Data)
{
if (AcceleratedMap.Contains(TargetClass))
{
return UpdateDataAt(AcceleratedMap[TargetClass], Data);
}
return INDEX_NONE;
}
int32 FGIS_MixinContainer::UpdateDataAt(const int32 Idx, const FInstancedStruct& Data)
{
if (Idx == INDEX_NONE || !Mixins.IsValidIndex(Idx))
{
return INDEX_NONE;
}
FGIS_Mixin& Entry = Mixins[Idx];
if (!CheckCompatibility(Entry.Target, Data))
{
return INDEX_NONE;
}
Entry.Data = Data;
Entry.Timestamp = OwningObject->GetWorld()->GetTimeSeconds();
if (IGIS_MixinOwnerInterface* MixinOwner = Cast<IGIS_MixinOwnerInterface>(OwningObject))
{
MixinOwner->OnMixinDataUpdated(Entry.Target, Entry.Data);
}
MarkItemDirty(Entry);
CacheMixins();
return Idx;
}
void FGIS_MixinContainer::RemoveDataByTargetClass(const TSubclassOf<UObject>& TargetClass)
{
const int32 Idx = AcceleratedMap.Contains(TargetClass) ? AcceleratedMap[TargetClass] : INDEX_NONE;
if (Idx != INDEX_NONE)
{
const FGIS_Mixin& Entry = Mixins[Idx];
if (IGIS_MixinOwnerInterface* MixinOwner = Cast<IGIS_MixinOwnerInterface>(OwningObject))
{
MixinOwner->OnMixinDataRemoved(Entry.Target, Entry.Data);
}
Mixins.RemoveAt(Idx);
MarkArrayDirty();
}
CacheMixins();
}
bool FGIS_MixinContainer::CheckCompatibility(const UObject* Target, const FInstancedStruct& Data) const
{
if (!IsValid(Target) || !Data.IsValid())
{
return false;
}
if (const IGIS_MixinTargetInterface* MixinTarget = Cast<IGIS_MixinTargetInterface>(Target))
{
return MixinTarget->GetCompatibleMixinDataType() == Data.GetScriptStruct();
}
return false;
}
TArray<FInstancedStruct> FGIS_MixinContainer::GetAllData() const
{
TArray<FInstancedStruct> AllData;
AllData.Reserve(Mixins.Num());
for (const FGIS_Mixin& Mixin : Mixins)
{
AllData.Add(Mixin.Data);
}
return AllData;
}
TArray<FGIS_Mixin> FGIS_MixinContainer::GetSerializableMixins() const
{
return Mixins.FilterByPredicate([this](const FGIS_Mixin& Mixin)
{
if (const IGIS_MixinTargetInterface* MixinTarget = Cast<IGIS_MixinTargetInterface>(Mixin.Target))
{
return MixinTarget->IsMixinDataSerializable() && Mixin.Data.IsValid() && MixinTarget->GetCompatibleMixinDataType() == Mixin.Data.GetScriptStruct() && IsObjectLoadedFromDisk(Mixin.Target);
}
return false;
});
}
TArray<FGIS_MixinRecord> FGIS_MixinContainer::GetSerializableMixinRecords() const
{
TArray<FGIS_MixinRecord> Records;
TArray<FGIS_Mixin> FilteredMixins = GetSerializableMixins();
for (const FGIS_Mixin& FilteredMixin : FilteredMixins)
{
FGIS_MixinRecord Record;
const FSoftObjectPath AssetPath = FSoftObjectPath(FilteredMixin.Target);
Record.TargetPath = AssetPath.ToString();
Record.Data = FilteredMixin.Data;
Records.Add(Record);
}
return Records;
}
void FGIS_MixinContainer::RestoreFromRecords(const TArray<FGIS_MixinRecord>& Records)
{
TArray<FGIS_Mixin> ConvertedMixins = ConvertRecordsToMixins(Records);
for (const FGIS_Mixin& ConvertedMixin : ConvertedMixins)
{
SetDataForTarget(ConvertedMixin.Target, ConvertedMixin.Data);
}
}
TArray<FGIS_Mixin> FGIS_MixinContainer::ConvertRecordsToMixins(const TArray<FGIS_MixinRecord>& Records)
{
TArray<FGIS_Mixin> Ret;
for (const FGIS_MixinRecord& Record : Records)
{
if (!Record.IsValid())
{
continue;
}
const FSoftObjectPath TargetPath = FSoftObjectPath(Record.TargetPath);
const TSoftObjectPtr<UObject> TargetObjectSoftPtr = TSoftObjectPtr<UObject>(TargetPath);
const TObjectPtr<const UObject> TargetObject = !TargetObjectSoftPtr.IsNull() ? TargetObjectSoftPtr.LoadSynchronous() : nullptr;
if (!IsValid(TargetObject))
{
continue;
}
const IGIS_MixinTargetInterface* TargetInterface = Cast<IGIS_MixinTargetInterface>(TargetObject);
if (TargetInterface == nullptr)
{
continue;
}
if (!TargetInterface->IsMixinDataSerializable())
{
GIS_LOG(Warning, "Skip restoring mixin's data, as target(%s,class:%s) existed in record no longer considered serializable!",
*GetNameSafe(TargetObject), *GetNameSafe(TargetObject->GetClass()));
continue;
}
if (TargetInterface->GetCompatibleMixinDataType() != Record.Data.GetScriptStruct())
{
GIS_LOG(Warning,
"Skip restoring mixin's data, as target(%s,class:%s)'s data type(%s) in record no longer compatible with the new type(%s).",
*GetNameSafe(TargetObject), *GetNameSafe(TargetObject->GetClass()),
*GetNameSafe(Record.Data.GetScriptStruct()), *GetNameSafe(TargetInterface->GetCompatibleMixinDataType()));
continue;
}
FGIS_Mixin Mixin;
Mixin.Target = TargetObject;
Mixin.Data = Record.Data;
Ret.Add(Mixin);
}
return Ret;
}
TArray<FInstancedStruct> FGIS_MixinContainer::GetAllSerializableData() const
{
TArray<FInstancedStruct> AllData;
AllData.Reserve(Mixins.Num());
for (const FGIS_Mixin& Mixin : Mixins)
{
if (const IGIS_MixinTargetInterface* MixinTarget = Cast<IGIS_MixinTargetInterface>(Mixin.Target))
{
if (MixinTarget->IsMixinDataSerializable())
{
AllData.Add(Mixin.Data);
}
}
}
return AllData;
}
void FGIS_MixinContainer::PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize)
{
for (const int32 Index : AddedIndices)
{
FGIS_Mixin& Mixin = Mixins[Index];
if (Mixin.Timestamp != Mixin.LastReplicatedTimestamp)
{
Mixin.LastReplicatedTimestamp = Mixin.Timestamp;
if (IGIS_MixinOwnerInterface* MixinOwner = Cast<IGIS_MixinOwnerInterface>(OwningObject))
{
MixinOwner->OnMixinDataAdded(Mixin.Target, Mixin.Data);
}
}
}
CacheMixins();
}
void FGIS_MixinContainer::PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize)
{
for (const int32 Index : ChangedIndices)
{
FGIS_Mixin& Mixin = Mixins[Index];
if (Mixin.Timestamp != Mixin.LastReplicatedTimestamp)
{
Mixin.LastReplicatedTimestamp = Mixin.Timestamp;
if (IGIS_MixinOwnerInterface* MixinOwner = Cast<IGIS_MixinOwnerInterface>(OwningObject))
{
MixinOwner->OnMixinDataUpdated(Mixin.Target, Mixin.Data);
}
}
}
CacheMixins();
}
void FGIS_MixinContainer::PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize)
{
for (const int32 Index : RemovedIndices)
{
const FGIS_Mixin& Mixin = Mixins[Index];
if (IGIS_MixinOwnerInterface* MixinOwner = Cast<IGIS_MixinOwnerInterface>(OwningObject))
{
MixinOwner->OnMixinDataRemoved(Mixin.Target, Mixin.Data);
}
}
CacheMixins();
}
void FGIS_MixinContainer::CacheMixins()
{
const uint32 MixinsHash = GetTypeHash(Mixins);
if (MixinsHash == LastCachedHash)
{
// Same hash, no need to cache things again.
return;
}
const int32 Size = Mixins.Num();
AcceleratedMap.Empty(Size);
for (int Idx = 0; Idx < Size; ++Idx)
{
const UObject* Target = Mixins[Idx].Target;
AcceleratedMap.Add(Target, Idx);
}
LastCachedHash = GetTypeHash(Mixins);
}
bool FGIS_MixinContainer::NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParams)
{
return FastArrayDeltaSerialize<FGIS_Mixin, FGIS_MixinContainer>(Mixins, DeltaParams, *this);
}

View File

@@ -0,0 +1,7 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_MixinOwnerInterface.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_MixinOwnerInterface)
// Add default functionality here for any IGIS_MixinOwnerInterface functions that are not pure virtual.

View File

@@ -0,0 +1,7 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_MixinTargetInterface.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_MixinTargetInterface)
// Add default functionality here for any IGIS_MixinTargetInterface functions that are not pure virtual.

View File

@@ -0,0 +1,139 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Items/GIS_ItemDefinition.h"
#include "Items/GIS_ItemInstance.h"
#include "Engine/Texture2D.h"
#include "Items/GIS_ItemDefinitionSchema.h"
#include "Fragments/GIS_ItemFragment.h"
#include "Misc/DataValidation.h"
#include "UObject/ObjectSaveContext.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemDefinition)
UGIS_ItemDefinition::UGIS_ItemDefinition(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
bUnique = false;
}
const UGIS_ItemFragment* UGIS_ItemDefinition::GetFragment(TSubclassOf<UGIS_ItemFragment> FragmentClass) const
{
if (FragmentClass != nullptr)
{
for (UGIS_ItemFragment* Fragment : Fragments)
{
if (Fragment && Fragment->IsA(FragmentClass))
{
return Fragment;
}
}
}
return nullptr;
}
bool UGIS_ItemDefinition::HasFloatAttribute(FGameplayTag AttributeTag) const
{
return FloatAttributeMap.Contains(AttributeTag);
}
bool UGIS_ItemDefinition::HasIntegerAttribute(FGameplayTag AttributeTag) const
{
return IntegerAttributeMap.Contains(AttributeTag);
}
float UGIS_ItemDefinition::GetFloatAttribute(FGameplayTag AttributeTag) const
{
return FloatAttributeMap.Contains(AttributeTag) ? FloatAttributeMap[AttributeTag] : 0;
}
int32 UGIS_ItemDefinition::GetIntegerAttribute(FGameplayTag AttributeTag) const
{
return IntegerAttributeMap.Contains(AttributeTag) ? IntegerAttributeMap[AttributeTag] : 0;
}
const UGIS_ItemFragment* UGIS_ItemDefinition::FindFragmentOfItemDefinition(TSoftObjectPtr<UGIS_ItemDefinition> ItemDefinition, TSubclassOf<UGIS_ItemFragment> FragmentClass)
{
if (!ItemDefinition.IsNull())
{
const UGIS_ItemDefinition* LoadedDefinition = ItemDefinition.LoadSynchronous();
if (LoadedDefinition != nullptr)
{
return LoadedDefinition->GetFragment(FragmentClass);
}
}
return nullptr;
}
#if WITH_EDITOR
void UGIS_ItemDefinition::PreSave(FObjectPreSaveContext SaveContext)
{
FloatAttributeMap.Empty();
for (const FGIS_GameplayTagFloat& Attribute : StaticFloatAttributes)
{
FloatAttributeMap.Add(Attribute.Tag, Attribute.Value);
}
IntegerAttributeMap.Empty();
for (const FGIS_GameplayTagInteger& Attribute : StaticIntegerAttributes)
{
IntegerAttributeMap.Add(Attribute.Tag, Attribute.Value);
}
FText SchemaError;
UGIS_ItemDefinitionSchema::TryPreSaveItemDefinition(this, SchemaError);
if (!SchemaError.IsEmpty())
{
UE_LOG(LogTemp, Warning, TEXT("ItemDefinition PreSave validation warning for %s: %s"), *GetPathName(), *SchemaError.ToString());
}
Super::PreSave(SaveContext);
}
EDataValidationResult UGIS_ItemDefinition::IsDataValid(class FDataValidationContext& Context) const
{
if (ItemTags.IsEmpty())
{
Context.AddWarning(FText::FromString(FString::Format(TEXT("Item tags should not be empty for %s, or it can't be queried by external system."), {GetPathName()})));
}
// Check for each fragment's data validation.
for (int32 i = 0; i < Fragments.Num(); i++)
{
const UGIS_ItemFragment* Fragment = Fragments[i];
if (Fragment != nullptr)
{
FText Message;
if (!Fragment->FragmentDataValidation(Message))
{
Context.AddWarning(FText::FromString(FString::Format(TEXT("DataValidation failed for fragment at index:{0},message:{1}"), {i, Message.ToString()})));
return EDataValidationResult::Invalid;
}
}
}
// Check for duplicate fragment types
TSet<TSubclassOf<UGIS_ItemFragment>> FragmentClassSet;
for (const UGIS_ItemFragment* Fragment : Fragments)
{
if (Fragment != nullptr)
{
TSubclassOf<UGIS_ItemFragment> FragmentClass = Fragment->GetClass();
if (FragmentClassSet.Contains(FragmentClass))
{
Context.AddError(FText::FromString(FString::Format(TEXT("Duplicate fragment type {0} found in Fragments array for %s."), {FragmentClass->GetName(), GetPathName()})));
return EDataValidationResult::Invalid;
}
FragmentClassSet.Add(FragmentClass);
}
}
FText SchemaError;
if (!UGIS_ItemDefinitionSchema::TryValidateItemDefinition(this, SchemaError))
{
Context.AddError(SchemaError);
return EDataValidationResult::Invalid;
}
return EDataValidationResult::Valid;
}
#endif

View File

@@ -0,0 +1,576 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Items/GIS_ItemDefinitionSchema.h"
#include "GIS_InventorySystemSettings.h"
#include "GIS_LogChannels.h"
#include "Items/GIS_ItemDefinition.h"
#include "Fragments/GIS_ItemFragment.h"
#include "Misc/DataValidation.h"
#include "UObject/ObjectSaveContext.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemDefinitionSchema)
bool UGIS_ItemDefinitionSchema::TryValidateItemDefinition(const UGIS_ItemDefinition* Definition, FText& OutError)
{
if (Definition == nullptr)
{
OutError = FText::FromString(TEXT("Item definition is null."));
return false;
}
if (const UGIS_InventorySystemSettings* Settings = UGIS_InventorySystemSettings::Get())
{
FString AssetPath = Definition->GetPathName();
const UGIS_ItemDefinitionSchema* Schema = Settings->GetItemDefinitionSchemaForAsset(AssetPath);
if (Schema)
{
return Schema->TryValidate(Definition, OutError);
}
// OutError = FText::FromString(FString::Format(TEXT("No valid schema found for item definition at path: {0}."), {AssetPath}));
}
return true;
}
void UGIS_ItemDefinitionSchema::TryPreSaveItemDefinition(UGIS_ItemDefinition* Definition, FText& OutError)
{
if (Definition == nullptr)
{
OutError = FText::FromString(TEXT("Item definition is null."));
return;
}
if (const UGIS_InventorySystemSettings* Settings = UGIS_InventorySystemSettings::Get())
{
FString AssetPath = Definition->GetPathName();
const UGIS_ItemDefinitionSchema* Schema = Settings->GetItemDefinitionSchemaForAsset(AssetPath);
if (Schema)
{
Schema->TryPreSave(Definition, OutError);
}
// OutError = FText::FromString(FString::Format(TEXT("No valid schema found for item definition at path: {0}."), {AssetPath}));
}
}
bool UGIS_ItemDefinitionSchema::TryValidate(const UGIS_ItemDefinition* Definition, FText& OutError) const
{
if (Definition == nullptr)
{
OutError = FText::FromString(TEXT("Item definition is null."));
return false;
}
// Validate parent tag: all ItemTags must be children of RequiredParentTag
if (RequiredParentTag.IsValid())
{
for (const FGameplayTag& Tag : Definition->ItemTags.GetGameplayTagArray())
{
if (!Tag.MatchesTag(RequiredParentTag))
{
OutError = FText::FromString(FString::Format(TEXT("Tag {0} is not a child of required parent tag {1}."), {Tag.ToString(), RequiredParentTag.ToString()}));
return false;
}
}
}
FGIS_ItemDefinitionValidationEntry FoundEntry;
bool bFoundEntry = false;
int32 ValidationEntryIndex = -1;
// Find matching validation entry
for (int32 Index = 0; Index < ValidationEntries.Num(); ++Index)
{
const FGIS_ItemDefinitionValidationEntry& Entry = ValidationEntries[Index];
if (!Entry.ItemTagQuery.IsEmpty() && Definition->ItemTags.MatchesQuery(Entry.ItemTagQuery))
{
FoundEntry = Entry;
bFoundEntry = true;
ValidationEntryIndex = Index;
break;
}
}
if (!bFoundEntry)
{
return true; // No matching entry
}
TMap<TSubclassOf<UGIS_ItemFragment>, int32> FragmentClassMap;
TArray<TSubclassOf<UGIS_ItemFragment>> FragmentClasses;
// Build fragment class map and check for duplicate fragment types
for (const UGIS_ItemFragment* Fragment : Definition->Fragments)
{
if (Fragment != nullptr)
{
TSubclassOf<UGIS_ItemFragment> FragmentClass = Fragment->GetClass();
int32& Count = FragmentClassMap.FindOrAdd(FragmentClass);
Count++;
if (Count > 1)
{
OutError = FText::FromString(FString::Format(TEXT("Duplicate fragment type {0} found in Fragments array for schema {1}."), {FragmentClass->GetName(), GetPathName()}));
return false;
}
}
}
FragmentClassMap.GetKeys(FragmentClasses);
// Validate common required fragments
for (const TSoftClassPtr<UGIS_ItemFragment>& CommonFragment : CommonRequiredFragments)
{
if (UClass* LoadedClass = CommonFragment.LoadSynchronous())
{
if (!FragmentClasses.Contains(LoadedClass))
{
OutError = FText::FromString(
FString::Format(TEXT("Missing common required fragment of type {0} for ValidationEntry {1} in schema {2}."), {LoadedClass->GetName(), ValidationEntryIndex, GetPathName()}));
return false;
}
}
}
// Validate forbidden fragments, excluding common required fragments and required fragments
TSet<TSoftClassPtr<UGIS_ItemFragment>> EffectiveRequiredFragments;
for (const TSoftClassPtr<UGIS_ItemFragment>& RequiredFragment : FoundEntry.RequiredFragments)
{
if (!CommonRequiredFragments.Contains(RequiredFragment))
{
EffectiveRequiredFragments.Add(RequiredFragment);
}
}
for (const TSoftClassPtr<UGIS_ItemFragment>& ForbiddenFragment : FoundEntry.ForbiddenFragments)
{
if (UClass* LoadedClass = ForbiddenFragment.LoadSynchronous())
{
if (CommonRequiredFragments.Contains(ForbiddenFragment))
{
OutError = FText::FromString(FString::Format(
TEXT("Forbidden fragment {0} in ValidationEntry {1} is a common required fragment in schema {2}."), {LoadedClass->GetName(), ValidationEntryIndex, GetPathName()}));
return false;
}
if (EffectiveRequiredFragments.Contains(ForbiddenFragment))
{
OutError = FText::FromString(
FString::Format(TEXT("Forbidden fragment {0} in ValidationEntry {1} is a required fragment in schema {2}."), {LoadedClass->GetName(), ValidationEntryIndex, GetPathName()}));
return false;
}
if (FragmentClasses.Contains(LoadedClass))
{
OutError = FText::FromString(FString::Format(
TEXT("Forbidden fragment of type {0} found in definition for ValidationEntry {1} in schema {2}."), {LoadedClass->GetName(), ValidationEntryIndex, GetPathName()}));
return false;
}
}
}
// Validate required fragments, excluding those already in CommonRequiredFragments
for (const TSoftClassPtr<UGIS_ItemFragment>& RequiredFragment : EffectiveRequiredFragments)
{
if (UClass* LoadedClass = RequiredFragment.LoadSynchronous())
{
if (!FragmentClasses.Contains(LoadedClass))
{
OutError = FText::FromString(
FString::Format(TEXT("Missing required fragment of type {0} for ValidationEntry {1} in schema {2}."), {LoadedClass->GetName(), ValidationEntryIndex, GetPathName()}));
return false;
}
}
}
// Validate required float attributes
for (const FGIS_GameplayTagFloat& RequiredFloat : FoundEntry.RequiredFloatAttributes)
{
bool bFound = false;
for (const FGIS_GameplayTagFloat& FloatAttr : Definition->StaticFloatAttributes)
{
if (FloatAttr.Tag == RequiredFloat.Tag)
{
bFound = true;
break;
}
}
if (!bFound)
{
OutError = FText::FromString(
FString::Format(TEXT("Missing required float attribute {0} for ValidationEntry {1} in schema {2}."), {RequiredFloat.Tag.ToString(), ValidationEntryIndex, GetPathName()}));
return false;
}
}
// Validate required integer attributes
for (const FGIS_GameplayTagInteger& RequiredInteger : FoundEntry.RequiredIntegerAttributes)
{
bool bFound = false;
for (const FGIS_GameplayTagInteger& IntegerAttr : Definition->StaticIntegerAttributes)
{
if (IntegerAttr.Tag == RequiredInteger.Tag)
{
bFound = true;
break;
}
}
if (!bFound)
{
OutError = FText::FromString(
FString::Format(TEXT("Missing required integer attribute {0} for ValidationEntry {1} in schema {2}."), {RequiredInteger.Tag.ToString(), ValidationEntryIndex, GetPathName()}));
return false;
}
}
// Validate bUnique
if (FoundEntry.bEnforceUnique && Definition->bUnique != FoundEntry.RequiredUniqueValue)
{
OutError = FText::FromString(FString::Format(
TEXT("bUnique must be {0} for ValidationEntry {1} in schema {2}."), {FoundEntry.RequiredUniqueValue ? TEXT("true") : TEXT("false"), ValidationEntryIndex, GetPathName()}));
return false;
}
return true;
}
void UGIS_ItemDefinitionSchema::TryPreSave(UGIS_ItemDefinition* Definition, FText& OutError) const
{
if (Definition == nullptr)
{
OutError = FText::FromString(TEXT("Item definition is null."));
return;
}
FGIS_ItemDefinitionValidationEntry FoundEntry;
bool bFoundEntry = false;
// Find matching validation entry
for (const FGIS_ItemDefinitionValidationEntry& Entry : ValidationEntries)
{
if (!Entry.ItemTagQuery.IsEmpty() && Definition->ItemTags.MatchesQuery(Entry.ItemTagQuery))
{
FoundEntry = Entry;
bFoundEntry = true;
break;
}
}
// Collect required fragments
TSet<TSoftClassPtr<UGIS_ItemFragment>> RequiredFragmentSet;
for (const TSoftClassPtr<UGIS_ItemFragment>& CommonFragment : CommonRequiredFragments)
{
RequiredFragmentSet.Add(CommonFragment);
}
if (bFoundEntry)
{
for (const TSoftClassPtr<UGIS_ItemFragment>& RequiredFragment : FoundEntry.RequiredFragments)
{
if (!CommonRequiredFragments.Contains(RequiredFragment))
{
RequiredFragmentSet.Add(RequiredFragment);
}
}
}
// Collect forbidden fragments
TSet<TSoftClassPtr<UGIS_ItemFragment>> ForbiddenFragmentSet;
if (bFoundEntry)
{
for (const TSoftClassPtr<UGIS_ItemFragment>& ForbiddenFragment : FoundEntry.ForbiddenFragments)
{
if (!CommonRequiredFragments.Contains(ForbiddenFragment) && !RequiredFragmentSet.Contains(ForbiddenFragment))
{
ForbiddenFragmentSet.Add(ForbiddenFragment);
}
}
}
// Build map of existing fragments, keeping only the first instance of each type
TMap<TSubclassOf<UGIS_ItemFragment>, TObjectPtr<UGIS_ItemFragment>> ExistingFragmentMap;
for (TObjectPtr<UGIS_ItemFragment> Fragment : Definition->Fragments)
{
if (Fragment != nullptr)
{
TSubclassOf<UGIS_ItemFragment> FragmentClass = Fragment->GetClass();
if (!ExistingFragmentMap.Contains(FragmentClass))
{
ExistingFragmentMap.Add(FragmentClass, Fragment);
}
else
{
UE_LOG(LogGIS, Warning, TEXT("Removed duplicate fragment of type %s from ItemDefinition(%s) by schema(%s)."), *FragmentClass->GetName(), *GetNameSafe(Definition), *GetPathName());
}
}
}
// Remove forbidden fragments
for (const TSoftClassPtr<UGIS_ItemFragment>& ForbiddenFragment : ForbiddenFragmentSet)
{
if (UClass* ForbiddenClass = ForbiddenFragment.LoadSynchronous())
{
if (ExistingFragmentMap.Remove(ForbiddenClass))
{
UE_LOG(LogGIS, Warning, TEXT("Removed forbidden fragment of type %s from ItemDefinition(%s) by schema(%s)."), *ForbiddenClass->GetName(), *GetNameSafe(Definition), *GetPathName());
}
}
}
// Add missing required fragments
for (const TSoftClassPtr<UGIS_ItemFragment>& RequiredFragment : RequiredFragmentSet)
{
if (UClass* FragmentClass = RequiredFragment.LoadSynchronous())
{
if (!ExistingFragmentMap.Contains(FragmentClass))
{
UGIS_ItemFragment* NewFragment = NewObject<UGIS_ItemFragment>(Definition, FragmentClass);
if (NewFragment)
{
ExistingFragmentMap.Add(FragmentClass, NewFragment);
UE_LOG(LogGIS, Warning, TEXT("Added missing required fragment of type %s to ItemDefinition(%s) by schema(%s)."), *FragmentClass->GetName(), *GetNameSafe(Definition),
*GetPathName());
}
}
}
}
// Sort fragments according to FragmentOrder
TArray<TObjectPtr<UGIS_ItemFragment>> NewFragments;
TArray<TObjectPtr<UGIS_ItemFragment>> NonRequiredFragments;
// Cache loaded fragment classes to avoid repeated LoadSynchronous calls
TMap<TSoftClassPtr<UGIS_ItemFragment>, UClass*> FragmentClassCache;
for (const TSoftClassPtr<UGIS_ItemFragment>& OrderedFragment : FragmentOrder)
{
UClass* FragmentClass = FragmentClassCache.FindOrAdd(OrderedFragment, OrderedFragment.LoadSynchronous());
if (FragmentClass)
{
if (TObjectPtr<UGIS_ItemFragment>* ExistingFragment = ExistingFragmentMap.Find(FragmentClass))
{
NewFragments.Add(*ExistingFragment);
ExistingFragmentMap.Remove(FragmentClass);
}
}
}
// Append remaining non-required fragments
ExistingFragmentMap.GenerateValueArray(NonRequiredFragments);
for (const TObjectPtr<UGIS_ItemFragment>& Fragment : NonRequiredFragments)
{
if (Fragment && !FragmentOrder.Contains(Fragment->GetClass()))
{
UE_LOG(LogGIS, Warning, TEXT("Fragment type %s is not included in FragmentOrder, appended to end of fragments with ItemDefinition(%s) by schema(%s)."), *Fragment->GetClass()->GetName(),
*GetNameSafe(Definition), *GetPathName());
}
}
NewFragments.Append(NonRequiredFragments);
// Update the definition's fragment array
Definition->Fragments = MoveTemp(NewFragments);
// Auto-fix required attributes and bUnique if a matching entry was found
if (bFoundEntry)
{
// Auto-fix float attributes
for (const FGIS_GameplayTagFloat& RequiredFloat : FoundEntry.RequiredFloatAttributes)
{
bool bFound = false;
for (FGIS_GameplayTagFloat& FloatAttr : Definition->StaticFloatAttributes)
{
if (FloatAttr.Tag == RequiredFloat.Tag)
{
bFound = true;
break;
}
}
if (!bFound)
{
Definition->StaticFloatAttributes.Add(RequiredFloat);
UE_LOG(LogGIS, Warning, TEXT("Added missing required float attribute %s to ItemDefinition(%s) by schema(%s)."), *RequiredFloat.Tag.ToString(), *GetNameSafe(Definition),
*GetPathName());
}
}
// Auto-fix integer attributes
for (const FGIS_GameplayTagInteger& RequiredInteger : FoundEntry.RequiredIntegerAttributes)
{
bool bFound = false;
for (FGIS_GameplayTagInteger& IntegerAttr : Definition->StaticIntegerAttributes)
{
if (IntegerAttr.Tag == RequiredInteger.Tag)
{
bFound = true;
break;
}
}
if (!bFound)
{
Definition->StaticIntegerAttributes.Add(RequiredInteger);
UE_LOG(LogGIS, Warning, TEXT("Added missing required integer attribute %s to ItemDefinition(%s) by schema(%s)."), *RequiredInteger.Tag.ToString(), *GetNameSafe(Definition),
*GetPathName());
}
}
// Auto-fix bUnique
if (FoundEntry.bEnforceUnique)
{
if (Definition->bUnique != FoundEntry.RequiredUniqueValue)
{
Definition->bUnique = FoundEntry.RequiredUniqueValue;
UE_LOG(LogGIS, Warning, TEXT("Set bUnique to %s for ItemDefinition(%s) by schema(%s)."), FoundEntry.RequiredUniqueValue ? TEXT("true") : TEXT("false"), *GetNameSafe(Definition),
*GetPathName());
}
}
}
// Mark the definition as modified
Definition->Modify();
}
#if WITH_EDITOR
void UGIS_ItemDefinitionSchema::PreSave(FObjectPreSaveContext SaveContext)
{
// Remove RequiredFragments and ForbiddenFragments that overlap with CommonRequiredFragments
for (int32 EntryIndex = 0; EntryIndex < ValidationEntries.Num(); ++EntryIndex)
{
FGIS_ItemDefinitionValidationEntry& Entry = ValidationEntries[EntryIndex];
int32 InitialRequiredCount = Entry.RequiredFragments.Num();
int32 InitialForbiddenCount = Entry.ForbiddenFragments.Num();
Entry.RequiredFragments.RemoveAll([&](const TSoftClassPtr<UGIS_ItemFragment>& Fragment)
{
if (CommonRequiredFragments.Contains(Fragment))
{
if (UClass* LoadedClass = Fragment.LoadSynchronous())
{
UE_LOG(LogGIS, Warning, TEXT("Removed fragment %s from ValidationEntry %d RequiredFragments as it is already in CommonRequiredFragments for schema(%s)."), *LoadedClass->GetName(),
EntryIndex, *GetPathName());
}
return true;
}
return false;
});
Entry.ForbiddenFragments.RemoveAll([&](const TSoftClassPtr<UGIS_ItemFragment>& Fragment)
{
if (CommonRequiredFragments.Contains(Fragment))
{
if (UClass* LoadedClass = Fragment.LoadSynchronous())
{
UE_LOG(LogGIS, Warning, TEXT("Removed fragment %s from ValidationEntry %d ForbiddenFragments as it is in CommonRequiredFragments for schema(%s)."), *LoadedClass->GetName(),
EntryIndex, *GetPathName());
}
return true;
}
return false;
});
if (Entry.RequiredFragments.Num() < InitialRequiredCount)
{
UE_LOG(LogGIS, Warning, TEXT("Removed %d fragment(s) from ValidationEntry %d RequiredFragments for schema(%s)."), InitialRequiredCount - Entry.RequiredFragments.Num(), EntryIndex,
*GetPathName());
}
if (Entry.ForbiddenFragments.Num() < InitialForbiddenCount)
{
UE_LOG(LogGIS, Warning, TEXT("Removed %d fragment(s) from ValidationEntry %d ForbiddenFragments for schema(%s)."), InitialForbiddenCount - Entry.ForbiddenFragments.Num(), EntryIndex,
*GetPathName());
}
}
// Mark the schema as modified if changes were made
Modify();
Super::PreSave(SaveContext);
}
EDataValidationResult UGIS_ItemDefinitionSchema::IsDataValid(class FDataValidationContext& Context) const
{
// Validate that RequiredFragments do not overlap with CommonRequiredFragments
for (int32 EntryIndex = 0; EntryIndex < ValidationEntries.Num(); ++EntryIndex)
{
const FGIS_ItemDefinitionValidationEntry& Entry = ValidationEntries[EntryIndex];
for (const TSoftClassPtr<UGIS_ItemFragment>& RequiredFragment : Entry.RequiredFragments)
{
if (CommonRequiredFragments.Contains(RequiredFragment))
{
if (UClass* LoadedClass = RequiredFragment.LoadSynchronous())
{
Context.AddWarning(FText::FromString(
FString::Format(
TEXT("ValidationEntry {0} contains fragment {1} in RequiredFragments, which is already in CommonRequiredFragments for schema {2}."),
{EntryIndex, LoadedClass->GetName(), GetPathName()})));
}
}
}
// Validate that ForbiddenFragments do not overlap with CommonRequiredFragments or RequiredFragments
TSet<TSoftClassPtr<UGIS_ItemFragment>> EffectiveRequiredFragments;
for (const TSoftClassPtr<UGIS_ItemFragment>& RequiredFragment : Entry.RequiredFragments)
{
if (!CommonRequiredFragments.Contains(RequiredFragment))
{
EffectiveRequiredFragments.Add(RequiredFragment);
}
}
for (const TSoftClassPtr<UGIS_ItemFragment>& ForbiddenFragment : Entry.ForbiddenFragments)
{
if (CommonRequiredFragments.Contains(ForbiddenFragment))
{
if (UClass* LoadedClass = ForbiddenFragment.LoadSynchronous())
{
Context.AddError(FText::FromString(
FString::Format(
TEXT("ValidationEntry {0} contains fragment {1} in ForbiddenFragments, which is in CommonRequiredFragments for schema {2}."),
{EntryIndex, LoadedClass->GetName(), GetPathName()})));
return EDataValidationResult::Invalid;
}
}
if (EffectiveRequiredFragments.Contains(ForbiddenFragment))
{
if (UClass* LoadedClass = ForbiddenFragment.LoadSynchronous())
{
Context.AddError(FText::FromString(
FString::Format(
TEXT("ValidationEntry {0} contains fragment {1} in ForbiddenFragments, which is in RequiredFragments for schema {2}."),
{EntryIndex, LoadedClass->GetName(), GetPathName()})));
return EDataValidationResult::Invalid;
}
}
}
}
// Validate FragmentOrder contains no duplicates
TSet<TSoftClassPtr<UGIS_ItemFragment>> FragmentOrderSet;
for (const TSoftClassPtr<UGIS_ItemFragment>& Fragment : FragmentOrder)
{
if (FragmentOrderSet.Contains(Fragment))
{
if (UClass* LoadedClass = Fragment.LoadSynchronous())
{
Context.AddError(FText::FromString(FString::Format(TEXT("FragmentOrder contains duplicate fragment {0} for schema {1}."), {LoadedClass->GetName(), GetPathName()})));
return EDataValidationResult::Invalid;
}
}
FragmentOrderSet.Add(Fragment);
}
// Suggest including all CommonRequiredFragments and RequiredFragments in FragmentOrder
TSet<TSoftClassPtr<UGIS_ItemFragment>> AllRequiredFragments = TSet<TSoftClassPtr<UGIS_ItemFragment>>(CommonRequiredFragments);
for (const FGIS_ItemDefinitionValidationEntry& Entry : ValidationEntries)
{
for (const TSoftClassPtr<UGIS_ItemFragment>& RequiredFragment : Entry.RequiredFragments)
{
if (!CommonRequiredFragments.Contains(RequiredFragment))
{
AllRequiredFragments.Add(RequiredFragment);
}
}
}
for (const TSoftClassPtr<UGIS_ItemFragment>& RequiredFragment : AllRequiredFragments)
{
if (!FragmentOrder.Contains(RequiredFragment))
{
if (UClass* LoadedClass = RequiredFragment.LoadSynchronous())
{
Context.AddWarning(FText::FromString(FString::Format(TEXT("Required fragment {0} is not included in FragmentOrder for schema {1}."), {LoadedClass->GetName(), GetPathName()})));
}
}
}
return Context.GetNumErrors() > 0 ? EDataValidationResult::Invalid : EDataValidationResult::Valid;
}
#endif

View File

@@ -0,0 +1,120 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Items/GIS_ItemInfo.h"
#include "Items/GIS_ItemInstance.h"
#include "GIS_ItemCollection.h"
#include "Items/GIS_ItemDefinition.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemInfo)
FGIS_ItemInfo FGIS_ItemInfo::None = FGIS_ItemInfo();
FGIS_ItemInfo::FGIS_ItemInfo()
{
Item = nullptr;
Amount = 0;
ItemCollection = nullptr;
StackId = FGIS_ItemStack::InvalidId;
CollectionTag = FGameplayTag::EmptyTag;
CollectionId = FGIS_ItemStack::InvalidId;
}
FGIS_ItemInfo::FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, UGIS_ItemCollection* InCollection)
{
Item = InItem;
Amount = InAmount;
ItemCollection = InCollection;
StackId = FGIS_ItemStack::InvalidId;
}
FGIS_ItemInfo::FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, UGIS_ItemCollection* InCollection, FGuid InStackId)
{
Item = InItem;
Amount = InAmount;
ItemCollection = InCollection;
CollectionId = InCollection->GetCollectionId();
StackId = InStackId;
}
FGIS_ItemInfo::FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount)
{
Item = InItem;
Amount = InAmount;
}
FGIS_ItemInfo::FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, FGuid InCollectionId)
{
Item = InItem;
Amount = InAmount;
CollectionId = InCollectionId;
}
FGIS_ItemInfo::FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, FGameplayTag InCollectionTag)
{
Item = InItem;
Amount = InAmount;
CollectionTag = InCollectionTag;
}
FGIS_ItemInfo::FGIS_ItemInfo(int32 InAmount, const FGIS_ItemInfo& OtherInfo)
{
Amount = InAmount;
Item = OtherInfo.Item;
ItemCollection = OtherInfo.ItemCollection;
StackId = OtherInfo.StackId;
CollectionId = OtherInfo.CollectionId;
CollectionTag = OtherInfo.CollectionTag;
}
FGIS_ItemInfo::FGIS_ItemInfo(int32 InAmount, int32 InIndex, const FGIS_ItemInfo& OtherInfo)
{
Amount = InAmount;
Index = InIndex;
Item = OtherInfo.Item;
ItemCollection = OtherInfo.ItemCollection;
StackId = OtherInfo.StackId;
CollectionId = OtherInfo.CollectionId;
CollectionTag = OtherInfo.CollectionTag;
}
FGIS_ItemInfo::FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, const FGIS_ItemInfo& OtherInfo)
{
Item = InItem;
Amount = InAmount;
ItemCollection = OtherInfo.ItemCollection;
StackId = OtherInfo.StackId;
CollectionId = OtherInfo.CollectionId;
}
FGIS_ItemInfo::FGIS_ItemInfo(const FGIS_ItemStack& ItemStack)
{
Item = ItemStack.Item;
ItemCollection = ItemStack.Collection;
Amount = ItemStack.Amount;
StackId = ItemStack.Id;
}
FString FGIS_ItemInfo::GetDebugString() const
{
if (!IsValid())
{
return TEXT("Invalid item info");
}
return FString::Format(TEXT("{0}({1})"), {Item->GetDefinition()->GetName(), Amount});
}
bool FGIS_ItemInfo::operator==(const FGIS_ItemInfo& Other) const
{
return Other.Amount == Amount && Other.Item == Item && Other.ItemCollection == ItemCollection;
}
bool FGIS_ItemInfo::IsValid() const
{
return Item != nullptr && Amount > 0;
}
UGIS_InventorySystemComponent* FGIS_ItemInfo::GetInventory() const
{
return ItemCollection != nullptr ? ItemCollection->GetOwningInventory() : nullptr;
}

View File

@@ -0,0 +1,422 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Items/GIS_ItemInstance.h"
#include "GIS_InventorySystemComponent.h"
#include "GIS_ItemCollection.h"
#include "Items/GIS_ItemDefinition.h"
#include "GIS_ItemFragment.h"
#include "Net/UnrealNetwork.h"
#include "Engine/BlueprintGeneratedClass.h"
#if UE_WITH_IRIS
#include "Iris/ReplicationSystem/ReplicationFragmentUtil.h"
#endif // UE_WITH_IRIS
#include "GIS_LogChannels.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemInstance)
UGIS_ItemInstance::UGIS_ItemInstance(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer), IntegerAttributes(this), FloatAttributes(this), FragmentStates(this)
{
OwningCollection = nullptr;
}
void UGIS_ItemInstance::GetOwnedGameplayTags(FGameplayTagContainer& TagContainer) const
{
TagContainer = GetItemTags();
}
void UGIS_ItemInstance::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// FastArray don't need push model.
DOREPLIFETIME(ThisClass, IntegerAttributes);
DOREPLIFETIME(ThisClass, FloatAttributes);
DOREPLIFETIME(ThisClass, FragmentStates);
FDoRepLifetimeParams SharedParams;
SharedParams.bIsPushBased = true;
DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, ItemId, SharedParams);
DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, Definition, SharedParams);
DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, OwningCollection, SharedParams);
SharedParams.Condition = COND_InitialOrOwner;
//fix: https://forums.unrealengine.com/t/subobject-replication-for-blueprint-child-class/106205/4
UBlueprintGeneratedClass* bpClass = Cast<UBlueprintGeneratedClass>(this->GetClass());
if (bpClass != nullptr)
{
bpClass->GetLifetimeBlueprintReplicationList(OutLifetimeProps);
}
}
FGuid UGIS_ItemInstance::GetItemId() const
{
return ItemId;
}
void UGIS_ItemInstance::SetItemId(FGuid NewId)
{
ItemId = NewId;
MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, ItemId, this);
}
bool UGIS_ItemInstance::IsUnique() const
{
return Definition->bUnique;
}
FText UGIS_ItemInstance::GetItemName() const
{
return Definition->DisplayName;
}
const UGIS_ItemDefinition* UGIS_ItemInstance::GetDefinition() const
{
return Definition;
}
void UGIS_ItemInstance::SetDefinition(const UGIS_ItemDefinition* NewDefinition)
{
Definition = NewDefinition;
MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, Definition, this);
}
FGameplayTagContainer UGIS_ItemInstance::GetItemTags() const
{
return Definition->ItemTags;
}
const UGIS_ItemFragment* UGIS_ItemInstance::GetFragment(TSubclassOf<UGIS_ItemFragment> FragmentClass) const
{
if (Definition != nullptr && FragmentClass != nullptr)
{
return Definition->GetFragment(FragmentClass);
}
return nullptr;
}
const UGIS_ItemFragment* UGIS_ItemInstance::FindFragment(TSubclassOf<UGIS_ItemFragment> FragmentClass, bool& bValid) const
{
bValid = false;
if (const UGIS_ItemFragment* Fragment = GetFragment(FragmentClass))
{
bValid = true;
return Fragment;
}
return nullptr;
}
bool UGIS_ItemInstance::HasAnyAttribute(FGameplayTag AttributeTag) const
{
return HasFloatAttribute(AttributeTag) || HasIntegerAttribute(AttributeTag); // || HasBoolAttribute(AttributeTag);
}
bool UGIS_ItemInstance::HasFloatAttribute(FGameplayTag AttributeTag) const
{
if (FloatAttributes.ContainsTag(AttributeTag))
{
return true;
}
return false;
}
FText UGIS_ItemInstance::GetItemDescription() const
{
return Definition->Description;
}
float UGIS_ItemInstance::GetFloatAttribute(FGameplayTag AttributeTag) const
{
if (FloatAttributes.ContainsTag(AttributeTag))
{
return FloatAttributes.GetValue(AttributeTag);
}
return 0;
}
int32 UGIS_ItemInstance::GetIntegerAttribute(FGameplayTag AttributeTag) const
{
if (IntegerAttributes.ContainsTag(AttributeTag))
{
return IntegerAttributes.GetValue(AttributeTag);
}
return 0;
}
void UGIS_ItemInstance::SetFloatAttribute(FGameplayTag AttributeTag, float NewValue)
{
FloatAttributes.SetItem(AttributeTag, NewValue);
}
void UGIS_ItemInstance::AddFloatAttribute(FGameplayTag AttributeTag, float Value)
{
FloatAttributes.AddItem(AttributeTag, Value);
}
void UGIS_ItemInstance::RemoveFloatAttribute(FGameplayTag AttributeTag, float Value)
{
FloatAttributes.RemoveItem(AttributeTag, Value);
}
bool UGIS_ItemInstance::HasIntegerAttribute(FGameplayTag AttributeTag) const
{
if (IntegerAttributes.ContainsTag(AttributeTag))
{
return true;
}
return false;
}
void UGIS_ItemInstance::SetIntegerAttribute(FGameplayTag AttributeTag, int32 NewValue)
{
IntegerAttributes.SetItem(AttributeTag, NewValue);
}
void UGIS_ItemInstance::AddIntegerAttribute(FGameplayTag AttributeTag, int32 NewValue)
{
IntegerAttributes.AddItem(AttributeTag, NewValue);
}
void UGIS_ItemInstance::RemoveIntegerAttribute(FGameplayTag AttributeTag, int32 Value)
{
IntegerAttributes.RemoveItem(AttributeTag, Value);
}
UGIS_ItemCollection* UGIS_ItemInstance::GetOwningCollection() const
{
return OwningCollection;
}
UGIS_InventorySystemComponent* UGIS_ItemInstance::GetOwningInventory() const
{
return IsValid(OwningCollection) ? OwningCollection->GetOwningInventory() : nullptr;
}
bool UGIS_ItemInstance::FindFragmentStateByClass(TSubclassOf<UGIS_ItemFragment> FragmentClass, FInstancedStruct& OutState) const
{
return FragmentStates.GetDataByTarget(GetFragment(FragmentClass), OutState);
}
void UGIS_ItemInstance::SetFragmentStateByClass(TSubclassOf<UGIS_ItemFragment> FragmentClass, const FInstancedStruct& NewState)
{
if (const UGIS_ItemFragment* Fragment = Definition->GetFragment(FragmentClass))
{
FragmentStates.SetDataForTarget(Fragment, NewState);
}
}
void UGIS_ItemInstance::AssignCollection(UGIS_ItemCollection* NewItemCollection)
{
if (NewItemCollection == nullptr || OwningCollection == NewItemCollection)
{
return;
}
if (OwningCollection != nullptr && OwningCollection->GetOwningInventory() != nullptr && OwningCollection != NewItemCollection)
{
GIS_CLOG(Error, "is unable to be added to a new item collection:%s when it is already a member of an existing item collection.", *OwningCollection->GetDebugString());
return;
}
OwningCollection = NewItemCollection;
GIS_CLOG(Verbose, "item(%s) assigned new collection(%s)", *GetNameSafe(this), *OwningCollection->GetDebugString());
MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, OwningCollection, this);
}
void UGIS_ItemInstance::UnassignCollection(UGIS_ItemCollection* ItemCollection)
{
if (OwningCollection != nullptr && ItemCollection != nullptr && OwningCollection != ItemCollection)
{
GIS_CLOG(Warning, "belong to %s, but trying to remove from %s.", *OwningCollection->GetDebugString(), *ItemCollection->GetDebugString());
return;
}
GIS_CLOG(Verbose, "item(%s) removed from collection(%s)", *GetNameSafe(this), *OwningCollection->GetDebugString());
OwningCollection = nullptr;
MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, OwningCollection, this);
}
// void UGIS_ItemInstance::ResetCollection()
// {
// OwningCollection = nullptr;
// MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, OwningCollection, this);
// }
bool UGIS_ItemInstance::IsItemValid() const
{
return ItemId.IsValid() && Definition != nullptr;
}
bool UGIS_ItemInstance::StackableEquivalentTo(const UGIS_ItemInstance* OtherItem) const
{
return AreStackableEquivalent(this, OtherItem);
}
bool UGIS_ItemInstance::SimilarTo(const UGIS_ItemInstance* OtherItem) const
{
return AreSimilar(this, OtherItem);
}
bool UGIS_ItemInstance::AreStackableEquivalent(const UGIS_ItemInstance* Lhs, const UGIS_ItemInstance* Rhs)
{
if (Lhs == nullptr || Rhs == nullptr)
{
return false;
}
// 有必要比较同一个指针吗?
if (Lhs == Rhs)
{
return true;
}
if (Lhs->GetClass() != Rhs->GetClass())
{
return false;
}
if (Lhs->GetDefinition() != nullptr && Rhs->GetDefinition() != nullptr)
{
if (Lhs->GetDefinition() != Rhs->GetDefinition())
{
return false;
}
}
if (Lhs->IsUnique())
{
return false;
}
return true;
//return AreValueEquivalent(Lhs, Rhs);
}
// bool UGIS_ItemInstance::AreValueEquivalent(const UGIS_ItemInstance* Lhs, const UGIS_ItemInstance* Rhs)
// {
// if (Lhs == nullptr || Rhs == nullptr)
// {
// return false;
// }
// if (Lhs == Rhs)
// {
// return true;
// }
// if (Lhs->GetClass() != Rhs->GetClass())
// {
// return false;
// }
// // if (Lhs->GetItemTags() != Rhs->GetItemTags())
// // return false;
// if (Lhs->GetDefinition() != Rhs->GetDefinition())
// {
// return false;
// }
// // if (!Lhs->GetDefinitionTag().IsValid() || !Rhs->GetDefinitionTag().IsValid())
// // {
// // return false;
// // }
// return true;
// }
bool UGIS_ItemInstance::AreSimilar(const UGIS_ItemInstance* Lhs, const UGIS_ItemInstance* Rhs)
{
if (Lhs == nullptr || Rhs == nullptr)
{
return false;
}
if (Lhs == Rhs)
{
return true;
}
if (Lhs->GetClass() != Rhs->GetClass())
{
return false;
}
if (Lhs->GetDefinition() != Rhs->GetDefinition())
{
return false;
}
if (Lhs->IsUnique())
{
return false;
}
if (Lhs->GetItemId() == Rhs->GetItemId())
{
return true;
}
return true;
}
void UGIS_ItemInstance::OnItemDuplicated(const UGIS_ItemInstance* SrcItem)
{
// FloatAttributes.ContainerOwner = this;
// IntegerAttributes.ContainerOwner = this;
}
const FGIS_MixinContainer& UGIS_ItemInstance::GetFragmentStates() const
{
return FragmentStates;
}
void UGIS_ItemInstance::OnMixinDataAdded(const TObjectPtr<const UObject>& Target, const FInstancedStruct& Data)
{
const UGIS_ItemFragment* Fragment = CastChecked<UGIS_ItemFragment>(Target);
OnFragmentStateAdded(Fragment, Data);
OnFragmentStateAddedEvent.Broadcast(Fragment, Data);
}
void UGIS_ItemInstance::OnMixinDataUpdated(const TObjectPtr<const UObject>& Target, const FInstancedStruct& Data)
{
const UGIS_ItemFragment* Fragment = CastChecked<UGIS_ItemFragment>(Target);
OnFragmentStateUpdated(Fragment, Data);
OnFragmentStateUpdatedEvent.Broadcast(Fragment, Data);
}
void UGIS_ItemInstance::OnMixinDataRemoved(const TObjectPtr<const UObject>& Target, const FInstancedStruct& Data)
{
const UGIS_ItemFragment* Fragment = CastChecked<UGIS_ItemFragment>(Target);
OnFragmentStateRemoved(Fragment, Data);
OnFragmentStateRemovedEvent.Broadcast(Fragment, Data);
}
void UGIS_ItemInstance::OnFragmentStateAdded(const UGIS_ItemFragment* Fragment, const FInstancedStruct& Data)
{
}
void UGIS_ItemInstance::OnFragmentStateUpdated(const UGIS_ItemFragment* Fragment, const FInstancedStruct& Data)
{
}
void UGIS_ItemInstance::OnFragmentStateRemoved(const UGIS_ItemFragment* Fragment, const FInstancedStruct& Data)
{
}
void UGIS_ItemInstance::OnTagFloatUpdate(const FGameplayTag& Tag, float OldValue, float NewValue)
{
OnFloatAttributeChanged(Tag, OldValue, NewValue);
OnFloatAttributeChangedEvent.Broadcast(Tag, OldValue, NewValue);
}
void UGIS_ItemInstance::OnTagIntegerUpdate(const FGameplayTag& Tag, int32 OldValue, int32 NewValue)
{
OnIntegerAttributeChanged(Tag, OldValue, NewValue);
OnIntegerAttributeChangedEvent.Broadcast(Tag, OldValue, NewValue);
}
void UGIS_ItemInstance::OnFloatAttributeChanged_Implementation(const FGameplayTag& Tag, float OldValue, float NewValue)
{
}
void UGIS_ItemInstance::OnIntegerAttributeChanged_Implementation(const FGameplayTag& Tag, int32 OldValue, int32 NewValue)
{
}
#if UE_WITH_IRIS
void UGIS_ItemInstance::RegisterReplicationFragments(UE::Net::FFragmentRegistrationContext& Context, UE::Net::EFragmentRegistrationFlags RegistrationFlags)
{
using namespace UE::Net;
UObject::RegisterReplicationFragments(Context, RegistrationFlags);
// Build descriptors and allocate PropertyReplicationFragments for this object
FReplicationFragmentUtil::CreateAndRegisterFragmentsForObject(this, Context, RegistrationFlags);
}
#endif

View File

@@ -0,0 +1,7 @@
// // Copyright 2025 https://yuewu.dev/en All Rights Reserved.
//
//
// #include "Items/GIS_ItemInterface.h"
//
// #include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemInterface)
//

View File

@@ -0,0 +1,195 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Items/GIS_ItemStack.h"
#include "Components/ActorComponent.h"
#include "GIS_InventoryMeesages.h"
#include "GIS_ItemCollection.h"
#include "Items/GIS_ItemDefinition.h"
#include "GIS_InventorySystemComponent.h"
#include "Items/GIS_ItemInstance.h"
#include "GIS_LogChannels.h"
#include "GIS_ItemSlotCollection.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemStack)
FGuid FGIS_ItemStack::InvalidId = FGuid(0, 0, 0, 0);
FGIS_ItemStack::FGIS_ItemStack()
{
Item = nullptr;
Id = InvalidId;
Amount = 0;
Collection = nullptr;
LastObservedAmount = -1;
}
FString FGIS_ItemStack::GetDebugString()
{
return FString::Format(TEXT("Item({0}),Amount({1}),ID({2})"), {Item && Item->GetDefinition() ? GetNameSafe(Item->GetDefinition()) : TEXT("None"), Amount, Id.ToString()});
}
void FGIS_ItemStack::Initialize(FGuid InStackId, UGIS_ItemInstance* InItem, int32 InAmount, UGIS_ItemCollection* InCollection, int32 InIndex)
{
Id = InStackId;
Item = InItem;
Amount = InAmount;
Collection = InCollection;
Index = InIndex;
LastObservedAmount = InAmount;
}
bool FGIS_ItemStack::IsValidStack() const
{
return Id.IsValid() && IsValid(Item) && IsValid(Collection) && Amount > 0;
}
void FGIS_ItemStack::Reset()
{
Id.Invalidate();
Item = nullptr;
Amount = 0;
Collection = nullptr;
LastObservedAmount = INDEX_NONE;
}
bool FGIS_ItemStack::operator==(const FGIS_ItemStack& Other) const
{
return Id == Other.Id && Item->GetItemId() == Other.Item->GetItemId();
}
bool FGIS_ItemStack::operator!=(const FGIS_ItemStack& Other) const
{
return !(operator==(Other));
}
bool FGIS_ItemStack::operator==(const FGuid& OtherId) const
{
return Id == OtherId;
}
bool FGIS_ItemStack::operator!=(const FGuid& OtherId) const
{
return Id != OtherId;
}
void FGIS_ItemStackContainer::PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize)
{
for (int32 Index : RemovedIndices)
{
FGIS_ItemStack& Stack = Stacks[Index];
if (OwningCollection->StackToIdxMap.Contains(Stack.Id))
{
OwningCollection->OnItemStackRemoved(Stack);
}
else if (OwningCollection->PendingItemStacks.Contains(Stack.Id))
{
GIS_OWNED_CLOG(OwningCollection, Warning, "Discard pending item stack(%s).", *OwningCollection->PendingItemStacks[Stack.Id].GetDebugString())
OwningCollection->PendingItemStacks.Remove(Stack.Id);
}
Stack.LastObservedAmount = 0;
}
}
void FGIS_ItemStackContainer::PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize)
{
for (int32 Index : AddedIndices)
{
FGIS_ItemStack& Stack = Stacks[Index];
if (OwningCollection && OwningCollection->IsInitialized() && Stack.IsValidStack())
{
OwningCollection->OnItemStackAdded(Stack);
}
else if (OwningCollection->PendingItemStacks.Contains(Stack.Id))
{
OwningCollection->PendingItemStacks[Stack.Id] = Stack;
}
else
{
OwningCollection->PendingItemStacks.Add(Stack.Id, Stack);
}
Stack.LastObservedAmount = Stack.Amount;
}
}
void FGIS_ItemStackContainer::PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize)
{
for (int32 Index : ChangedIndices)
{
FGIS_ItemStack& Stack = Stacks[Index];
check(Stack.LastObservedAmount != INDEX_NONE);
if (OwningCollection->StackToIdxMap.Contains(Stack.Id)) //Already Added.
{
OwningCollection->OnItemStackUpdated(Stack);
}
else if (OwningCollection->PendingItemStacks.Contains(Stack.Id)) //In pending list.
{
OwningCollection->PendingItemStacks.Emplace(Stack.Id, Stack); // Updated to pending.
}
else
{
OwningCollection->PendingItemStacks.Add(Stack.Id, Stack); //Add to pending list.
}
Stack.LastObservedAmount = Stack.Amount;
}
}
const FGIS_ItemStack* FGIS_ItemStackContainer::FindById(const FGuid& StackId) const
{
if (!StackId.IsValid())
{
return nullptr;
}
return Stacks.FindByPredicate([StackId](const FGIS_ItemStack& Stack)
{
return Stack.Id == StackId;
});
}
const FGIS_ItemStack* FGIS_ItemStackContainer::FindByItemId(const FGuid& ItemId) const
{
if (!ItemId.IsValid())
{
return nullptr;
}
return Stacks.FindByPredicate([ItemId](const FGIS_ItemStack& Stack)
{
return Stack.Item->GetItemId() == ItemId;
});
}
int32 FGIS_ItemStackContainer::IndexOfById(const FGuid& StackId) const
{
if (!StackId.IsValid())
{
return INDEX_NONE;
}
return Stacks.IndexOfByPredicate([StackId](const FGIS_ItemStack& Stack)
{
return Stack.Id == StackId;
});
}
int32 FGIS_ItemStackContainer::IndexOfByItemId(const FGuid& ItemId) const
{
return Stacks.IndexOfByPredicate([ItemId](const FGIS_ItemStack& Stack)
{
return Stack.Item->GetItemId() == ItemId;
});
}
int32 FGIS_ItemStackContainer::IndexOfByIds(const FGuid& StackId, const FGuid& ItemId) const
{
if (!StackId.IsValid() || !ItemId.IsValid())
{
return INDEX_NONE;
}
return Stacks.IndexOfByPredicate([StackId,ItemId](const FGIS_ItemStack& Stack)
{
return Stack.Id == StackId && Stack.Item->GetItemId() == ItemId;
});
}

View File

@@ -0,0 +1,6 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_CraftingStructLibrary.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CraftingStructLibrary)

View File

@@ -0,0 +1,252 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_CraftingSystemComponent.h"
#include "Engine/World.h"
#include "GIS_InventorySystemComponent.h"
#include "GIS_CurrencySystemComponent.h"
#include "GIS_InventoryFunctionLibrary.h"
#include "GIS_InventorySubsystem.h"
#include "GIS_ItemCollection.h"
#include "Items/GIS_ItemDefinition.h"
#include "GIS_ItemFragment_CraftingRecipe.h"
#include "Items/GIS_ItemInstance.h"
#include "Kismet/KismetMathLibrary.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CraftingSystemComponent)
// Sets default values for this component's properties
UGIS_CraftingSystemComponent::UGIS_CraftingSystemComponent()
{
// Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features
// off to improve performance if you don't need them.
PrimaryComponentTick.bCanEverTick = false;
NumOfSelectedItemsCache = 0;
// ...
}
bool UGIS_CraftingSystemComponent::Craft(const UGIS_ItemDefinition* Recipe, UGIS_InventorySystemComponent* CostInventory, int32 Quantity)
{
const UGIS_ItemFragment_CraftingRecipe* RecipeFragment = GetRecipeFragment(Recipe);
if (RecipeFragment == nullptr)
{
return false;
}
return CraftInternal(Recipe, CostInventory, Quantity);
}
bool UGIS_CraftingSystemComponent::CanCraft(const UGIS_ItemDefinition* RecipeDefinition, UGIS_InventorySystemComponent* Inventory, int32 Quantity)
{
return CanCraftInternal(RecipeDefinition, Inventory, Quantity);
}
bool UGIS_CraftingSystemComponent::RemoveItemIngredients(UGIS_InventorySystemComponent* Inventory, const TArray<FGIS_ItemInfo>& ItemIngredients)
{
for (int32 i = 0; i < ItemIngredients.Num(); i++)
{
FGIS_ItemInfo ItemInfoToRemove;
ItemInfoToRemove.Item = ItemIngredients[i].Item;
ItemInfoToRemove.Amount = ItemIngredients[i].Amount;
if (Inventory->GetDefaultCollection()->RemoveItem(ItemInfoToRemove).Amount == ItemInfoToRemove.Amount) { continue; }
return false;
}
return false;
}
bool UGIS_CraftingSystemComponent::IsValidRecipe_Implementation(const UGIS_ItemDefinition* RecipeDefinition) const
{
if (const UGIS_ItemFragment_CraftingRecipe* Fragment = GetRecipeFragment(RecipeDefinition))
{
return !Fragment->InputItems.IsEmpty() && !Fragment->OutputItems.IsEmpty();
}
return false;
}
bool UGIS_CraftingSystemComponent::CanCraftInternal(const UGIS_ItemDefinition* RecipeDefinition, UGIS_InventorySystemComponent* Inventory,
int32 Quantity)
{
if (!IsValidRecipe(RecipeDefinition))
{
return false;
}
const UGIS_ItemFragment_CraftingRecipe* RecipeFragment = GetRecipeFragment(RecipeDefinition);
check(RecipeFragment != nullptr);
if (!SelectItemForIngredients(Inventory, RecipeFragment->InputItems, Quantity))
{
return false;
}
TArray<FGIS_ItemInfo> Selected;
int32 SelectedCount = 0;
// Has enough items? 有足够的道具?
if (!CheckIfEnoughItemIngredients(RecipeFragment->InputItems, Quantity, SelectedItemsCache, Selected, SelectedCount))
{
return false;
}
UGIS_CurrencySystemComponent* CurrencySystem = Inventory->GetCurrencySystem();
if (CurrencySystem == nullptr)
{
return false;
}
// Has enough currencies? 有足够货币?
if (!CurrencySystem->HasCurrencies(UGIS_InventoryFunctionLibrary::MultiplyCurrencies(RecipeFragment->InputCurrencies, Quantity)))
{
return false;
}
return true;
}
bool UGIS_CraftingSystemComponent::CraftInternal(const UGIS_ItemDefinition* RecipeDefinition, UGIS_InventorySystemComponent* Inventory, int32 Quantity)
{
const UGIS_ItemFragment_CraftingRecipe* RecipeFragment = GetRecipeFragment(RecipeDefinition);
if (RecipeFragment == nullptr)
{
return false;
}
if (!SelectItemForIngredients(Inventory, RecipeFragment->InputItems, Quantity))
{
return false;
}
if (!CanCraftInternal(RecipeDefinition, Inventory, Quantity))
{
return false;
}
UGIS_CurrencySystemComponent* CurrencySystem = Inventory->GetCurrencySystem();
bool bCurrencyRemoveSuccess = CurrencySystem->RemoveCurrencies(UGIS_InventoryFunctionLibrary::MultiplyCurrencies(RecipeFragment->InputCurrencies, Quantity));
bool bItemRemoveSuccess = RemoveItemIngredients(Inventory, SelectedItemsCache);
if (bCurrencyRemoveSuccess && bItemRemoveSuccess)
{
ProduceCraftingOutput(RecipeDefinition, Inventory, Quantity);
return true;
}
return false;
}
void UGIS_CraftingSystemComponent::ProduceCraftingOutput(const UGIS_ItemDefinition* RecipeDefinition, UGIS_InventorySystemComponent* Inventory, int32 Quantity)
{
const UGIS_ItemFragment_CraftingRecipe* RecipeFragment = GetRecipeFragment(RecipeDefinition);
if (RecipeFragment == nullptr)
{
return;
}
TArray<FGIS_ItemDefinitionAmount> ItemAmounts = UGIS_InventoryFunctionLibrary::MultiplyItemAmounts(RecipeFragment->OutputItems, Quantity);
for (int32 i = 0; i < ItemAmounts.Num(); i++)
{
FGIS_ItemInfo ItemInfoToAdd;
ItemInfoToAdd.Item = UGIS_InventorySubsystem::Get(GetWorld())->CreateItem(Inventory->GetOwner(), ItemAmounts[i].Definition);
ItemInfoToAdd.Amount = ItemAmounts[i].Amount;
Inventory->AddItem(ItemInfoToAdd);
}
}
const UGIS_ItemFragment_CraftingRecipe* UGIS_CraftingSystemComponent::GetRecipeFragment(const UGIS_ItemDefinition* RecipeDefinition) const
{
return RecipeDefinition ? RecipeDefinition->FindFragment<UGIS_ItemFragment_CraftingRecipe>() : nullptr;
}
bool UGIS_CraftingSystemComponent::SelectItemForIngredients(const UGIS_InventorySystemComponent* Inventory, const TArray<FGIS_ItemDefinitionAmount>& ItemIngredients, int32 Quantity)
{
SelectedItemsCache.Empty();
NumOfSelectedItemsCache = 0;
for (int32 i = 0; i < ItemIngredients.Num(); i++)
{
auto RequiredItem = ItemIngredients[i];
int32 NeededAmount = RequiredItem.Amount * Quantity;
TArray<FGIS_ItemInfo> FilteredItemInfos;
if (Inventory->GetItemInfosByDefinition(RequiredItem.Definition, FilteredItemInfos))
{
for (int32 j = 0; j < FilteredItemInfos.Num(); j++)
{
const FGIS_ItemInfo& itemInfo = FilteredItemInfos[j];
int32 HaveAmount = itemInfo.Amount;
bool FoundSelectedItem = false;
for (int k = 0; k < NumOfSelectedItemsCache; k++)
{
const FGIS_ItemInfo& SelectedItemInfo = SelectedItemsCache[k];
if (itemInfo.Item != SelectedItemInfo.Item) { continue; }
if (itemInfo.StackId != SelectedItemInfo.StackId) { continue; }
FoundSelectedItem = true;
HaveAmount = FMath::Max(0, HaveAmount - SelectedItemInfo.Amount);
int32 additionalIgnoreAmount = FMath::Clamp(HaveAmount, 0, NeededAmount);
//更新已经选择道具信息。
SelectedItemsCache[k] = FGIS_ItemInfo(SelectedItemInfo.Amount + additionalIgnoreAmount, SelectedItemInfo);
}
int32 SelectedAmount = FMath::Clamp(HaveAmount, 0, NeededAmount);
//记录已选择道具信息。
if (FoundSelectedItem == false)
{
SelectedItemsCache.SetNum(NumOfSelectedItemsCache + 1);
SelectedItemsCache[NumOfSelectedItemsCache] = FGIS_ItemInfo(SelectedAmount, itemInfo);
NumOfSelectedItemsCache++;
}
NeededAmount -= SelectedAmount;
if (NeededAmount <= 0) { break; }
}
}
if (NeededAmount <= 0) { continue; }
return false;
}
return true;
}
bool UGIS_CraftingSystemComponent::CheckIfEnoughItemIngredients(const TArray<FGIS_ItemDefinitionAmount>& ItemIngredients, int32 Quantity, const TArray<FGIS_ItemInfo>& SelectedItems,
TArray<FGIS_ItemInfo>& ItemsToIgnore, int32& NumOfItemsToIgnore)
{
for (int32 i = 0; i < ItemIngredients.Num(); i++)
{
const FGIS_ItemDefinitionAmount& ingredientAmount = ItemIngredients[i];
int32 neededAmount = Quantity * ingredientAmount.Amount;
for (int32 j = 0; j < SelectedItems.Num(); j++)
{
const FGIS_ItemInfo& itemInfo = SelectedItems[j];
if (ingredientAmount.Definition != itemInfo.Item->GetDefinition()) { continue; }
int32 haveAmount = itemInfo.Amount;
bool foundMatch = false;
int32 selectedAmount = FMath::Clamp(haveAmount, 0, neededAmount);
if (foundMatch == false)
{
ItemsToIgnore.SetNum(NumOfItemsToIgnore + 1);
ItemsToIgnore[NumOfItemsToIgnore] = FGIS_ItemInfo(selectedAmount, itemInfo);
NumOfItemsToIgnore++;
}
neededAmount -= selectedAmount;
if (neededAmount <= 0) { break; }
}
if (neededAmount > 0)
{
return false;
}
}
return true;
}

View File

@@ -0,0 +1,20 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_ItemFragment_CraftingRecipe.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemFragment_CraftingRecipe)
#if WITH_EDITOR
#include "UObject/ObjectSaveContext.h"
void UGIS_ItemFragment_CraftingRecipe::PreSave(FObjectPreSaveContext SaveContext)
{
Super::PreSave(SaveContext);
for (FGIS_ItemDefinitionAmount& DefaultItem : OutputItems)
{
DefaultItem.EditorFriendlyName = FString::Format(TEXT("{0}x {1}"), {DefaultItem.Amount, DefaultItem.Definition.IsNull() ? TEXT("Invalid Item") : DefaultItem.Definition.GetAssetName()});
}
}
#endif

View File

@@ -0,0 +1,44 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Drops/GIS_CurrencyDropper.h"
#include "GIS_CurrencySystemComponent.h"
#include "GIS_LogChannels.h"
#include "Misc/DataValidation.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CurrencyDropper)
void UGIS_CurrencyDropper::BeginPlay()
{
MyCurrency = UGIS_CurrencySystemComponent::GetCurrencySystemComponent(GetOwner());
if (MyCurrency == nullptr)
{
GIS_CLOG(Warning, "Mising currency system component!");
}
Super::BeginPlay();
}
void UGIS_CurrencyDropper::Drop()
{
if (AActor* PickupActor = CreatePickupActorInstance())
{
if (UGIS_CurrencySystemComponent* CurrencySys = UGIS_CurrencySystemComponent::GetCurrencySystemComponent(PickupActor))
{
CurrencySys->SetCurrencies(MyCurrency->GetAllCurrencies());
}
}
}
#if WITH_EDITOR
EDataValidationResult UGIS_CurrencyDropper::IsDataValid(FDataValidationContext& Context) const
{
if (PickupActorClass.IsNull())
{
Context.AddError(FText::FromString(FString::Format(TEXT("%s has no pickup actor class.!"), {*GetName()})));
return EDataValidationResult::Invalid;
}
return Super::IsDataValid(Context);
}
#endif

View File

@@ -0,0 +1,78 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Drops/GIS_DropperComponent.h"
#include "Engine/World.h"
#include "GIS_LogChannels.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/Character.h"
#include "Kismet/KismetMathLibrary.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_DropperComponent)
UGIS_DropperComponent::UGIS_DropperComponent()
{
PrimaryComponentTick.bStartWithTickEnabled = false;
PrimaryComponentTick.bCanEverTick = false;
SetIsReplicatedByDefault(true);
}
void UGIS_DropperComponent::Drop()
{
}
AActor* UGIS_DropperComponent::CreatePickupActorInstance_Implementation()
{
UWorld* World = GetWorld();
check(World);
if (PickupActorClass.IsNull())
{
GIS_CLOG(Error, "missing PickupActorClass!");
return nullptr;
}
UClass* PickupClass = PickupActorClass.LoadSynchronous();
if (PickupClass == nullptr)
{
GIS_CLOG(Error, "failed to load PickupActorClass!");
return nullptr;
}
FVector Origin = CalcDropOrigin();
AActor* Pickup = World->SpawnActor<AActor>(PickupClass, FTransform(Origin + CalcDropOffset()));
if (Pickup == nullptr)
{
GIS_CLOG(Error, "failed to spawn pickup actor from PickupActorClass(%s)!", *PickupClass->GetName());
return nullptr;
}
return Pickup;
}
FVector UGIS_DropperComponent::CalcDropOrigin_Implementation() const
{
if (IsValid(DropTransform))
{
return DropTransform->GetActorLocation();
}
FVector OriginLocation = GetOwner()->GetActorLocation();
if (const ACharacter* Character = Cast<ACharacter>(GetOwner()))
{
OriginLocation.Z -= Character->GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
}
else if (const UCapsuleComponent* CapsuleComponent = Cast<UCapsuleComponent>(GetOwner()->GetRootComponent()))
{
OriginLocation.Z -= CapsuleComponent->GetScaledCapsuleHalfHeight();
}
return OriginLocation;
}
FVector UGIS_DropperComponent::CalcDropOffset_Implementation() const
{
const float RandomX = UKismetMathLibrary::RandomFloatInRange(-DropRadius, DropRadius);
const float RandomY = UKismetMathLibrary::RandomFloatInRange(-DropRadius, DropRadius);
return FVector(RandomX, RandomY, 0);
}

View File

@@ -0,0 +1,108 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Drops/GIS_ItemDropperComponent.h"
#include "GIS_InventoryTags.h"
#include "GameFramework/Actor.h"
#include "GIS_InventorySystemComponent.h"
#include "GIS_ItemCollection.h"
#include "GIS_LogChannels.h"
#include "Pickups/GIS_InventoryPickupComponent.h"
#include "Pickups/GIS_ItemPickupComponent.h"
#include "Pickups/GIS_WorldItemComponent.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemDropperComponent)
void UGIS_ItemDropperComponent::Drop()
{
TArray<FGIS_ItemInfo> ItemsToDrop = GetItemsToDrop();
DropItemsInternal(ItemsToDrop);
}
void UGIS_ItemDropperComponent::BeginPlay()
{
if (!CollectionTag.IsValid())
{
CollectionTag = GIS_CollectionTags::Main;
}
Super::BeginPlay();
}
TArray<FGIS_ItemInfo> UGIS_ItemDropperComponent::GetItemsToDrop() const
{
return GetItemsToDropInternal();
}
TArray<FGIS_ItemInfo> UGIS_ItemDropperComponent::GetItemsToDropInternal() const
{
TArray<FGIS_ItemInfo> Items;
UGIS_InventorySystemComponent* Inventory = UGIS_InventorySystemComponent::FindInventorySystemComponent(GetOwner());
if (Inventory == nullptr)
{
GIS_CLOG(Error, "requires inventory system component to drop items.")
return Items;
}
UGIS_ItemCollection* Collection = Inventory->GetCollectionByTag(CollectionTag);
if (Collection == nullptr)
{
GIS_CLOG(Error, " inventory missing collection with tag:%s'", *CollectionTag.ToString())
return Items;
}
Items = Collection->GetAllItemInfos();
return Items;
}
void UGIS_ItemDropperComponent::DropItemsInternal(const TArray<FGIS_ItemInfo>& ItemInfos)
{
if (bDropAsInventory)
{
DropInventoryPickup(ItemInfos);
}
else
{
for (int32 i = 0; i < ItemInfos.Num(); i++)
{
DropItemPickup(ItemInfos[i]);
}
}
}
void UGIS_ItemDropperComponent::DropInventoryPickup(const TArray<FGIS_ItemInfo>& ItemInfos)
{
if (AActor* PickupActor = CreatePickupActorInstance())
{
UGIS_InventorySystemComponent* Inventory = PickupActor->FindComponentByClass<UGIS_InventorySystemComponent>();
UGIS_InventoryPickupComponent* Pickup = PickupActor->FindComponentByClass<UGIS_InventoryPickupComponent>();
if (Inventory == nullptr || Pickup == nullptr)
{
GIS_CLOG(Error, "Spawned pickup(%s) missing either inventory component or inventory pickup component.", *PickupActor->GetName());
return;
}
UGIS_ItemCollection* Collection = Inventory->GetDefaultCollection();
if (Collection == nullptr)
{
GIS_CLOG(Error, "Spawned pickup(%s)'s inventory doesn't have default collection.", *PickupActor->GetName());
return;
}
Collection->RemoveAll();
Collection->AddItems(ItemInfos);
}
}
void UGIS_ItemDropperComponent::DropItemPickup(const FGIS_ItemInfo& ItemInfo)
{
if (AActor* Pickup = CreatePickupActorInstance())
{
UGIS_ItemPickupComponent* ItemPickup = Pickup->FindComponentByClass<UGIS_ItemPickupComponent>();
UGIS_WorldItemComponent* WorldItem = Pickup->FindComponentByClass<UGIS_WorldItemComponent>();
if (ItemPickup == nullptr || WorldItem == nullptr)
{
GIS_CLOG(Error, "Spawned pickup(%s) missing either ItemPickup component or WorldItem component.", *Pickup->GetName());
}
WorldItem->SetItemInfo(ItemInfo.Item, ItemInfo.Amount);
}
}

View File

@@ -0,0 +1,94 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Drops/GIS_RandomItemDropperComponent.h"
#include "GameFramework/Actor.h"
#include "GIS_InventorySystemComponent.h"
#include "GIS_ItemCollection.h"
#include "GIS_LogChannels.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_RandomItemDropperComponent)
TArray<FGIS_ItemInfo> UGIS_RandomItemDropperComponent::GetItemsToDropInternal() const
{
TArray<FGIS_ItemInfo> Results;
UGIS_InventorySystemComponent* Inventory = UGIS_InventorySystemComponent::FindInventorySystemComponent(GetOwner());
if (Inventory == nullptr)
{
GIS_CLOG(Error, "requires inventory system component to drop items.")
return Results;
}
UGIS_ItemCollection* Collection = Inventory->GetCollectionByTag(CollectionTag);
if (Collection == nullptr)
{
GIS_CLOG(Error, " inventory missing collection with tag:%s'", *CollectionTag.ToString())
return Results;
}
const TArray<FGIS_ItemStack>& ItemStacks = Collection->GetAllItemStacks();
TArray<FGIS_ItemInfo> ItemInfos;
ItemInfos.Reserve(ItemStacks.Num());
int32 ProbabilitySum = 0;
for (int32 i = 0; i < ItemStacks.Num(); ++i)
{
//加权
ProbabilitySum += ItemStacks[i].Amount;
ItemInfos[i] = FGIS_ItemInfo(ProbabilitySum, ItemStacks[i]);
}
int32 RandomAmount = FMath::RandRange(MinAmount, MaxAmount + 1);
Results.Empty();
for (int i = 0; i < RandomAmount; i++)
{
auto& selectedItemInfo = GetRandomItemInfo(ItemInfos, ProbabilitySum);
bool foundMatch = false;
//去重,多个栈可能指向同一个道具实例,若发现重复
for (int j = 0; j < Results.Num(); j++)
{
if (Results[j].Item == selectedItemInfo.Item)
{
Results[j] = FGIS_ItemInfo(Results[j].Amount + 1, Results[j]);
foundMatch = true;
break;
}
}
if (!foundMatch) { Results.Add(FGIS_ItemInfo(1, selectedItemInfo)); }
}
return Results;
}
const FGIS_ItemInfo& UGIS_RandomItemDropperComponent::GetRandomItemInfo(const TArray<FGIS_ItemInfo>& ItemInfos, int32 ProbabilitySum) const
{
int32 RandomProbabilityIdx = FMath::RandRange(0, ProbabilitySum);
int32 min = 0;
int32 max = ItemInfos.Num() - 1;
int32 mid = 0;
while (min <= max)
{
mid = (min + max) / 2;
if (ItemInfos[mid].Amount == RandomProbabilityIdx)
{
++mid;
break;
}
if (RandomProbabilityIdx < ItemInfos[mid].Amount
&& (mid == 0 || RandomProbabilityIdx > ItemInfos[mid - 1].Amount)) { break; }
if (RandomProbabilityIdx < ItemInfos[mid].Amount) { max = mid - 1; }
else
{
min = mid + 1;
}
}
return ItemInfos[mid];
}

View File

@@ -0,0 +1,30 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_EquipmentActorInterface.h"
#include "GIS_EquipmentInstance.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_EquipmentActorInterface)
// Add default functionality here for any IGIS_EquipmentActorInterface functions that are not pure virtual.
void IGIS_EquipmentActorInterface::ReceiveSourceEquipment_Implementation(UGIS_EquipmentInstance* NewEquipmentInstance, int32 Idx)
{
}
UGIS_EquipmentInstance* IGIS_EquipmentActorInterface::GetSourceEquipment_Implementation() const
{
return nullptr;
}
void IGIS_EquipmentActorInterface::ReceiveEquipmentBeginPlay_Implementation()
{
}
void IGIS_EquipmentActorInterface::ReceiveEquipmentEndPlay_Implementation()
{
}
UPrimitiveComponent* IGIS_EquipmentActorInterface::GetPrimitiveComponent_Implementation() const
{
return nullptr;
}

View File

@@ -0,0 +1,336 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_EquipmentInstance.h"
#include "Engine/World.h"
#include "Components/SkeletalMeshComponent.h"
#include "Engine/BlueprintGeneratedClass.h"
#include "GameFramework/Character.h"
#include "Net/UnrealNetwork.h"
#include "TimerManager.h"
#if UE_WITH_IRIS
#include "Iris/ReplicationSystem/ReplicationFragmentUtil.h"
#endif // UE_WITH_IRIS
#include "GIS_EquipmentActorInterface.h"
#include "GIS_EquipmentSystemComponent.h"
#include "Items/GIS_ItemInstance.h"
#include "GIS_ItemFragment_Equippable.h"
#include "GIS_LogChannels.h"
#include "Pickups/GIS_WorldItemComponent.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_EquipmentInstance)
class FLifetimeProperty;
class UClass;
class USceneComponent;
UGIS_EquipmentInstance::UGIS_EquipmentInstance(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
bIsActive = false;
}
bool UGIS_EquipmentInstance::IsSupportedForNetworking() const
{
return true;
}
UWorld* UGIS_EquipmentInstance::GetWorld() const
{
if (OwningPawn)
{
return OwningPawn->GetWorld();
}
if (UObject* Outer = GetOuter())
{
return Outer->GetWorld();
}
return nullptr;
}
void UGIS_EquipmentInstance::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// DOREPLIFETIME(ThisClass, SourceItem);
// fix: https://forums.unrealengine.com/t/subobject-replication-for-blueprint-child-class/106205/4
UBlueprintGeneratedClass* bpClass = Cast<UBlueprintGeneratedClass>(this->GetClass());
if (bpClass != nullptr)
{
bpClass->GetLifetimeBlueprintReplicationList(OutLifetimeProps);
}
DOREPLIFETIME(ThisClass, EquipmentActors);
}
void UGIS_EquipmentInstance::ReceiveOwningPawn_Implementation(APawn* NewPawn)
{
OwningPawn = NewPawn;
}
APawn* UGIS_EquipmentInstance::GetOwningPawn_Implementation() const
{
return OwningPawn;
}
void UGIS_EquipmentInstance::ReceiveSourceItem_Implementation(UGIS_ItemInstance* NewItem)
{
SourceItem = NewItem;
}
UGIS_ItemInstance* UGIS_EquipmentInstance::GetSourceItem_Implementation() const
{
return SourceItem;
}
void UGIS_EquipmentInstance::OnEquipmentBeginPlay_Implementation()
{
if (OwningPawn->HasAuthority())
{
SpawnAndSetupEquipmentActors(SourceItem->FindFragmentByClass<UGIS_ItemFragment_Equippable>()->ActorsToSpawn);
}
else
{
SetupEquipmentActors(EquipmentActors);
}
}
void UGIS_EquipmentInstance::OnEquipmentTick_Implementation(float DeltaSeconds)
{
}
void UGIS_EquipmentInstance::OnEquipmentEndPlay_Implementation()
{
DestroyEquipmentActors();
}
#if UE_WITH_IRIS
void UGIS_EquipmentInstance::RegisterReplicationFragments(UE::Net::FFragmentRegistrationContext& Context, UE::Net::EFragmentRegistrationFlags RegistrationFlags)
{
using namespace UE::Net;
// Build descriptors and allocate PropertyReplicationFragments for this object
FReplicationFragmentUtil::CreateAndRegisterFragmentsForObject(this, Context, RegistrationFlags);
}
#endif // UE_WITH_IRIS
bool UGIS_EquipmentInstance::IsEquipmentActive_Implementation() const
{
return bIsActive;
}
void UGIS_EquipmentInstance::OnActiveStateChanged_Implementation(bool NewActiveState)
{
bIsActive = NewActiveState;
SetupActiveStateForEquipmentActors(EquipmentActors);
OnActiveStateChangedEvent.Broadcast(NewActiveState);
}
APawn* UGIS_EquipmentInstance::GetTypedOwningPawn(TSubclassOf<APawn> PawnType) const
{
APawn* Result = nullptr;
if (UClass* ActualPawnType = PawnType)
{
if (APawn* Pawn = Execute_GetOwningPawn(this))
{
if (Pawn->IsA(ActualPawnType))
{
Result = Pawn;
}
}
}
return Result;
}
bool UGIS_EquipmentInstance::CanActivate_Implementation() const
{
return true;
}
int32 UGIS_EquipmentInstance::GetIndexOfEquipmentActor(const AActor* InEquipmentActor) const
{
if (IsValid(InEquipmentActor) && EquipmentActors.Contains(InEquipmentActor))
{
for (int32 i = 0; i < EquipmentActors.Num(); i++)
{
if (EquipmentActors[i] == InEquipmentActor)
{
return i;
}
}
}
return INDEX_NONE;
}
AActor* UGIS_EquipmentInstance::GetTypedEquipmentActor(TSubclassOf<AActor> DesiredClass) const
{
if (UClass* RealClass = DesiredClass)
{
for (const TObjectPtr<AActor>& SpawnedActor : EquipmentActors)
{
if (SpawnedActor && SpawnedActor->GetClass()->IsChildOf(RealClass))
{
return SpawnedActor;
}
}
}
return nullptr;
}
void UGIS_EquipmentInstance::SpawnAndSetupEquipmentActors(const TArray<FGIS_EquipmentActorToSpawn>& ActorsToSpawn)
{
if (OwningPawn == nullptr || !OwningPawn->HasAuthority())
{
return;
}
USceneComponent* AttachParent = GetAttachParentForSpawnedActors(OwningPawn);
for (int32 i = 0; i < ActorsToSpawn.Num(); ++i)
{
const FGIS_EquipmentActorToSpawn& SpawnInfo = ActorsToSpawn[i];
if (SpawnInfo.ActorToSpawn.IsNull())
{
continue;
}
TSubclassOf<AActor> ActorClass = SpawnInfo.ActorToSpawn.LoadSynchronous();
if (!ActorClass)
{
GIS_CLOG(Warning, "%s Failed to load actor class at index: %d ", *GetName(), i);
continue;
}
if (!ActorClass->ImplementsInterface(UGIS_EquipmentActorInterface::StaticClass()))
{
GIS_CLOG(Warning, "actor class(%s) doesn't implements:%s", *ActorClass->GetName(), *UGIS_EquipmentActorInterface::StaticClass()->GetName());
continue;
}
AActor* NewActor = GetWorld()->SpawnActorDeferred<AActor>(ActorClass, FTransform::Identity, OwningPawn);
if (NewActor == nullptr)
{
GIS_CLOG(Warning, "%s Failed to spawn actor of class: %s at index: %d ", *GetName(), *ActorClass->GetName(), i);
continue;
}
BeforeSpawningActor(NewActor);
NewActor->FinishSpawning(FTransform::Identity, /*bIsDefaultTransform=*/true);
if (SpawnInfo.bShouldAttach)
{
NewActor->SetActorRelativeTransform(SpawnInfo.AttachTransform);
NewActor->AttachToComponent(AttachParent, FAttachmentTransformRules::KeepRelativeTransform, SpawnInfo.AttachSocket);
}
EquipmentActors.Add(NewActor);
}
SetupEquipmentActors(EquipmentActors);
}
void UGIS_EquipmentInstance::DestroyEquipmentActors()
{
if (OwningPawn == nullptr || !OwningPawn->HasAuthority())
{
return;
}
for (AActor* Actor : EquipmentActors)
{
if (!IsValid(Actor))
{
continue;
}
if (IsValid(Actor) && Actor->GetClass()->ImplementsInterface(UGIS_EquipmentActorInterface::StaticClass()))
{
IGIS_EquipmentActorInterface::Execute_ReceiveEquipmentEndPlay(Actor);
}
if (IsValid(Actor))
{
Actor->Destroy();
}
}
EquipmentActors.Empty();
}
USceneComponent* UGIS_EquipmentInstance::GetAttachParentForSpawnedActors_Implementation(APawn* Pawn) const
{
if (ACharacter* Char = Cast<ACharacter>(Pawn))
{
return Char->GetMesh();
}
if (Pawn)
{
return Pawn->FindComponentByClass<USkeletalMeshComponent>();
}
return nullptr;
}
void UGIS_EquipmentInstance::BeforeSpawningActor_Implementation(AActor* SpawningActor) const
{
}
void UGIS_EquipmentInstance::SetupEquipmentActors_Implementation(const TArray<AActor*>& InActors)
{
if (!OwningPawn)
{
return;
}
if (OwningPawn->HasAuthority())
{
for (int32 i = 0; i < InActors.Num(); i++)
{
UGIS_WorldItemComponent* WorldItem = InActors[i]->FindComponentByClass<UGIS_WorldItemComponent>();
if (WorldItem != nullptr)
{
WorldItem->SetItemInfo(SourceItem, 1);
}
}
}
for (int32 i = 0; i < InActors.Num(); i++)
{
AActor* Actor = InActors[i];
if (IsValid(Actor))
{
if (Actor->GetClass()->ImplementsInterface(UGIS_EquipmentActorInterface::StaticClass()))
{
IGIS_EquipmentActorInterface::Execute_ReceiveSourceEquipment(Actor, this, i);
IGIS_EquipmentActorInterface::Execute_ReceiveEquipmentBeginPlay(Actor);
}
}
}
}
void UGIS_EquipmentInstance::OnRep_EquipmentActors()
{
}
bool UGIS_EquipmentInstance::IsEquipmentActorsValid(int32 Num) const
{
if (EquipmentActors.Num() == Num)
{
bool bValid = true;
for (int32 i = 0; i < EquipmentActors.Num(); i++)
{
if (!IsValid(EquipmentActors[i]))
{
bValid = false;
break;
}
}
return bValid;
}
return false;
}
void UGIS_EquipmentInstance::SetupInitialStateForEquipmentActors(const TArray<AActor*>& InActors)
{
}
void UGIS_EquipmentInstance::SetupActiveStateForEquipmentActors(const TArray<AActor*>& InActors) const
{
for (int32 i = 0; i < InActors.Num(); i++)
{
AActor* Actor = InActors[i];
if (IsValid(Actor) && Actor->GetClass()->ImplementsInterface(UGIS_EquipmentInterface::StaticClass()))
{
Execute_OnActiveStateChanged(Actor, bIsActive);
}
}
}

View File

@@ -0,0 +1,55 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_EquipmentInterface.h"
#include "GameFramework/Pawn.h"
#include "GIS_EquipmentInstance.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_EquipmentInterface)
void IGIS_EquipmentInterface::ReceiveOwningPawn_Implementation(APawn* NewPawn)
{
}
APawn* IGIS_EquipmentInterface::GetOwningPawn_Implementation() const
{
APawn* ReturnPawn = Cast<APawn>(_getUObject()->GetOuter());
return ReturnPawn;
}
void IGIS_EquipmentInterface::ReceiveSourceItem_Implementation(UGIS_ItemInstance* NewItem)
{
}
UGIS_ItemInstance* IGIS_EquipmentInterface::GetSourceItem_Implementation() const
{
return nullptr;
}
void IGIS_EquipmentInterface::OnActiveStateChanged_Implementation(bool bNewActiveState)
{
}
bool IGIS_EquipmentInterface::IsEquipmentActive_Implementation() const
{
return false;
}
void IGIS_EquipmentInterface::OnEquipmentBeginPlay_Implementation()
{
}
void IGIS_EquipmentInterface::OnOnEquipmentTick_Implementation(float DeltaSeconds)
{
}
void IGIS_EquipmentInterface::OnEquipmentEndPlay_Implementation()
{
}
// Add default functionality here for any IGIS_EquipmentInterface functions that are not pure virtual.
bool IGIS_EquipmentInterface::IsReplicationManaged_Implementation()
{
return _getUObject() && _getUObject()->GetClass()->IsChildOf(UGIS_EquipmentInstance::StaticClass());
}

View File

@@ -0,0 +1,179 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_EquipmentStructLibrary.h"
#include "GIS_EquipmentInstance.h"
#include "GIS_EquipmentSystemComponent.h"
#include "GIS_ItemFragment_Equippable.h"
#include "GIS_ItemInstance.h"
#include "GIS_LogChannels.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_EquipmentStructLibrary)
FString FGIS_EquipmentEntry::GetDebugString() const
{
return FString::Printf(TEXT("%s"), *GetNameSafe(Instance));
}
bool FGIS_EquipmentEntry::CheckClientDataReady() const
{
bool bDataReady = Instance != nullptr && ItemInstance != nullptr && EquippedSlot.IsValid();
if (!bDataReady)
{
return false;
}
UGIS_EquipmentInstance* EquipmentInstance = Cast<UGIS_EquipmentInstance>(Instance);
if (!EquipmentInstance)
return true;
const UGIS_ItemFragment_Equippable* Equippable = ItemInstance->FindFragmentByClass<UGIS_ItemFragment_Equippable>();
if (!Equippable || Equippable->bActorBased || Equippable->ActorsToSpawn.IsEmpty())
return true;
TArray<AActor*> EquipmentActors = EquipmentInstance->GetEquipmentActors();
// actor num doesn't match.
if (EquipmentActors.Num() != Equippable->ActorsToSpawn.Num())
{
return false;
}
bool bValid = true;
for (int32 i = 0; i < EquipmentActors.Num(); i++)
{
if (!::IsValid(EquipmentActors[i]))
{
bValid = false;
break;
}
}
return bValid;
}
bool FGIS_EquipmentEntry::IsValidEntry() const
{
return Instance != nullptr && ItemInstance != nullptr && EquippedSlot.IsValid();
}
void FGIS_EquipmentContainer::PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize)
{
for (int32 Index : RemovedIndices)
{
FGIS_EquipmentEntry& Entry = Entries[Index];
// already in the list.
if (OwningComponent->SlotToInstanceMap.Contains(Entry.EquippedSlot))
{
OwningComponent->OnEquipmentEntryRemoved(Entry, Index);
}
else if (OwningComponent->PendingEquipmentEntries.Contains(Index))
{
GIS_OWNED_CLOG(OwningComponent, Warning, "Discard pending equipment(%s).", *OwningComponent->PendingEquipmentEntries[Index].GetDebugString())
OwningComponent->PendingEquipmentEntries.Remove(Index);
}
Entry.bPrevActive = Entry.bActive;
Entry.PrevEquippedGroup = Entry.EquippedGroup;
}
}
void FGIS_EquipmentContainer::PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize)
{
for (int32 Index : AddedIndices)
{
FGIS_EquipmentEntry& Entry = Entries[Index];
if (OwningComponent)
{
if (OwningComponent->GetOwner() && OwningComponent->IsEquipmentSystemInitialized() && Entry.CheckClientDataReady())
{
OwningComponent->OnEquipmentEntryAdded(Entry, Index);
}
else
{
OwningComponent->PendingEquipmentEntries.Add(Index, Entry);
}
}
Entry.bPrevActive = Entry.bActive;
}
}
void FGIS_EquipmentContainer::PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize)
{
for (int32 Index : ChangedIndices)
{
FGIS_EquipmentEntry& Entry = Entries[Index];
if (OwningComponent->SlotToInstanceMap.Contains(Entry.EquippedSlot)) // Already Added.
{
OwningComponent->OnEquipmentEntryChanged(Entry, Index);
}
else if (OwningComponent->PendingEquipmentEntries.Contains(Index)) // In pending list.
{
OwningComponent->PendingEquipmentEntries.Emplace(Index, Entry);
}
else
{
OwningComponent->PendingEquipmentEntries.Add(Index, Entry); // Add to pending list.
}
Entry.bPrevActive = Entry.bActive;
Entry.PrevEquippedGroup = Entry.EquippedGroup;
}
}
int32 FGIS_EquipmentContainer::IndexOfBySlot(const FGameplayTag& Slot) const
{
if (!Slot.IsValid())
{
return INDEX_NONE;
}
return Entries.IndexOfByPredicate([Slot](const FGIS_EquipmentEntry& Entry)
{
return Entry.EquippedSlot == Slot;
});
}
int32 FGIS_EquipmentContainer::IndexOfByGroup(const FGameplayTag& Group) const
{
if (!Group.IsValid())
{
return INDEX_NONE;
}
return Entries.IndexOfByPredicate([Group](const FGIS_EquipmentEntry& Entry)
{
return Entry.EquippedGroup.IsValid() && Entry.EquippedGroup == Group;
});
}
int32 FGIS_EquipmentContainer::IndexOfByInstance(const UObject* Instance) const
{
if (!IsValid(Instance))
{
return INDEX_NONE;
}
return Entries.IndexOfByPredicate([Instance](const FGIS_EquipmentEntry& Entry)
{
return Entry.Instance && Entry.Instance == Instance;
});
}
int32 FGIS_EquipmentContainer::IndexOfByItem(const UGIS_ItemInstance* Item) const
{
if (!IsValid(Item))
{
return INDEX_NONE;
}
return Entries.IndexOfByPredicate([Item](const FGIS_EquipmentEntry& Entry)
{
return Entry.ItemInstance && Entry.ItemInstance == Item;
});
}
int32 FGIS_EquipmentContainer::IndexOfByItemId(const FGuid& ItemId) const
{
if (!ItemId.IsValid())
{
return INDEX_NONE;
}
return Entries.IndexOfByPredicate([ItemId](const FGIS_EquipmentEntry& Entry)
{
return Entry.ItemInstance && Entry.ItemInstance->GetItemId() == ItemId;
});
}

View File

@@ -0,0 +1,46 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_CurrencyContainer.h"
#include "GIS_CurrencySystemComponent.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CurrencyContainer)
void FGIS_CurrencyContainer::PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize)
{
for (int32 Index : RemovedIndices)
{
FGIS_CurrencyEntry& Entry = Entries[Index];
if (OwningComponent)
{
OwningComponent->OnCurrencyEntryRemoved(Entry, Index);
}
Entry.PrevAmount = 0;
}
}
void FGIS_CurrencyContainer::PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize)
{
for (int32 Index : AddedIndices)
{
FGIS_CurrencyEntry& Entry = Entries[Index];
if (OwningComponent)
{
OwningComponent->OnCurrencyEntryAdded(Entry, Index);
}
Entry.PrevAmount = Entry.Amount;
}
}
void FGIS_CurrencyContainer::PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize)
{
for (int32 Index : ChangedIndices)
{
FGIS_CurrencyEntry& Entry = Entries[Index];
if (OwningComponent)
{
OwningComponent->OnCurrencyEntryUpdated(Entry, Index, Entry.PrevAmount, Entry.Amount);
}
Entry.PrevAmount = Entry.Amount;
}
}

View File

@@ -0,0 +1,51 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Exchange/GIS_CurrencyDefinition.h"
#include "GIS_InventorySubsystem.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CurrencyDefinition)
FGIS_CurrencyExchangeRate::FGIS_CurrencyExchangeRate(const UGIS_CurrencyDefinition* InCurrency, float InExchangeRate)
{
Currency = InCurrency;
ExchangeRate = InExchangeRate;
}
bool UGIS_CurrencyDefinition::TryGetExchangeRateTo(const UGIS_CurrencyDefinition* OtherCurrency, double& ExchangeRate) const
{
if (OtherCurrency == nullptr)
{
ExchangeRate = -1;
return false;
}
//same currency
if (OtherCurrency == this)
{
ExchangeRate = 1;
return true;
}
const FGIS_CurrencyExchangeRate RootExchangeRate = GetRootExchangeRate();
const FGIS_CurrencyExchangeRate OtherRootExchangeRate = OtherCurrency->GetRootExchangeRate();
if (RootExchangeRate.Currency != OtherRootExchangeRate.Currency)
{
ExchangeRate = -1;
return false;
}
ExchangeRate = RootExchangeRate.ExchangeRate / OtherRootExchangeRate.ExchangeRate;
return true;
}
FGIS_CurrencyExchangeRate UGIS_CurrencyDefinition::GetRootExchangeRate(double AdditionalExchangeRate) const
{
if (ParentCurrency)
{
return ParentCurrency->GetRootExchangeRate(AdditionalExchangeRate * ExchangeRateToParent);
}
return FGIS_CurrencyExchangeRate(this, AdditionalExchangeRate);
}

View File

@@ -0,0 +1,44 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_CurrencyEntry.h"
#include "GIS_CurrencyDefinition.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CurrencyEntry)
FGIS_CurrencyEntry::FGIS_CurrencyEntry()
{
Definition = nullptr;
Amount = 0;
}
FGIS_CurrencyEntry::FGIS_CurrencyEntry(const TObjectPtr<const UGIS_CurrencyDefinition>& InDefinition, float InAmount)
{
Definition = InDefinition;
Amount = InAmount;
}
FGIS_CurrencyEntry::FGIS_CurrencyEntry(float InAmount, const TObjectPtr<const UGIS_CurrencyDefinition>& InDefinition)
{
Definition = InDefinition;
Amount = InAmount;
}
bool FGIS_CurrencyEntry::Equals(const FGIS_CurrencyEntry& Other) const
{
return Amount == Other.Amount && Definition == Other.Definition;
}
FString FGIS_CurrencyEntry::ToString() const
{
return FString::Format(TEXT("{0} {1}"), {Definition ? Definition->GetName() : TEXT("None"), Amount});
}
bool FGIS_CurrencyEntry::operator==(const FGIS_CurrencyEntry& Rhs) const
{
return Equals(Rhs);
}
bool FGIS_CurrencyEntry::operator!=(const FGIS_CurrencyEntry& Rhs) const
{
return !Equals(Rhs);
}

View File

@@ -0,0 +1,550 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Exchange/GIS_CurrencySystemComponent.h"
#include "Engine/World.h"
#include "GIS_InventorySubsystem.h"
#include "GameFramework/Actor.h"
#include "UObject/Object.h"
#include "GIS_InventoryTags.h"
#include "GIS_LogChannels.h"
#include "Net/UnrealNetwork.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CurrencySystemComponent)
UGIS_CurrencySystemComponent::UGIS_CurrencySystemComponent() : Container(this)
{
PrimaryComponentTick.bCanEverTick = false;
SetIsReplicatedByDefault(true);
bReplicateUsingRegisteredSubObjectList = true;
bWantsInitializeComponent = true;
}
UGIS_CurrencySystemComponent* UGIS_CurrencySystemComponent::GetCurrencySystemComponent(const AActor* Actor)
{
return IsValid(Actor) ? Actor->FindComponentByClass<UGIS_CurrencySystemComponent>() : nullptr;
}
void UGIS_CurrencySystemComponent::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ThisClass, Container);
}
void UGIS_CurrencySystemComponent::InitializeComponent()
{
Super::InitializeComponent();
if (GetWorld() && !GetWorld()->IsGameWorld())
{
return;
}
Container.OwningComponent = this;
}
TArray<FGIS_CurrencyEntry> UGIS_CurrencySystemComponent::GetAllCurrencies() const
{
TArray<FGIS_CurrencyEntry> Ret;
for (const FGIS_CurrencyEntry& Item : Container.Entries)
{
Ret.Add(FGIS_CurrencyEntry(Item.Definition, Item.Amount));
}
return Ret;
}
void UGIS_CurrencySystemComponent::SetCurrencies(const TArray<FGIS_CurrencyEntry>& InCurrencyInfos)
{
TArray<FGIS_CurrencyEntry> NewEntries = InCurrencyInfos.FilterByPredicate([](const FGIS_CurrencyEntry& Item)
{
return Item.Definition != nullptr && Item.Amount > 0;
});
Container.Entries.Empty();
CurrencyMap.Empty();
Container.Entries = NewEntries;
for (const FGIS_CurrencyEntry& NewItem : NewEntries)
{
CurrencyMap.Add(NewItem.Definition, NewItem.Amount);
}
Container.MarkArrayDirty();
}
void UGIS_CurrencySystemComponent::EmptyCurrencies()
{
Container.Entries.Empty();
CurrencyMap.Empty();
Container.MarkArrayDirty();
}
bool UGIS_CurrencySystemComponent::GetCurrency(TSoftObjectPtr<const UGIS_CurrencyDefinition> CurrencyDefinition, FGIS_CurrencyEntry& OutCurrencyInfo) const
{
if (CurrencyDefinition.IsNull())
{
return false;
}
const UGIS_CurrencyDefinition* Definition = CurrencyDefinition.LoadSynchronous();
return GetCurrencyInternal(Definition, OutCurrencyInfo);
}
bool UGIS_CurrencySystemComponent::GetCurrencies(TArray<TSoftObjectPtr<UGIS_CurrencyDefinition>> CurrencyDefinitions, TArray<FGIS_CurrencyEntry>& OutCurrencyInfos) const
{
TArray<TObjectPtr<const UGIS_CurrencyDefinition>> Definitions;
for (TSoftObjectPtr<const UGIS_CurrencyDefinition> Currency : CurrencyDefinitions)
{
if (const UGIS_CurrencyDefinition* Definition = !Currency.IsNull() ? Currency.LoadSynchronous() : nullptr)
{
Definitions.AddUnique(Definition);
}
}
return GetCurrenciesInternal(Definitions, OutCurrencyInfos);
}
bool UGIS_CurrencySystemComponent::AddCurrency(FGIS_CurrencyEntry CurrencyInfo)
{
return AddCurrencyInternal(CurrencyInfo);
}
bool UGIS_CurrencySystemComponent::RemoveCurrency(FGIS_CurrencyEntry CurrencyInfo)
{
return RemoveCurrencyInternal(CurrencyInfo);
}
bool UGIS_CurrencySystemComponent::HasCurrency(FGIS_CurrencyEntry CurrencyInfo) const
{
return HasCurrencyInternal(CurrencyInfo);
}
bool UGIS_CurrencySystemComponent::HasCurrencies(const TArray<FGIS_CurrencyEntry>& CurrencyInfos)
{
bool bOk = true;
for (auto& Currency : CurrencyInfos)
{
if (!HasCurrencyInternal(Currency))
{
bOk = false;
break;
}
}
return bOk;
}
bool UGIS_CurrencySystemComponent::AddCurrencies(const TArray<FGIS_CurrencyEntry>& CurrencyInfos)
{
for (const FGIS_CurrencyEntry& Currency : CurrencyInfos)
{
AddCurrencyInternal(Currency);
}
return true;
}
bool UGIS_CurrencySystemComponent::RemoveCurrencies(const TArray<FGIS_CurrencyEntry>& CurrencyInfos)
{
for (const FGIS_CurrencyEntry& Currency : CurrencyInfos)
{
RemoveCurrencyInternal(Currency);
}
return true;
}
void UGIS_CurrencySystemComponent::BeginPlay()
{
Super::BeginPlay();
AddInitialCurrencies();
}
void UGIS_CurrencySystemComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
}
#pragma region Static Calculations (To be continued)
#if 0
bool UGIS_CurrencySystemComponent::AddCurrencyInternal(const FGIS_CurrencyEntry& CurrencyInfo, bool bNotify)
{
if (!CurrencyInfo.Definition || CurrencyInfo.Amount <= 0)
{
FFrame::KismetExecutionMessage(TEXT("An invalid currency definition was passed."), ELogVerbosity::Warning);
return false;
}
auto discreteCurrencyAmounts = ConvertToDiscrete(CurrencyInfo.Definition, CurrencyInfo.Amount);
auto currencyAmountCopy = Container.Entries;
auto added = DiscreteAddition(currencyAmountCopy, discreteCurrencyAmounts);
SetCurrencyInfos(added);
return true;
}
int32 UGIS_CurrencySystemComponent::FindIndexWithCurrency(const UGIS_CurrencyDefinition* Currency, const TArray<FGIS_CurrencyEntry>& List)
{
for (int32 i = 0; i < List.Num(); i++)
{
if (List[i].Definition == Currency)
{
return i;
}
}
return INDEX_NONE;
}
int32 UGIS_CurrencySystemComponent::FindOrCreateCurrencyIndex(const UGIS_CurrencyDefinition* Currency, TArray<FGIS_CurrencyEntry>& Result)
{
int32 Index = FindIndexWithCurrency(Currency, Result);
if (Index != INDEX_NONE)
{
return Index;
}
Result.Add(FGIS_CurrencyEntry(0.0f, Currency));
return Result.Num() - 1;
}
TArray<FGIS_CurrencyEntry> UGIS_CurrencySystemComponent::DiscreteAddition(const TArray<FGIS_CurrencyEntry>& Lhs, const TArray<FGIS_CurrencyEntry>& Rhs)
{
TArray<FGIS_CurrencyEntry> Result;
TArray<FGIS_CurrencyEntry> TempArray = Lhs; // 复制 Lhs 作为初始结果
for (int32 i = 0; i < Rhs.Num(); i++)
{
// 交替使用 Result 和 TempArray 避免重复分配
TArray<FGIS_CurrencyEntry>& Target = (i % 2 == 0) ? Result : TempArray;
TempArray = DiscreteAddition(TempArray, Rhs[i].Definition, Rhs[i].Amount);
}
// 确保最终结果存储在 Result 中
if (TempArray != Result)
{
Result = TempArray;
}
return Result;
}
TArray<FGIS_CurrencyEntry> UGIS_CurrencySystemComponent::DiscreteAddition(const TArray<FGIS_CurrencyEntry>& CurrencyAmounts, const UGIS_CurrencyDefinition* Currency, float Amount)
{
TArray<FGIS_CurrencyEntry> Result = CurrencyAmounts; // 复制输入数组
while (true)
{
int32 Index = FindOrCreateCurrencyIndex(Currency, Result);
float Sum = Amount + Result[Index].Amount;
float Mod = FMath::Fmod(Sum, Currency->MaxAmount + 1.0f);
Result[Index] = FGIS_CurrencyEntry(Mod, Currency);
float Overflow = Sum - Mod;
if (Overflow <= 0.0f)
{
break;
}
const UGIS_CurrencyDefinition* OverflowCurrency = Currency->OverflowCurrency;
double Rate;
if (!OverflowCurrency || !Currency->TryGetExchangeRateTo(OverflowCurrency, Rate))
{
return SetAllFractionToMax(CurrencyAmounts, Currency);
}
double OverflowDouble = Overflow * Rate; // 使用 Rate 而非 Amount
int32 OverflowInt = FMath::TruncToInt(OverflowDouble);
double Diff = OverflowDouble - OverflowInt;
if (Diff > 0.0)
{
TArray<FGIS_CurrencyEntry> DiscreteDiff = ConvertToDiscrete(OverflowCurrency, Diff);
Result = DiscreteAddition(Result, DiscreteDiff);
}
Currency = OverflowCurrency;
Amount = static_cast<float>(OverflowInt);
}
return Result;
}
TArray<FGIS_CurrencyEntry> UGIS_CurrencySystemComponent::SetAllFractionToMax(const TArray<FGIS_CurrencyEntry>& CurrencyAmounts, const UGIS_CurrencyDefinition* Currency)
{
TArray<FGIS_CurrencyEntry> Result = CurrencyAmounts;
while (Currency != nullptr)
{
int32 Index = FindOrCreateCurrencyIndex(Currency, Result);
Result[Index] = FGIS_CurrencyEntry(Currency->MaxAmount, Currency);
Currency = Currency->FractionCurrency;
}
return Result;
}
TArray<FGIS_CurrencyEntry> UGIS_CurrencySystemComponent::MaxedOutAmount(const UGIS_CurrencyDefinition* Currency)
{
TArray<FGIS_CurrencyEntry> Result;
int32 Index = 0;
while (Currency != nullptr)
{
Result.SetNum(Index + 1);
Result[Index] = FGIS_CurrencyEntry(Currency->MaxAmount, Currency);
Index++;
Currency = Currency->FractionCurrency;
}
return Result;
}
TArray<FGIS_CurrencyEntry> UGIS_CurrencySystemComponent::ConvertToDiscrete(const UGIS_CurrencyDefinition* Currency, double Amount)
{
TArray<FGIS_CurrencyEntry> Result;
int32 Index = 0;
TArray<FGIS_CurrencyEntry> OverflowCurrencies = ConvertOverflow(Currency, Amount);
bool bMaxed = false;
for (int32 i = OverflowCurrencies.Num() - 1; i >= 0; i--)
{
Result.SetNum(Index + 1);
if (Currency->FractionCurrency == OverflowCurrencies[i].Definition)
{
bMaxed = true;
}
Result[Index] = OverflowCurrencies[i];
Index++;
}
if (!bMaxed)
{
TArray<FGIS_CurrencyEntry> FractionCurrencies = ConvertFraction(Currency, Amount);
for (int32 i = 0; i < FractionCurrencies.Num(); i++)
{
Result.SetNum(Index + 1);
Result[Index] = FractionCurrencies[i];
Index++;
}
}
return Result;
}
TArray<FGIS_CurrencyEntry> UGIS_CurrencySystemComponent::ConvertOverflow(const UGIS_CurrencyDefinition* Currency, double Amount)
{
TArray<FGIS_CurrencyEntry> Result;
int32 Index = 0;
const UGIS_CurrencyDefinition* NextCurrency = Currency;
Amount = FMath::TruncToDouble(Amount);
while (NextCurrency != nullptr && Amount > 0.0)
{
double Mod = fmod(Amount, NextCurrency->MaxAmount + 1.0);
float IntMod = static_cast<float>(Mod);
if (IntMod > 0.0f)
{
Result.SetNum(Index + 1);
Result[Index] = FGIS_CurrencyEntry(IntMod, NextCurrency);
Index++;
}
if (Amount - IntMod <= 0.0)
{
break;
}
double Rate;
if (!NextCurrency->OverflowCurrency || !NextCurrency->TryGetExchangeRateTo(NextCurrency->OverflowCurrency, Rate))
{
return MaxedOutAmount(NextCurrency);
}
Amount -= Mod;
Amount *= Rate;
NextCurrency = NextCurrency->OverflowCurrency;
}
return Result;
}
TArray<FGIS_CurrencyEntry> UGIS_CurrencySystemComponent::ConvertFraction(const UGIS_CurrencyDefinition* Currency, double Amount)
{
TArray<FGIS_CurrencyEntry> Result;
if (FMath::IsNearlyZero(fmod(Amount, 1.0)))
{
return Result;
}
int32 Index = 0;
const UGIS_CurrencyDefinition* NextCurrency = Currency;
double DecimalAmount = fmod(Amount, 1.0);
while (NextCurrency != nullptr && DecimalAmount > 0.0)
{
if (!NextCurrency->FractionCurrency)
{
break;
}
double Rate;
if (!NextCurrency->TryGetExchangeRateTo(NextCurrency->FractionCurrency, Rate))
{
break;
}
NextCurrency = NextCurrency->FractionCurrency;
DecimalAmount *= Rate;
int32 Floor = static_cast<int32>(FMath::Floor(DecimalAmount));
if (Floor > 0)
{
Result.SetNum(Index + 1);
Result[Index] = FGIS_CurrencyEntry(static_cast<float>(Floor), NextCurrency);
Index++;
}
DecimalAmount -= Floor;
}
return Result;
}
#endif
#pragma endregion
void UGIS_CurrencySystemComponent::OnCurrencyEntryAdded(const FGIS_CurrencyEntry& Entry, int32 Idx)
{
CurrencyMap.Add(Entry.Definition, Entry.Amount);
OnCurrencyChangedEvent.Broadcast(Entry.Definition, 0, Entry.Amount);
}
void UGIS_CurrencySystemComponent::OnCurrencyEntryRemoved(const FGIS_CurrencyEntry& Entry, int32 Idx)
{
CurrencyMap.Remove(Entry.Definition);
OnCurrencyChangedEvent.Broadcast(Entry.Definition, Entry.PrevAmount, 0);
}
void UGIS_CurrencySystemComponent::OnCurrencyEntryUpdated(const FGIS_CurrencyEntry& Entry, int32 Idx, float OldAmount, float NewAmount)
{
CurrencyMap.Emplace(Entry.Definition, NewAmount);
OnCurrencyChangedEvent.Broadcast(Entry.Definition, OldAmount, NewAmount);
}
void UGIS_CurrencySystemComponent::OnCurrencyChanged(TObjectPtr<const UGIS_CurrencyDefinition> Currency, float OldValue, float NewValue)
{
OnCurrencyChangedEvent.Broadcast(Currency, OldValue, NewValue);
}
void UGIS_CurrencySystemComponent::AddInitialCurrencies_Implementation()
{
if (GetOwner()->HasAuthority())
{
//TODO check records from save game.
if (!DefaultCurrencies.IsEmpty())
{
AddCurrencies(DefaultCurrencies);
}
}
}
bool UGIS_CurrencySystemComponent::GetCurrencyInternal(const TObjectPtr<const UGIS_CurrencyDefinition>& Currency, FGIS_CurrencyEntry& OutCurrencyInfo) const
{
if (CurrencyMap.Contains(Currency))
{
OutCurrencyInfo = FGIS_CurrencyEntry(Currency, CurrencyMap[Currency]);
return true;
}
return false;
}
bool UGIS_CurrencySystemComponent::GetCurrenciesInternal(const TArray<TObjectPtr<const UGIS_CurrencyDefinition>>& Currencies, TArray<FGIS_CurrencyEntry>& OutCurrencies) const
{
TArray<FGIS_CurrencyEntry> Result;
for (int32 i = 0; i < Currencies.Num(); i++)
{
FGIS_CurrencyEntry Info;
if (GetCurrencyInternal(Currencies[i], Info))
{
Result.Add(Info);
}
}
OutCurrencies = Result;
return !OutCurrencies.IsEmpty();
}
bool UGIS_CurrencySystemComponent::AddCurrencyInternal(const FGIS_CurrencyEntry& CurrencyInfo, bool bNotify)
{
if (!CurrencyInfo.Definition || CurrencyInfo.Amount <= 0)
{
GIS_CLOG(Warning, "An invalid currency definition was passed.")
// FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed."), ELogVerbosity::Warning);
return false;
}
for (int32 i = 0; i < Container.Entries.Num(); i++)
{
// handle adding to existing value.
FGIS_CurrencyEntry& Entry = Container.Entries[i];
if (Entry.Definition == CurrencyInfo.Definition)
{
const float OldValue = Entry.Amount;
const float NewValue = Entry.Amount + CurrencyInfo.Amount;
Entry.Amount = NewValue;
OnCurrencyEntryUpdated(Entry, i, OldValue, NewValue);
Container.MarkItemDirty(Entry);
return true;
}
}
int32 Idx = Container.Entries.AddDefaulted();
Container.Entries[Idx] = CurrencyInfo;
OnCurrencyEntryAdded(Container.Entries[Idx], Idx);
Container.MarkItemDirty(Container.Entries[Idx]);
return true;
}
bool UGIS_CurrencySystemComponent::RemoveCurrencyInternal(const FGIS_CurrencyEntry& CurrencyInfo, bool bNotify)
{
if (!CurrencyInfo.Definition || CurrencyInfo.Amount <= 0)
{
GIS_CLOG(Warning, "An invalid tag was passed to RemoveItem")
// FFrame::KismetExecutionMessage(TEXT("An invalid tag was passed to RemoveItem"), ELogVerbosity::Warning);
return false;
}
for (int32 i = 0; i < Container.Entries.Num(); i++)
{
FGIS_CurrencyEntry& Entry = Container.Entries[i];
if (Entry.Definition == CurrencyInfo.Definition)
{
if (Entry.Amount <= CurrencyInfo.Amount)
{
OnCurrencyEntryRemoved(Entry, i);
Container.Entries.RemoveAt(i);
Container.MarkArrayDirty();
}
else
{
const float OldValue = Entry.Amount;
const float NewValue = Entry.Amount - CurrencyInfo.Amount;
Entry.Amount = NewValue;
OnCurrencyEntryUpdated(Entry, i, OldValue, NewValue);
Container.MarkItemDirty(Entry);
}
return true;
}
}
return false;
}
bool UGIS_CurrencySystemComponent::HasCurrencyInternal(const FGIS_CurrencyEntry& CurrencyInfo) const
{
if (CurrencyMap.Contains(CurrencyInfo.Definition) && CurrencyInfo.Amount > 0)
{
return CurrencyMap[CurrencyInfo.Definition] >= CurrencyInfo.Amount;
}
return false;
}

View File

@@ -0,0 +1,7 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Exchange/Shops/GIS_ShopCondition.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ShopCondition)
// Add default functionality here for any IGShopCondition functions that are not pure virtual.

View File

@@ -0,0 +1,365 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Exchange/Shops/GIS_ShopSystemComponent.h"
#include "UObject/Object.h"
#include "GameFramework/Actor.h"
#include "GIS_CurrencySystemComponent.h"
#include "GIS_InventoryFunctionLibrary.h"
#include "GIS_InventorySubsystem.h"
#include "GIS_InventorySystemComponent.h"
#include "GIS_ItemCollection.h"
#include "GIS_ItemDefinition.h"
#include "GIS_ItemFragment_Shoppable.h"
#include "Items/GIS_ItemInstance.h"
#include "GIS_LogChannels.h"
#include "Exchange/Shops/GIS_ShopCondition.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ShopSystemComponent)
UGIS_ShopSystemComponent::UGIS_ShopSystemComponent()
{
PrimaryComponentTick.bStartWithTickEnabled = false;
PrimaryComponentTick.bCanEverTick = false;
}
UGIS_ShopSystemComponent* UGIS_ShopSystemComponent::GetShopSystemComponent(const AActor* Actor)
{
return IsValid(Actor) ? Actor->FindComponentByClass<UGIS_ShopSystemComponent>() : nullptr;
}
UGIS_InventorySystemComponent* UGIS_ShopSystemComponent::GetInventory() const
{
return OwningInventory;
}
bool UGIS_ShopSystemComponent::BuyItem(UGIS_InventorySystemComponent* BuyerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo)
{
return BuyItemInternal(BuyerInventory, CurrencySystem, ItemInfo);
}
bool UGIS_ShopSystemComponent::SellItem(UGIS_InventorySystemComponent* SellerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo)
{
return SellItemInternal(SellerInventory, CurrencySystem, ItemInfo);
}
bool UGIS_ShopSystemComponent::CanBuyerBuyItem(UGIS_InventorySystemComponent* BuyerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo) const
{
UGIS_ItemCollection* TargetCollection = BuyerInventory->GetCollectionByTag(TargetItemCollectionToAddOnBuy);
if (TargetCollection == nullptr)
{
GIS_CLOG(Warning, "buyer's inventory missing collection named:%s", *TargetItemCollectionToAddOnBuy.ToString());
return false;
}
FGIS_ItemInfo CanAddItemInfo;
if (!TargetCollection->CanAddItem(ItemInfo, CanAddItemInfo))
{
GIS_CLOG(Warning, "buyer's collection can't add this item(%s)", *ItemInfo.GetDebugString());
return false;
}
for (int i = 0; i < BuyConditions.Num(); i++)
{
if (BuyConditions[i]->CanBuy(this, BuyerInventory, CurrencySystem, ItemInfo)) { continue; }
GIS_CLOG(Warning, "buy collection(%s) reject buying item(%s)", *BuyConditions[i].GetObject()->GetClass()->GetName(), *ItemInfo.GetDebugString());
return false;
}
return CanBuyerBuyItemInternal(BuyerInventory, CurrencySystem, ItemInfo);
}
bool UGIS_ShopSystemComponent::CanSellerSellItem(UGIS_InventorySystemComponent* SellerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo) const
{
for (int i = 0; i < SellConditions.Num(); i++)
{
if (SellConditions[i]->CanSell(this, SellerInventory, CurrencySystem, ItemInfo)) { continue; }
GIS_CLOG(Warning, "sell collection(%s) reject selling item(%s)", *SellConditions[i].GetObject()->GetClass()->GetName(), *ItemInfo.GetDebugString());
return false;
}
return CanSellerSellItemInternal(SellerInventory, CurrencySystem, ItemInfo);
}
bool UGIS_ShopSystemComponent::IsItemBuyable(const FGIS_ItemInfo& ItemInfo) const
{
if (OwningInventory == nullptr) { return false; }
if (!ItemInfo.IsValid())
{
GIS_CLOG(Warning, "invalid item to buy.");
return false;
}
UGIS_ItemCollection* ItemCollection = ItemInfo.Item->GetOwningCollection();
if (ItemCollection == nullptr) { ItemCollection = OwningInventory->GetDefaultCollection(); }
if (!ItemCollection->HasItem(ItemInfo.Item, 1))
{
GIS_CLOG(Warning, "shop's inventory doesn't have item:%s", *ItemInfo.Item->GetDefinition()->GetName());
return false;
}
const UGIS_ItemFragment_Shoppable* Shoppable = ItemInfo.Item->FindFragmentByClass<UGIS_ItemFragment_Shoppable>();
if (Shoppable == nullptr)
{
GIS_CLOG(Warning, "item(%s) is not buyable, missing Shoppable fragment!", *ItemInfo.GetDebugString());
return false;
}
if (Shoppable->BuyCurrencyAmounts.IsEmpty())
{
GIS_CLOG(Warning, "item(%s) is not buyable, missing BuyCurrencyAmounts in shoppable fragment!", *ItemInfo.GetDebugString());
return false;
}
return true;
}
bool UGIS_ShopSystemComponent::IsItemSellable(const FGIS_ItemInfo& ItemInfo) const
{
if (!ItemInfo.IsValid())
{
GIS_CLOG(Warning, "invalid item to sell.");
return false;
}
const UGIS_ItemFragment_Shoppable* Shoppable = ItemInfo.Item->FindFragmentByClass<UGIS_ItemFragment_Shoppable>();
if (Shoppable == nullptr)
{
GIS_CLOG(Warning, "item(%s) is not sellable, missing Shoppable fragment!", *ItemInfo.GetDebugString());
return false;
}
if (Shoppable->SellCurrencyAmounts.IsEmpty())
{
GIS_CLOG(Warning, "item(%s) is not sellable, missing SellCurrencyAmounts in shoppable fragment!", *ItemInfo.GetDebugString());
return false;
}
return true;
}
float UGIS_ShopSystemComponent::GetBuyModifierForBuyer_Implementation(UGIS_InventorySystemComponent* BuyerInventory) const
{
return 1 + BuyPriceModifier;
}
// float UGIS_ShopSystemComponent::GetBuyModifierForItem(UGIS_InventorySystemComponent* BuyerInventory, FGIS_ItemInfo ItemInfo) const
// {
// return 1;
// }
float UGIS_ShopSystemComponent::GetSellModifierForSeller_Implementation(UGIS_InventorySystemComponent* SellerInventory) const
{
return 1 + SellPriceModifer;
}
// float UGIS_ShopSystemComponent::GetSellModifierForItem(UGIS_InventorySystemComponent* SellerInventory, const FGIS_ItemInfo& ItemInfo) const
// {
// return 1;
// }
bool UGIS_ShopSystemComponent::TryGetBuyValueForBuyer_Implementation(UGIS_InventorySystemComponent* Buyer, const FGIS_ItemInfo& ItemInfo, TArray<FGIS_CurrencyEntry>& BuyValue) const
{
if (!IsValid(ItemInfo.Item))
{
GIS_CLOG(Warning, "invalid item to buy.");
return false;
}
const UGIS_ItemFragment_Shoppable* Shoppable = ItemInfo.Item->FindFragmentByClass<UGIS_ItemFragment_Shoppable>();
if (!IsValid(Shoppable))
{
GIS_CLOG(Warning, "missing Shoppable fragment for item:%s!", *GetNameSafe(ItemInfo.Item->GetDefinition()));
return false;
}
float Modifier = GetBuyModifierForBuyer(Buyer);
BuyValue = UGIS_InventoryFunctionLibrary::MultiplyCurrencies(Shoppable->BuyCurrencyAmounts, Modifier * ItemInfo.Amount);
return BuyValue.IsEmpty() == false;
}
bool UGIS_ShopSystemComponent::TryGetSellValueForSeller_Implementation(UGIS_InventorySystemComponent* Seller, const FGIS_ItemInfo& ItemInfo, TArray<FGIS_CurrencyEntry>& SellValue) const
{
if (!IsValid(ItemInfo.Item))
{
GIS_CLOG(Warning, "invalid item to sell.");
return false;
}
const UGIS_ItemFragment_Shoppable* Shoppable = ItemInfo.Item->FindFragmentByClass<UGIS_ItemFragment_Shoppable>();
if (!IsValid(Shoppable))
{
GIS_CLOG(Warning, "missing Shoppable fragment for item:%s!", *GetNameSafe(ItemInfo.Item->GetDefinition()));
return false;
}
float Modifier = GetSellModifierForSeller(Seller);
SellValue = UGIS_InventoryFunctionLibrary::MultiplyCurrencies(Shoppable->SellCurrencyAmounts, Modifier * ItemInfo.Amount);
return SellValue.IsEmpty() == false;
}
void UGIS_ShopSystemComponent::BeginPlay()
{
OwningInventory = UGIS_InventorySystemComponent::FindInventorySystemComponent(GetOwner());
if (!OwningInventory)
{
GIS_CLOG(Error, "Requires inventory system component!");
}
{
TArray<UActorComponent*> Components = GetOwner()->GetComponentsByInterface(UGIS_ShopBuyCondition::StaticClass());
BuyConditions.Empty();
for (const auto Component : Components)
{
BuyConditions.Add(Component);
}
}
{
TArray<UActorComponent*> Components = GetOwner()->GetComponentsByInterface(UGIS_ShopSellCondition::StaticClass());
SellConditions.Empty();
for (const auto Component : Components)
{
SellConditions.Add(Component);
}
}
Super::BeginPlay();
}
void UGIS_ShopSystemComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
}
bool UGIS_ShopSystemComponent::CanSellerSellItemInternal_Implementation(UGIS_InventorySystemComponent* SellerInventory, UGIS_CurrencySystemComponent* CurrencySystem,
const FGIS_ItemInfo& ItemInfo) const
{
UGIS_ItemCollection* ItemCollection = ItemInfo.ItemCollection;
if (ItemCollection == nullptr || ItemCollection->GetOwningInventory() == SellerInventory)
{
ItemCollection = SellerInventory->GetDefaultCollection();
}
if (ItemCollection == nullptr)
{
GIS_CLOG(Warning, "seller:%s doesn't have valid default collection.", *GetNameSafe(SellerInventory));
return false;
}
//atleast has one.
if (!ItemCollection->HasItem(ItemInfo.Item, 1))
{
return false;
}
return true;
}
bool UGIS_ShopSystemComponent::CanBuyerBuyItemInternal_Implementation(UGIS_InventorySystemComponent* BuyerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo) const
{
TArray<FGIS_CurrencyEntry> BuyPrice;
if (TryGetBuyValueForBuyer(BuyerInventory, ItemInfo, BuyPrice))
{
return CurrencySystem->HasCurrencies(BuyPrice);
}
return false;
}
bool UGIS_ShopSystemComponent::SellItemInternal_Implementation(UGIS_InventorySystemComponent* Seller, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo)
{
if (!IsValid(Seller) || !ItemInfo.IsValid() || !IsValid(CurrencySystem))
{
GIS_CLOG(Warning, "passed invalid parameters!");
return false;
}
if (!IsItemSellable(ItemInfo))
{
GIS_CLOG(Warning, "item:%s is not sellable", *ItemInfo.GetDebugString());
return false;
}
if (!CanSellerSellItem(Seller, CurrencySystem, ItemInfo))
{
GIS_CLOG(Warning, "seller can sell this item:%s", *ItemInfo.GetDebugString());
return false;
}
UGIS_ItemCollection* ItemCollection = ItemInfo.ItemCollection;
if (ItemCollection == nullptr || ItemCollection->GetOwningInventory() == Seller)
{
ItemCollection = Seller->GetDefaultCollection();
}
if (ItemCollection->RemoveItem(ItemInfo).Amount != ItemInfo.Amount)
{
GIS_CLOG(Error, "Failed to remove item(%s) from inventory!", *ItemInfo.GetDebugString());
return false;
}
TArray<FGIS_CurrencyEntry> SellCurrencyAmount;
if (!TryGetSellValueForSeller(Seller, ItemInfo, SellCurrencyAmount))
{
GIS_CLOG(Error, "can't get sell value for item:%s", *ItemInfo.GetDebugString());
return false;
}
CurrencySystem->AddCurrencies(SellCurrencyAmount);
OwningInventory->AddItem(ItemInfo);
return true;
}
bool UGIS_ShopSystemComponent::BuyItemInternal_Implementation(UGIS_InventorySystemComponent* BuyerInventory, UGIS_CurrencySystemComponent* CurrencySystem, const FGIS_ItemInfo& ItemInfo)
{
if (!IsValid(BuyerInventory) || !ItemInfo.IsValid() || !IsValid(CurrencySystem))
{
return false;
}
if (!IsItemBuyable(ItemInfo))
{
return false;
}
if (!CanBuyerBuyItem(BuyerInventory, CurrencySystem, ItemInfo))
{
return false;
}
UGIS_ItemCollection* TargetCollection = BuyerInventory->GetCollectionByTag(TargetItemCollectionToAddOnBuy);
if (TargetCollection == nullptr)
{
return false;
}
//remove currency from buyer's currency system.
TArray<FGIS_CurrencyEntry> BuyPrice;
if (TryGetBuyValueForBuyer(BuyerInventory, ItemInfo, BuyPrice))
{
CurrencySystem->RemoveCurrencies(BuyPrice);
}
else
{
return false;
}
if (ItemInfo.Item->IsUnique())
{
for (int32 i = 0; i < ItemInfo.Amount; ++i)
{
UGIS_ItemInstance* NewItem = UGIS_InventorySubsystem::Get(this)->CreateItem(BuyerInventory->GetOwner(), ItemInfo.Item->GetDefinition());
TargetCollection->AddItem(NewItem, 1);
}
}
else
{
TargetCollection->AddItem(ItemInfo);
}
// Remove item from shop's inventory.
OwningInventory->RemoveItem(ItemInfo);
// Add currency to shop's currency system.
if (UGIS_CurrencySystemComponent* OwningCurrencySystem = UGIS_CurrencySystemComponent::GetCurrencySystemComponent(GetOwner()))
{
OwningCurrencySystem->AddCurrencies(BuyPrice);
}
return true;
}

View File

@@ -0,0 +1,400 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_InventoryFactory.h"
#include "UObject/Object.h"
#include "GameFramework/Actor.h"
#include "Serialization/MemoryReader.h"
#include "Serialization/MemoryWriter.h"
#include "GIS_CollectionContainer.h"
#include "GIS_InventorySystemComponent.h"
#include "GIS_ItemCollection.h"
#include "Items/GIS_ItemDefinition.h"
#include "GIS_ItemFragment.h"
#include "GIS_LogChannels.h"
#include "Items/GIS_ItemInstance.h"
#include "Misc/DataValidation.h"
#include "Serialization/ObjectAndNameAsStringProxyArchive.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_InventoryFactory)
UGIS_ItemInstance* UGIS_InventoryFactory::DuplicateItem_Implementation(AActor* Owner, UGIS_ItemInstance* SrcItem, bool bGenerateNewId)
{
if (!IsValid(SrcItem) || SrcItem->GetDefinition() == nullptr)
{
GIS_LOG(Error, "Missing src item or src item doesn't have valid definition.");
return nullptr;
}
UGIS_ItemInstance* NewItem = DuplicateObject(SrcItem, Owner);
if (bGenerateNewId)
{
NewItem->SetItemId(FGuid::NewGuid());
}
NewItem->OnItemDuplicated(SrcItem);
return NewItem;
}
UGIS_ItemCollection* UGIS_InventoryFactory::CreateCollection_Implementation(AActor* Owner, const UGIS_ItemCollectionDefinition* Definition)
{
if (!IsValid(Owner))
{
GIS_LOG(Error, "Missing owner.");
return nullptr;
}
if (!IsValid(Definition))
{
GIS_LOG(Error, "Cannot create collection with null collection Definition.");
return nullptr;
}
TSubclassOf<UGIS_ItemCollection> CollectionClass = Definition->GetCollectionInstanceClass();
if (CollectionClass == nullptr)
{
GIS_LOG(Error, "definition(%s) doesn't specify valid item collection class.", *Definition->GetName())
return nullptr;
}
UGIS_ItemCollection* NewCollection = NewObject<UGIS_ItemCollection>(Owner, CollectionClass);
if (NewCollection == nullptr)
{
GIS_LOG(Error, "failed to create instance of %s", *GetNameSafe(Definition))
return nullptr;
}
return NewCollection;
}
UGIS_InventoryFactory::UGIS_InventoryFactory()
{
DefaultItemInstanceClass = UGIS_ItemInstance::StaticClass();
}
UGIS_ItemInstance* UGIS_InventoryFactory::CreateItem_Implementation(AActor* Owner, const UGIS_ItemDefinition* ItemDefinition)
{
if (!IsValid(Owner))
{
GIS_LOG(Error, "Missing owner.");
return nullptr;
}
if (ItemDefinition == nullptr)
{
GIS_LOG(Error, "Cannot create Item with null Item Definition.");
return nullptr;
}
TSubclassOf<UGIS_ItemInstance> ItemInstanceClass = DefaultItemInstanceClass.LoadSynchronous();
if (ItemInstanceClass == nullptr)
{
GIS_LOG(Error, "ItemDefinition: %s has invalid InstanceType.", *ItemDefinition->GetName());
return nullptr;
}
UGIS_ItemInstance* Item = NewObject<UGIS_ItemInstance>(Owner, ItemInstanceClass);
if (Item == nullptr)
{
GIS_LOG(Error, "ItemInstanceClass: %s create failed.", *ItemInstanceClass->GetName());
return nullptr;
}
Item->SetItemId(FGuid::NewGuid());
Item->SetDefinition(ItemDefinition);
for (const UGIS_ItemFragment* Fragment : ItemDefinition->Fragments)
{
if (Fragment == nullptr)
{
continue;
}
if (Fragment->GetCompatibleMixinDataType() != nullptr)
{
FInstancedStruct FragmentState;
if (Fragment->MakeDefaultMixinData(FragmentState))
{
if (FragmentState.IsValid() && FragmentState.GetScriptStruct() == Fragment->GetCompatibleMixinDataType())
{
Item->SetFragmentStateByClass(Fragment->GetClass(), FragmentState);
}
}
}
Fragment->OnInstanceCreated(Item);
}
return Item;
}
UGIS_ItemInstance* UGIS_InventoryFactory::DeserializeItem_Implementation(AActor* Owner, const FGIS_ItemRecord& Record)
{
if (!IsValid(Owner))
{
GIS_LOG(Error, "Missing owner.");
return nullptr;
}
const FSoftObjectPath ItemDefinitionAssetPath = FSoftObjectPath(Record.DefinitionAssetPath);
const TSoftObjectPtr<UGIS_ItemDefinition> ItemDefinitionReference = TSoftObjectPtr<UGIS_ItemDefinition>(ItemDefinitionAssetPath);
UGIS_ItemDefinition* ItemDefinition = !ItemDefinitionReference.IsNull() ? ItemDefinitionReference.LoadSynchronous() : nullptr;
if (!IsValid(ItemDefinition))
{
GIS_LOG(Warning, "invalid item definition on path:%s", *ItemDefinitionAssetPath.ToString());
return nullptr;
}
UGIS_ItemInstance* ItemInstance = CreateItem(Owner, ItemDefinition);
if (!IsValid(ItemInstance))
{
GIS_LOG(Warning, "failed to create item instance from definition:%s", *GetNameSafe(ItemDefinition));
return nullptr;
}
ItemInstance->SetItemId(Record.ItemId);
ItemInstance->SetDefinition(ItemDefinition);
TArray<FGIS_Mixin> ConvertedMixins = FGIS_MixinContainer::ConvertRecordsToMixins(Record.FragmentStateRecords);
ConvertedMixins = ConvertedMixins.FilterByPredicate([ItemDefinition](const FGIS_Mixin& Mixin)
{
return ItemDefinition->Fragments.Contains(Mixin.Target);
});
for (const FGIS_Mixin& ConvertedMixin : ConvertedMixins)
{
ItemInstance->SetFragmentStateByClass(ConvertedMixin.Target->GetClass(), ConvertedMixin.Data);
}
FMemoryReader Reader(Record.ByteData);
FObjectAndNameAsStringProxyArchive Ar2(Reader, true);
Ar2.ArIsSaveGame = true;
ItemInstance->Serialize(Ar2);
return ItemInstance;
}
bool UGIS_InventoryFactory::SerializeItem_Implementation(UGIS_ItemInstance* Item, FGIS_ItemRecord& Record)
{
if (!IsValid(Item))
{
GIS_LOG(Error, "Missing item.");
return false;
}
Record.ItemId = Item->GetItemId();
const FSoftObjectPath AssetPath = FSoftObjectPath(Item->GetDefinition());
Record.DefinitionAssetPath = AssetPath.ToString();
Record.FragmentStateRecords = Item->GetFragmentStates().GetSerializableMixinRecords();
FMemoryWriter Writer(Record.ByteData);
FObjectAndNameAsStringProxyArchive Ar(Writer, true);
Ar.ArIsSaveGame = true;
Item->Serialize(Ar);
return true;
}
bool UGIS_InventoryFactory::SerializeCollection_Implementation(UGIS_ItemCollection* Collection, FGIS_CollectionRecord& Record)
{
if (!IsValid(Collection))
{
GIS_LOG(Error, "Missing collection.");
return false;
}
Record.Tag = Collection->GetCollectionTag();
Record.Id = Collection->GetCollectionId();
const FSoftObjectPath AssetPath = FSoftObjectPath(Collection->GetDefinition());
Record.DefinitionAssetPath = AssetPath.ToString();
const TArray<FGIS_ItemStack>& ValidStacks = Collection->GetAllItemStacks().FilterByPredicate([](const FGIS_ItemStack& ItemStack)
{
return ItemStack.IsValidStack();
});
for (const FGIS_ItemStack& Stack : ValidStacks)
{
FGIS_StackRecord StackRecord;
StackRecord.Id = Stack.Id;
StackRecord.CollectionId = Stack.Collection->GetCollectionId();
StackRecord.ItemId = Stack.Item->GetItemId();
StackRecord.Amount = Stack.Amount;
Record.StackRecords.Add(StackRecord);
}
return Record.IsValid();
}
void UGIS_InventoryFactory::DeserializeCollection_Implementation(UGIS_InventorySystemComponent* InventorySystem, const FGIS_CollectionRecord& Record, TMap<FGuid, UGIS_ItemInstance*>& ItemsMap)
{
if (!IsValid(InventorySystem))
{
GIS_LOG(Error, "Missing inventory system.");
return;
}
const FSoftObjectPath DefinitionAssetPath = FSoftObjectPath(Record.DefinitionAssetPath);
const TSoftObjectPtr<UGIS_ItemCollectionDefinition> DefinitionReference = TSoftObjectPtr<UGIS_ItemCollectionDefinition>(DefinitionAssetPath);
UGIS_ItemCollectionDefinition* Definition = DefinitionReference.LoadSynchronous();
if (Definition == nullptr)
{
GIS_LOG(Error, "failed to load definition from path:%s", *DefinitionAssetPath.ToString());
return;
}
UGIS_ItemCollection* NewCollection = CreateCollection(InventorySystem->GetOwner(), Definition);
if (NewCollection == nullptr)
{
GIS_LOG(Error, "failed to create collection from definition:%s", *GetNameSafe(Definition));
return;
}
FGIS_CollectionEntry NewEntry;
NewEntry.Id = Record.Id;
NewEntry.Instance = NewCollection;
NewEntry.Definition = Definition;
InventorySystem->AddCollectionEntry(NewEntry);
for (const FGIS_StackRecord& StackRecord : Record.StackRecords)
{
FGIS_ItemInfo Info;
Info.Item = ItemsMap[StackRecord.ItemId];
Info.Amount = StackRecord.Amount;
Info.ItemCollection = NewCollection;
InventorySystem->AddItem(Info);
}
}
bool UGIS_InventoryFactory::SerializeInventory_Implementation(UGIS_InventorySystemComponent* InventorySystem, FGIS_InventoryRecord& Record)
{
if (!IsValid(InventorySystem))
{
GIS_LOG(Error, "Missing inventory system.");
return false;
}
TArray<FGIS_CollectionRecord> CollectionRecords;
TArray<FGIS_ItemRecord> ItemRecords;
TArray<UGIS_ItemCollection*> Collections = InventorySystem->GetItemCollections();
TArray<UGIS_ItemInstance*> Items;
// build collection records.
for (UGIS_ItemCollection* Collection : Collections)
{
if (Collection->IsInitialized())
{
FGIS_CollectionRecord CollectionRecord;
if (SerializeCollection(Collection, CollectionRecord))
{
CollectionRecords.Add(CollectionRecord);
}
Items.Append(Collection->GetAllItems());
}
}
ItemRecords.Reserve(Items.Num());
for (UGIS_ItemInstance* Item : Items)
{
FGIS_ItemRecord ItemRecord;
if (SerializeItem(Item, ItemRecord))
{
ItemRecords.Add(ItemRecord);
}
}
Record.ItemRecords = ItemRecords;
Record.CollectionRecords = CollectionRecords;
return true;
}
void UGIS_InventoryFactory::DeserializeInventory_Implementation(UGIS_InventorySystemComponent* InventorySystem, const FGIS_InventoryRecord& InRecord)
{
if (!IsValid(InventorySystem))
{
GIS_LOG(Error, "Missing inventory system.");
return;
}
TMap<FGuid, UGIS_ItemInstance*> ItemsMap;
for (const FGIS_ItemRecord& ItemRecord : InRecord.ItemRecords)
{
if (UGIS_ItemInstance* Instance = DeserializeItem(InventorySystem->GetOwner(), ItemRecord))
{
ItemsMap.Emplace(Instance->GetItemId(), Instance);
}
}
for (const FGIS_CollectionRecord& CollectionRecord : InRecord.CollectionRecords)
{
DeserializeCollection(InventorySystem, CollectionRecord, ItemsMap);
}
if (!InventorySystem->IsDefaultCollectionCreated())
{
GIS_OWNED_CLOG(InventorySystem, Warning,
"The default collection definitions is not match with collections restored from inventory record. That may be a problem as you changed the layout of inventory.")
}
}
// TArray<FGIS_ItemFragmentStateRecord> UGIS_InventoryFactory::FilterSerializableFragmentStates(const UGIS_ItemInstance* ItemInstance)
// {
// TArray<FGIS_Mixin> Mixins = ItemInstance->GetFragmentStates().GetSerializableMixins();
// TArray<FGIS_ItemFragmentStateRecord> Records;
// for (const FGIS_Mixin& Mixin : Mixins)
// {
// if (Mixin.Target->IsA<UGIS_ItemFragment>())
// {
// FGIS_ItemFragmentStateRecord Record;
// Record.FragmentClass = Mixin.Target->GetClass();
// Record.FragmentState = Mixin.Data;
// Records.Add(Record);
// }
// }
// return Records;
// }
// TArray<FGIS_ItemFragmentStateRecord> UGIS_InventoryFactory::FilterCompatibleFragmentStateRecords(const UGIS_ItemDefinition* ItemDefinition, const FGIS_ItemRecord& Record)
// {
// TArray<FGIS_Mixin> ConvertedMixins = FGIS_MixinContainer::ConvertRecordsToMixins(Record.FragmentStateRecords);
//
// TArray<FGIS_ItemFragmentStateRecord> CompatibleRecords;
// for (const FGIS_ItemFragmentStateRecord& StateRecord : Record.FragmentStateRecords)
// {
// if (StateRecord.FragmentClass == nullptr || !StateRecord.FragmentState.IsValid())
// {
// GIS_LOG(Warning, "Skip restoring invalid fragment state for item:%s", *ItemDefinition->GetName());
// continue;
// }
// const UGIS_ItemFragment* Fragment = ItemDefinition->GetFragment(StateRecord.FragmentClass);
//
// if (Fragment == nullptr)
// {
// GIS_LOG(Warning, "Skip restoring fragment's state, as fragment(%s) existed in record no longer exists on item(%s).",
// *GetNameSafe(StateRecord.FragmentClass), *ItemDefinition->GetName());
// continue;
// }
//
// if (!Fragment->IsMixinDataSerializable())
// {
// GIS_LOG(Warning, "Skip restoring fragment's state, as fragment(%s) existed in record no longer considered serializable on item(%s).",
// *GetNameSafe(StateRecord.FragmentClass), *ItemDefinition->GetName());
// continue;
// }
//
// if (Fragment->GetCompatibleMixinDataType() != StateRecord.FragmentState.GetScriptStruct())
// {
// GIS_LOG(Warning,
// "Skip restoring fragment's state, as fragment(%s)'s state type(%s) in record no longer compatible with the new type(%s) on item(%s).",
// *GetNameSafe(StateRecord.FragmentClass), *GetNameSafe(StateRecord.FragmentState.GetScriptStruct()), *GetNameSafe(Fragment->GetCompatibleMixinDataType()),
// *ItemDefinition->GetName());
// }
// CompatibleRecords.Add(StateRecord);
// }
// return CompatibleRecords;
// }
#if WITH_EDITOR
EDataValidationResult UGIS_InventoryFactory::IsDataValid(class FDataValidationContext& Context) const
{
if (DefaultItemInstanceClass.IsNull())
{
Context.AddError(FText::FromString(TEXT("Missing Default Item Instance Class")));
return EDataValidationResult::Invalid;
}
return Super::IsDataValid(Context);
}
#endif

View File

@@ -0,0 +1,63 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_InventoryFunctionLibrary.h"
#include "GIS_ItemCollection.h"
#include "Items/GIS_ItemInstance.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_InventoryFunctionLibrary)
TArray<FGIS_ItemDefinitionAmount> UGIS_InventoryFunctionLibrary::MultiplyItemAmounts(const TArray<FGIS_ItemDefinitionAmount>& ItemAmounts, int32 Multiplier)
{
TArray<FGIS_ItemDefinitionAmount> Results;
for (int32 i = 0; i < ItemAmounts.Num(); i++)
{
Results.Add(FGIS_ItemDefinitionAmount(ItemAmounts[i].Definition, ItemAmounts[i].Amount * Multiplier));
}
return Results;
}
TArray<FGIS_CurrencyEntry> UGIS_InventoryFunctionLibrary::MultiplyCurrencies(const TArray<FGIS_CurrencyEntry>& Currencies, float Multiplier)
{
TArray<FGIS_CurrencyEntry> Results;
for (int32 i = 0; i < Currencies.Num(); i++)
{
Results.Add(FGIS_CurrencyEntry(Currencies[i].Definition, Currencies[i].Amount * Multiplier));
}
return Results;
}
TArray<FGIS_ItemInfo> UGIS_InventoryFunctionLibrary::FilterItemInfosByTagQuery(const TArray<FGIS_ItemInfo>& ItemInfos, const FGameplayTagQuery& Query)
{
return ItemInfos.FilterByPredicate([&](const FGIS_ItemInfo& ItemInfo)
{
return ItemInfo.Item != nullptr && ItemInfo.Item->GetItemTags().MatchesQuery(Query);
});
}
TArray<FGIS_ItemStack> UGIS_InventoryFunctionLibrary::FilterItemStacksByTagQuery(const TArray<FGIS_ItemStack>& ItemStacks, const FGameplayTagQuery& TagQuery)
{
return ItemStacks.FilterByPredicate([TagQuery](const FGIS_ItemStack& Stack)
{
return TagQuery.Matches(Stack.Item->GetItemTags());
});
}
TArray<FGIS_ItemStack> UGIS_InventoryFunctionLibrary::FilterItemStacksByDefinition(const TArray<FGIS_ItemStack>& ItemStacks, const UGIS_ItemDefinition* Definition)
{
return ItemStacks.FilterByPredicate([Definition](const FGIS_ItemStack& Stack)
{
return Stack.Item->GetDefinition() == Definition;
});
}
TArray<FGIS_ItemStack> UGIS_InventoryFunctionLibrary::FilterItemStacksByCollectionTags(const TArray<FGIS_ItemStack>& ItemStacks, const FGameplayTagContainer& CollectionTags)
{
return ItemStacks.FilterByPredicate([CollectionTags](const FGIS_ItemStack& Stack)
{
return Stack.Collection->GetCollectionTag().MatchesAnyExact(CollectionTags);
});
}

View File

@@ -0,0 +1,146 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_InventorySubsystem.h"
#include "Engine/World.h"
#include "GIS_InventorySystemSettings.h"
#include "GIS_InventoryFactory.h"
#include "GIS_LogChannels.h"
#include "Items/GIS_ItemDefinition.h"
#include "Items/GIS_ItemInstance.h"
#include "Items/GIS_ItemInterface.h"
#include "Kismet/KismetMathLibrary.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_InventorySubsystem)
UGIS_InventorySubsystem* UGIS_InventorySubsystem::Get(const UObject* WorldContextObject)
{
if (WorldContextObject)
{
return WorldContextObject->GetWorld()->GetGameInstance()->GetSubsystem<UGIS_InventorySubsystem>();
}
return nullptr;
}
void UGIS_InventorySubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
InitializeFactory();
}
void UGIS_InventorySubsystem::Deinitialize()
{
Super::Deinitialize();
Factory = nullptr;
}
UGIS_ItemInstance* UGIS_InventorySubsystem::CreateItem(AActor* Owner, TSoftObjectPtr<UGIS_ItemDefinition> ItemDefinition)
{
if (Factory && !ItemDefinition.IsNull())
{
UGIS_ItemDefinition* LoadedDefinition = ItemDefinition.LoadSynchronous();
if (LoadedDefinition == nullptr)
{
GIS_LOG(Error, "Cannot create Item with invalid Item Definition.");
return nullptr;
}
return Factory->CreateItem(Owner, LoadedDefinition);
}
return nullptr;
}
UGIS_ItemInstance* UGIS_InventorySubsystem::CreateItem(AActor* Owner, const UGIS_ItemDefinition* ItemDefinition)
{
if (Factory && ItemDefinition != nullptr)
{
return Factory->CreateItem(Owner, ItemDefinition);
}
return nullptr;
}
UGIS_ItemInstance* UGIS_InventorySubsystem::DuplicateItem(AActor* Owner, UGIS_ItemInstance* FromItem, bool bGenerateNewId)
{
if (Factory)
{
return Factory->DuplicateItem(Owner, FromItem, bGenerateNewId);
}
return nullptr;
}
bool UGIS_InventorySubsystem::SerializeItem(UGIS_ItemInstance* Item, FGIS_ItemRecord& Record)
{
if (Factory)
{
return Factory->SerializeItem(Item, Record);
}
return false;
}
UGIS_ItemInstance* UGIS_InventorySubsystem::DeserializeItem(AActor* Owner, const FGIS_ItemRecord& Record)
{
if (Factory)
{
return Factory->DeserializeItem(Owner, Record);
}
return nullptr;
}
bool UGIS_InventorySubsystem::SerializeCollection(UGIS_ItemCollection* ItemCollection, FGIS_CollectionRecord& Record)
{
if (Factory)
{
return Factory->SerializeCollection(ItemCollection, Record);
}
return false;
}
void UGIS_InventorySubsystem::DeserializeCollection(UGIS_InventorySystemComponent* InventorySystem, const FGIS_CollectionRecord& Record, TMap<FGuid, UGIS_ItemInstance*>& ItemsMap)
{
if (Factory)
{
return Factory->DeserializeCollection(InventorySystem, Record, ItemsMap);
}
}
bool UGIS_InventorySubsystem::SerializeInventory(UGIS_InventorySystemComponent* InventorySystem, FGIS_InventoryRecord& Record)
{
if (Factory)
{
return Factory->SerializeInventory(InventorySystem, Record);
}
return false;
}
void UGIS_InventorySubsystem::DeserializeInventory(UGIS_InventorySystemComponent* InventorySystem, const FGIS_InventoryRecord& Record)
{
if (Factory)
{
return Factory->DeserializeInventory(InventorySystem, Record);
}
}
void UGIS_InventorySubsystem::InitializeFactory()
{
if (UGIS_InventorySystemSettings::Get() == nullptr || UGIS_InventorySystemSettings::Get()->InventoryFactoryClass.IsNull())
{
GIS_LOG(Error, "Missing ItemFactoryClass in inventory system settings.");
return;
}
const UClass* FactoryClass = UGIS_InventorySystemSettings::Get()->InventoryFactoryClass.LoadSynchronous();
if (FactoryClass == nullptr)
{
GIS_LOG(Error, "invalid ItemFactoryClass found inventory system settings.");
return;
}
UGIS_InventoryFactory* TempFactory = NewObject<UGIS_InventoryFactory>(this, FactoryClass);
if (TempFactory == nullptr)
{
GIS_LOG(Error, "Failed to create item factory instance.Class:%s", *FactoryClass->GetName());
return;
}
Factory = TempFactory;
}

View File

@@ -0,0 +1,55 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_InventorySystemSettings.h"
#include "GIS_InventoryFactory.h"
#include "Items/GIS_ItemDefinitionSchema.h"
#include "Misc/DataValidation.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_InventorySystemSettings)
UGIS_InventorySystemSettings::UGIS_InventorySystemSettings()
{
InventoryFactoryClass = UGIS_InventoryFactory::StaticClass();
}
FName UGIS_InventorySystemSettings::GetCategoryName() const
{
return TEXT("Game");
}
const UGIS_InventorySystemSettings* UGIS_InventorySystemSettings::Get()
{
return GetDefault<UGIS_InventorySystemSettings>();
}
const UGIS_ItemDefinitionSchema* UGIS_InventorySystemSettings::GetItemDefinitionSchemaForAsset(const FString& AssetPath) const
{
// Check path-specific schemas first
for (const FGIS_ItemDefinitionSchemaEntry& Entry : ItemDefinitionSchemaMap)
{
if (!Entry.PathPrefix.IsEmpty() && AssetPath.StartsWith(Entry.PathPrefix))
{
if (Entry.Schema.IsValid())
{
if (UGIS_ItemDefinitionSchema* Schema = Cast<UGIS_ItemDefinitionSchema>(Entry.Schema.TryLoad()))
{
UE_LOG(LogTemp, Log, TEXT("Using path-specific schema %s for asset %s"), *Entry.Schema.ToString(), *AssetPath);
return Schema;
}
}
}
}
// Fall back to default schema
if (DefaultItemDefinitionSchema.IsValid())
{
if (UGIS_ItemDefinitionSchema* Schema = Cast<UGIS_ItemDefinitionSchema>(DefaultItemDefinitionSchema.TryLoad()))
{
UE_LOG(LogTemp, Log, TEXT("Using default schema %s for asset %s"), *DefaultItemDefinitionSchema.ToString(), *AssetPath);
return Schema;
}
}
UE_LOG(LogTemp, Warning, TEXT("No valid schema found for asset %s"), *AssetPath);
return nullptr;
}

View File

@@ -0,0 +1,40 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_InventoryTags.h"
namespace GIS_CollectionTags
{
UE_DEFINE_GAMEPLAY_TAG(Main, "GIS.Collection.Main");
UE_DEFINE_GAMEPLAY_TAG(Equipped, "GIS.Collection.Equipped");
UE_DEFINE_GAMEPLAY_TAG(Hidden, "GIS.Collection.Hidden");
UE_DEFINE_GAMEPLAY_TAG(QuickBar, "GIS.Collection.QuickBar");
}
namespace GIS_InventoryInitState
{
UE_DEFINE_GAMEPLAY_TAG_COMMENT(Spawned, "GIS.InitState.Spawned", "1: Actor/component has initially spawned and can be extended");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(DataAvailable, "GIS.InitState.DataAvailable", "2: All required data has been loaded/replicated and is ready for initialization");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(DataInitialized, "GIS.InitState.DataInitialized", "3: The available data has been initialized for this actor/component, but it is not ready for full gameplay");
UE_DEFINE_GAMEPLAY_TAG_COMMENT(GameplayReady, "GIS.InitState.GameplayReady", "4: The actor/component is fully ready for active gameplay");
}
// namespace GIS_MessageTags
// {
// UE_DEFINE_GAMEPLAY_TAG(ItemStackUpdate, "GIS.Message.StackUpdate")
// UE_DEFINE_GAMEPLAY_TAG(InventoryUpdate, "GIS.Message.Inventory.Update")
// UE_DEFINE_GAMEPLAY_TAG(CollectionUpdate, "GIS.Message.Inventory.Collection.Update")
// UE_DEFINE_GAMEPLAY_TAG(InventoryAddItemInfo, "GIS.Message.Inventory.AddItemInfo")
// UE_DEFINE_GAMEPLAY_TAG(InventoryAddItemInfoRejected, "GIS.Message.Inventory.AddItemInfo.Rejected")
// UE_DEFINE_GAMEPLAY_TAG(InventoryRemoveItemInfo, "GIS.Message.Inventory.RemoveItemInfo")
// UE_DEFINE_GAMEPLAY_TAG(QuickBarSlotsChanged, "GIS.Message.QuickBar.SlotsChanged")
// UE_DEFINE_GAMEPLAY_TAG(QuickBarActiveIndexChanged, "GIS.Message.QuickBar.ActiveIndexChanged")
// }
namespace GIS_AttributeTags
{
UE_DEFINE_GAMEPLAY_TAG(Dummy, "GIS.Attribute.Dummy");
// UE_DEFINE_GAMEPLAY_TAG(EnhancedLevel, "GIS.Attribute.EnhancedLevel");
// UE_DEFINE_GAMEPLAY_TAG(MaxEnhancedLevel, "GIS.Attribute.MaxEnhancedLevel");
UE_DEFINE_GAMEPLAY_TAG(StackSizeLimit, "GIS.Attribute.StackSizeLimit");
}

View File

@@ -0,0 +1,77 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_LogChannels.h"
#include "GIS_EquipmentInstance.h"
#include "UObject/Object.h"
#include "GameFramework/Actor.h"
#include "Components/ActorComponent.h"
#include "GIS_ItemInstance.h"
#include "GIS_ItemCollection.h"
#include "GIS_ItemDefinition.h"
DEFINE_LOG_CATEGORY(LogGIS)
FString GetGISLogContextString(const UObject* ContextObject)
{
ENetRole Role = ROLE_None;
FString RoleName = TEXT("None");
FString Name = "None";
if (const AActor* Actor = Cast<AActor>(ContextObject))
{
Role = Actor->GetLocalRole();
Name = Actor->GetName();
}
else if (const UActorComponent* Component = Cast<UActorComponent>(ContextObject))
{
if (AActor* ActorOwner = Cast<AActor>(Component->GetOuter()))
{
Role = ActorOwner->GetLocalRole();
Name = ActorOwner->GetName();
}
else
{
const AActor* Owner = Component->GetOwner();
Role = IsValid(Owner) ? Owner->GetLocalRole() : ROLE_None;
Name = IsValid(Owner) ? Owner->GetName() : TEXT("None");
}
}
else if (const UGIS_ItemInstance* ItemInstance = Cast<UGIS_ItemInstance>(ContextObject))
{
if (AActor* ActorOwner = Cast<AActor>(ItemInstance->GetOuter()))
{
Role = ActorOwner->GetLocalRole();
Name = ActorOwner->GetName();
}
else
{
return FString::Printf(TEXT("(%s)'s instance(%s) "), *ItemInstance->GetDefinition()->GetName(), *ItemInstance->GetName());
}
}
else if (const UGIS_ItemCollection* Collection = Cast<UGIS_ItemCollection>(ContextObject))
{
if (AActor* ActorOwner = Cast<AActor>(Collection->GetOuter()))
{
Role = ActorOwner->GetLocalRole();
Name = ActorOwner->GetName();
}
if (Role != ROLE_None)
{
RoleName = (Role == ROLE_Authority) ? TEXT("Server") : TEXT("Client");
}
return FString::Printf(TEXT("[%s] (%s)'s %s"), *RoleName, *Name, *Collection->GetCollectionName());
}
else if (IsValid(ContextObject))
{
Name = ContextObject->GetName();
}
if (Role != ROLE_None)
{
RoleName = (Role == ROLE_Authority) ? TEXT("Server") : TEXT("Client");
}
return FString::Printf(TEXT("[%s] (%s)"), *RoleName, *Name);
}

View File

@@ -0,0 +1,22 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GenericInventorySystem.h"
#define LOCTEXT_NAMESPACE "FGenericInventorySystemModule"
void FGenericInventorySystemModule::StartupModule()
{
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
}
void FGenericInventorySystemModule::ShutdownModule()
{
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
// we call this function before unloading the module.
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FGenericInventorySystemModule, GenericInventorySystem)

View File

@@ -0,0 +1,53 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Pickups/GIS_CurrencyPickupComponent.h"
#include "GIS_CurrencySystemComponent.h"
#include "GameFramework/Actor.h"
#include "GIS_InventorySystemComponent.h"
#include "GIS_LogChannels.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_CurrencyPickupComponent)
void UGIS_CurrencyPickupComponent::BeginPlay()
{
OwningCurrencySystem = UGIS_CurrencySystemComponent::GetCurrencySystemComponent(GetOwner());
if (OwningCurrencySystem == nullptr)
{
GIS_CLOG(Warning, "Mising CurrencySystemComponent!");
}
Super::BeginPlay();
}
bool UGIS_CurrencyPickupComponent::Pickup(UGIS_InventorySystemComponent* Picker)
{
if (!GetOwner()->HasAuthority())
{
GIS_CLOG(Warning, "has no authority!");
return false;
}
if (OwningCurrencySystem == nullptr || !IsValid(OwningCurrencySystem))
{
GIS_CLOG(Warning, "mising CurrencySystemComponent!");
return false;
}
if (Picker == nullptr || !IsValid(Picker))
{
GIS_CLOG(Warning, "passed-in invalid picker.");
return false;
}
UGIS_CurrencySystemComponent* PickerCurrencySystem = Picker->GetCurrencySystem();
if (PickerCurrencySystem == nullptr)
{
GIS_CLOG(Warning, "Picker:%s has no CurrencySystem!", *Picker->GetOwner()->GetName());
return false;
}
if (PickerCurrencySystem->AddCurrencies(OwningCurrencySystem->GetAllCurrencies()))
{
OwningCurrencySystem->EmptyCurrencies();
return true;
}
return false;
}

View File

@@ -0,0 +1,91 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Pickups/GIS_InventoryPickupComponent.h"
#include "GameFramework/Actor.h"
#include "GIS_InventorySystemComponent.h"
#include "GIS_InventoryTags.h"
#include "GIS_ItemCollection.h"
#include "GIS_LogChannels.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_InventoryPickupComponent)
void UGIS_InventoryPickupComponent::BeginPlay()
{
if (!CollectionTag.IsValid())
{
CollectionTag = GIS_CollectionTags::Main;
}
Inventory = UGIS_InventorySystemComponent::FindInventorySystemComponent(GetOwner());
if (!Inventory)
{
GIS_CLOG(Warning, "InventoryPickup requries an inventory system component on the same actor!")
}
Super::BeginPlay();
}
bool UGIS_InventoryPickupComponent::Pickup(UGIS_InventorySystemComponent* Picker)
{
if (!GetOwner()->HasAuthority())
{
GIS_CLOG(Warning, "has no authority!");
return false;
}
if (Inventory == nullptr || !IsValid(Inventory))
{
GIS_CLOG(Warning, "doesn't have an inventory system component to function.")
return false;
}
if (!CollectionTag.IsValid() || !IsValid(Picker))
{
GIS_CLOG(Warning, "doesn't have valid picker to function.")
return false;
}
UGIS_ItemCollection* DestCollection = Picker->GetCollectionByTag(CollectionTag);
if (DestCollection == nullptr)
{
GIS_CLOG(Warning, "picker(%s) doesn't have valid collection named:%s", *Picker->GetOwner()->GetName(), *CollectionTag.ToString());
return false;
}
return AddPickupToCollection(DestCollection);
}
UGIS_InventorySystemComponent* UGIS_InventoryPickupComponent::GetOwningInventory() const
{
return Inventory;
}
bool UGIS_InventoryPickupComponent::AddPickupToCollection(UGIS_ItemCollection* DestCollection)
{
TArray<FGIS_ItemInfo> PickupItems = Inventory->GetDefaultCollection()->GetAllItemInfos();
bool bAtLeastOneCanBeAdded = false;
for (int32 i = 0; i < PickupItems.Num(); i++)
{
FGIS_ItemInfo ItemInfo = PickupItems[i];
FGIS_ItemInfo CanAddedItemInfo;
if (DestCollection->CanAddItem(ItemInfo, CanAddedItemInfo))
{
if (CanAddedItemInfo.Amount != 0)
{
bAtLeastOneCanBeAdded = true;
}
}
}
if (bAtLeastOneCanBeAdded == false)
{
NotifyPickupFailed();
return false;
}
for (int32 i = 0; i < PickupItems.Num(); i++)
{
DestCollection->AddItem(PickupItems[i]);
}
Inventory->GetDefaultCollection()->RemoveAll();
NotifyPickupSuccess();
return true;
}

View File

@@ -0,0 +1,87 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Pickups/GIS_ItemPickupComponent.h"
#include "GameFramework/Actor.h"
#include "GIS_InventoryTags.h"
#include "GIS_InventorySystemComponent.h"
#include "GIS_ItemCollection.h"
#include "Items/GIS_ItemInstance.h"
#include "GIS_LogChannels.h"
#include "Pickups/GIS_WorldItemComponent.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_ItemPickupComponent)
bool UGIS_ItemPickupComponent::Pickup(UGIS_InventorySystemComponent* Picker)
{
if (!GetOwner()->HasAuthority())
{
GIS_CLOG(Warning, "has no authority!");
return false;
}
if (!CollectionTag.IsValid() || !IsValid(Picker))
{
GIS_CLOG(Warning, "passed-in invalid picker.");
return false;
}
if (WorldItemComponent == nullptr && WorldItemComponent->GetItemInstance()->IsItemValid())
{
GIS_CLOG(Warning, "doesn't have valid WordItem component attached or it has invalid item instance reference.");
return false;
}
return TryAddToCollection(Picker);
}
UGIS_WorldItemComponent* UGIS_ItemPickupComponent::GetWorldItem() const
{
return WorldItemComponent;
}
// Called when the game starts
void UGIS_ItemPickupComponent::BeginPlay()
{
if (!CollectionTag.IsValid())
{
CollectionTag = GIS_CollectionTags::Main;
}
Super::BeginPlay();
WorldItemComponent = GetOwner()->FindComponentByClass<UGIS_WorldItemComponent>();
if (WorldItemComponent == nullptr)
{
GIS_CLOG(Error, "requires GIS_WorldItemComponent to function!")
}
}
bool UGIS_ItemPickupComponent::TryAddToCollection(UGIS_InventorySystemComponent* Picker)
{
UGIS_ItemInstance* NewItemInstance = WorldItemComponent->GetDuplicatedItemInstance(Picker->GetOwner());
if (NewItemInstance == nullptr)
{
GIS_CLOG(Error, "referenced invalid item! Pickup failed!");
NotifyPickupFailed();
return false;
}
FGIS_ItemInfo NewItemInfo;
NewItemInfo.Item = NewItemInstance;
NewItemInfo.Amount = WorldItemComponent->GetItemAmount();
const FGameplayTag TargetCollection = CollectionTag.IsValid() ? CollectionTag : GIS_CollectionTags::Main;
NewItemInfo.CollectionTag = TargetCollection;
FGIS_ItemInfo CanAddedItemInfo;
const bool bResult = Picker->CanAddItem(NewItemInfo, CanAddedItemInfo);
if (!bResult || CanAddedItemInfo.Amount == 0 || (bFailIfFullAmountNotFit && CanAddedItemInfo.Amount != NewItemInfo.Amount))
{
NotifyPickupFailed();
return false;
}
Picker->AddItem(NewItemInfo);
NotifyPickupSuccess();
return true;
}

View File

@@ -0,0 +1,6 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Pickups/GIS_PickupActorInterface.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_PickupActorInterface)

View File

@@ -0,0 +1,31 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Pickups/GIS_PickupComponent.h"
#include "Sound/SoundBase.h"
#include "Kismet/GameplayStatics.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_PickupComponent)
// Sets default values for this component's properties
UGIS_PickupComponent::UGIS_PickupComponent()
{
PrimaryComponentTick.bStartWithTickEnabled = false;
PrimaryComponentTick.bCanEverTick = false;
SetIsReplicatedByDefault(true);
}
bool UGIS_PickupComponent::Pickup(UGIS_InventorySystemComponent* Picker)
{
return true;
}
void UGIS_PickupComponent::NotifyPickupSuccess()
{
OnPickupSuccess.Broadcast();
}
void UGIS_PickupComponent::NotifyPickupFailed()
{
OnPickupFail.Broadcast();
}

View File

@@ -0,0 +1,154 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Pickups/GIS_WorldItemComponent.h"
#include "UObject/Object.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
#include "GIS_InventorySubsystem.h"
#include "GIS_InventorySystemComponent.h"
#include "Items/GIS_ItemDefinition.h"
#include "Items/GIS_ItemInstance.h"
#include "GIS_LogChannels.h"
#include "Net/UnrealNetwork.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_WorldItemComponent)
UGIS_WorldItemComponent::UGIS_WorldItemComponent(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
PrimaryComponentTick.bStartWithTickEnabled = false;
PrimaryComponentTick.bCanEverTick = false;
SetIsReplicatedByDefault(true);
bReplicateUsingRegisteredSubObjectList = true;
}
void UGIS_WorldItemComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
FDoRepLifetimeParams Parameters;
Parameters.bIsPushBased = true;
DOREPLIFETIME_WITH_PARAMS_FAST(ThisClass, ItemInfo, Parameters)
}
UGIS_WorldItemComponent* UGIS_WorldItemComponent::GetWorldItemComponent(const AActor* Actor)
{
if (IsValid(Actor))
{
return Actor->FindComponentByClass<UGIS_WorldItemComponent>();
}
return nullptr;
}
void UGIS_WorldItemComponent::CreateItemFromDefinition(FGIS_ItemDefinitionAmount ItemDefinition)
{
if (!ItemDefinition.Definition.IsNull() && ItemDefinition.Amount >= 1)
{
if (!ItemInfo.IsValid())
{
UGIS_ItemInstance* NewItemInstance = UGIS_InventorySubsystem::Get(GetWorld())->CreateItem(GetOwner(), ItemDefinition.Definition.LoadSynchronous());
if (NewItemInstance == nullptr)
{
GIS_CLOG(Error, "failed to create item instance from definition!");
}
else
{
SetItemInfo(NewItemInstance, ItemDefinition.Amount);
}
}
else
{
GIS_CLOG(Warning, "Already have valid item info, skip creation.")
}
}
else
{
GIS_CLOG(Error, "passed invalid definition setup,skip item instance creating!");
}
}
bool UGIS_WorldItemComponent::HasValidDefinition() const
{
return !Definition.Definition.IsNull() && Definition.Amount >= 1;
}
void UGIS_WorldItemComponent::SetItemInfo(UGIS_ItemInstance* InItem, int32 InAmount)
{
if (InItem == nullptr || InAmount <= 0)
{
return;
}
if (ItemInfo.IsValid())
{
GIS_CLOG(Warning, "Already have valid item info.")
return;
}
ItemInfo = FGIS_ItemInfo(InItem, InAmount);
MARK_PROPERTY_DIRTY_FROM_NAME(ThisClass, ItemInfo, this)
// add to ReplicatedSubObject list only if it was created by this component.
if (bReplicateUsingRegisteredSubObjectList && InItem->GetOuter() == GetOwner())
{
AddReplicatedSubObject(ItemInfo.Item);
}
}
void UGIS_WorldItemComponent::ResetItemInfo()
{
if (ItemInfo.IsValid())
{
// remove from replicated sub object list only if it was created by this component.
if (bReplicateUsingRegisteredSubObjectList && ItemInfo.Item->GetOuter() == GetOwner())
{
RemoveReplicatedSubObject(ItemInfo.Item);
}
}
}
UGIS_ItemInstance* UGIS_WorldItemComponent::GetItemInstance()
{
return ItemInfo.Item;
}
UGIS_ItemInstance* UGIS_WorldItemComponent::GetDuplicatedItemInstance(AActor* NewOwner)
{
if (ItemInfo.IsValid())
{
return UGIS_InventorySubsystem::Get(GetWorld())->DuplicateItem(NewOwner, ItemInfo.Item);
}
return nullptr;
}
FGIS_ItemInfo UGIS_WorldItemComponent::GetItemInfo() const
{
return ItemInfo;
}
int32 UGIS_WorldItemComponent::GetItemAmount() const
{
return ItemInfo.Amount;
}
void UGIS_WorldItemComponent::BeginPlay()
{
if (HasValidDefinition() && GetOwner()->HasAuthority())
{
CreateItemFromDefinition(Definition);
}
Super::BeginPlay();
}
void UGIS_WorldItemComponent::OnRep_ItemInfo()
{
if (ItemInfo.IsValid())
{
GIS_CLOG(Verbose, "item:%s replicated!", *ItemInfo.Item->GetDefinition()->GetName());
ItemInfoSetEvent.Broadcast(ItemInfo);
}
}

View File

@@ -0,0 +1,42 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "GIS_SerializationStructLibrary.h"
#include "GIS_ItemFragment.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_SerializationStructLibrary)
bool FGIS_ItemRecord::operator==(const FGIS_ItemRecord& Other) const
{
return ItemId == Other.ItemId && DefinitionAssetPath == Other.DefinitionAssetPath;
}
bool FGIS_ItemRecord::IsValid() const
{
return ItemId.IsValid() && !DefinitionAssetPath.IsEmpty();
}
// bool FGIS_ItemFragmentStateRecord::operator==(const FGIS_ItemFragmentStateRecord& Other) const
// {
// return FragmentClass == Other.FragmentClass;
// }
//
// bool FGIS_ItemFragmentStateRecord::IsValid() const
// {
// return FragmentClass != nullptr && FragmentState.IsValid();
// }
bool FGIS_StackRecord::IsValid() const
{
return ItemId.IsValid() && Id.IsValid() && CollectionId.IsValid();
}
bool FGIS_CollectionRecord::IsValid() const
{
return Id.IsValid() && !DefinitionAssetPath.IsEmpty();
}
FGIS_CurrencyRecord::FGIS_CurrencyRecord()
{
Key = NAME_None;
}