第一次提交

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,8 @@
[CoreRedirects]
;GIS 1.1 migration.
+FunctionRedirects = (OldName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.ServerCycleGroupActiveIndex",NewName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.ServerCycleGroupActiveSlot")
+FunctionRedirects = (OldName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.CycleGroupActiveIndex",NewName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.CycleGroupActiveSlot")
+FunctionRedirects = (OldName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.ServerSetGroupActiveIndex",NewName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.ServerSetGroupActiveSlot")
+FunctionRedirects = (OldName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.SetGroupActiveIndex",NewName="/Script/GenericInventorySystem.GIS_EquipmentSystemComponent.SetGroupActiveSlot")

View File

@@ -0,0 +1,9 @@
[FilterPlugin]
; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and
; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively.
;
; Examples:
; /README.txt
; /Extras/...
; /Binaries/ThirdParty/*.dll
/Config/*

View File

@@ -0,0 +1,42 @@
{
"FileVersion": 3,
"Version": 3,
"VersionName": "1.1.1",
"FriendlyName": "GenericInventorySystem",
"Description": "Advanced and Flexible Inventory framework.",
"Category": "Gameplay",
"CreatedBy": "YuewuDev",
"CreatedByURL": "https://yuewu.dev/en",
"DocsURL": "https://www.yuewu.dev/en/wiki",
"MarketplaceURL": "com.epicgames.launcher://ue/Fab/product/9a9b6f10-4d4c-4897-90ec-809854653402",
"SupportURL": "https://discord.com/invite/xMRXAB2",
"EngineVersion": "5.7.0",
"CanContainContent": false,
"Installed": true,
"Modules": [
{
"Name": "GenericInventorySystem",
"Type": "Runtime",
"LoadingPhase": "Default",
"PlatformAllowList": [
"Win64",
"Android",
"Linux"
]
},
{
"Name": "GenericInventoryEditor",
"Type": "UncookedOnly",
"LoadingPhase": "PreDefault",
"PlatformAllowList": [
"Win64"
]
}
],
"Plugins": [
{
"Name": "ModularGameplay",
"Enabled": true
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,27 @@
using UnrealBuildTool;
public class GenericInventoryEditor : ModuleRules
{
public GenericInventoryEditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"GenericInventorySystem",
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"UnrealEd",
"AssetTools",
}
);
}
}

View File

@@ -0,0 +1,49 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Factories/GIS_AssetTypeActions.h"
#include "GenericInventoryEditor.h"
#include "GIS_CurrencyDefinition.h"
#include "GIS_ItemCollection.h"
#include "GIS_ItemDefinition.h"
#include "GIS_ItemDefinitionSchema.h"
#include "GIS_ItemMultiStackCollection.h"
#include "GIS_ItemSlotCollection.h"
uint32 FGIS_AssetTypeAction::GetCategories()
{
return FGenericInventoryEditorModule::GetAssetsCategory();
}
FColor FGIS_AssetTypeAction::GetTypeColor() const
{
return FColor(114, 40, 199);
}
#define IMPLEMENT_GIS_ASSET_ACTION(ActionName, NameText, DescText) \
FText FGIS_AssetTypeAction_##ActionName::GetName() const \
{ \
return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_" #ActionName "_Name", NameText); \
} \
FText FGIS_AssetTypeAction_##ActionName::GetAssetDescription(const FAssetData& AssetData) const \
{ \
return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_" #ActionName "_Description", DescText); \
} \
UClass* FGIS_AssetTypeAction_##ActionName::GetSupportedClass() const \
{ \
return UGIS_##ActionName::StaticClass(); \
}
IMPLEMENT_GIS_ASSET_ACTION(ItemDefinition, "Item Definition",
"Data Asset that defines your item design.")
IMPLEMENT_GIS_ASSET_ACTION(ItemDefinitionSchema, "Item Definition Schema",
"Data Asset that defines the validation rules for item definition.")
IMPLEMENT_GIS_ASSET_ACTION(ItemCollectionDefinition, "Item Collection Definition(Normal)",
"Data Asset that defines the design for normal item collection.")
IMPLEMENT_GIS_ASSET_ACTION(ItemSlotCollectionDefinition, "Item Collection Definition(Slot)",
"Data Asset that defines the design for slot based item collection.")
IMPLEMENT_GIS_ASSET_ACTION(ItemMultiStackCollectionDefinition, "Item Collection Definition(MultiStack)",
"Data Asset that defines the design for multi stack based item collection.")
IMPLEMENT_GIS_ASSET_ACTION(CurrencyDefinition, "CurrencyDefinition",
"Data Asset that defines the in game currency.")

View File

@@ -0,0 +1,50 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#include "Factories/GIS_DataAssetsFactories.h"
#include "GIS_ItemCollection.h"
#include "GIS_ItemDefinition.h"
#include "GIS_ItemDefinitionSchema.h"
#include "GIS_ItemMultiStackCollection.h"
#include "GIS_ItemSlotCollection.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(GIS_DataAssetsFactories)
UGIS_Factory::UGIS_Factory(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
bEditAfterNew = true;
bCreateNew = true;
}
uint32 UGIS_Factory::GetMenuCategories() const
{
return Super::GetMenuCategories();
}
const TArray<FText>& UGIS_Factory::GetMenuCategorySubMenus() const
{
return Super::GetMenuCategorySubMenus();
}
#define IMPLEMENT_GIS_FACTORY(FactoryName) \
UGIS_Factory_##FactoryName::UGIS_Factory_##FactoryName(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) \
{ \
SupportedClass = UGIS_##FactoryName::StaticClass(); \
} \
UObject* UGIS_Factory_##FactoryName::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) \
{ \
check(Class->IsChildOf(UGIS_##FactoryName::StaticClass())); \
return NewObject<UGIS_##FactoryName>(InParent, Class, Name, Flags | RF_Transactional, Context); \
}
IMPLEMENT_GIS_FACTORY(ItemDefinition)
IMPLEMENT_GIS_FACTORY(ItemDefinitionSchema)
IMPLEMENT_GIS_FACTORY(ItemCollectionDefinition)
IMPLEMENT_GIS_FACTORY(ItemSlotCollectionDefinition)
IMPLEMENT_GIS_FACTORY(ItemMultiStackCollectionDefinition)

View File

@@ -0,0 +1,42 @@
#include "GenericInventoryEditor.h"
#include "Factories/GIS_AssetTypeActions.h"
#define LOCTEXT_NAMESPACE "FGenericInventoryEditorModule"
TArray<TSharedPtr<IAssetTypeActions>> FGenericInventoryEditorModule::AssetTypeActions = {
MakeShared<FGIS_AssetTypeAction_ItemDefinition>(),
MakeShared<FGIS_AssetTypeAction_ItemDefinitionSchema>(),
MakeShared<FGIS_AssetTypeAction_ItemCollectionDefinition>(),
MakeShared<FGIS_AssetTypeAction_ItemSlotCollectionDefinition>(),
MakeShared<FGIS_AssetTypeAction_ItemMultiStackCollectionDefinition>()
};
EAssetTypeCategories::Type FGenericInventoryEditorModule::AssetsCategory;
void FGenericInventoryEditorModule::StartupModule()
{
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
AssetsCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("GenericInventorySystem")), LOCTEXT("GIS_AssetsCategory", "Generic Inventory System"));
for (TSharedPtr<IAssetTypeActions>& Action : AssetTypeActions)
{
AssetTools.RegisterAssetTypeActions(Action.ToSharedRef());
}
}
void FGenericInventoryEditorModule::ShutdownModule()
{
if (const FAssetToolsModule* AssetTools = FModuleManager::GetModulePtr<FAssetToolsModule>("AssetTools"))
{
for (TSharedPtr<IAssetTypeActions>& Action : AssetTypeActions)
{
AssetTools->Get().UnregisterAssetTypeActions(Action.ToSharedRef());
}
}
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FGenericInventoryEditorModule, GenericInventoryEditor)

View File

@@ -0,0 +1,33 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "AssetTypeActions/AssetTypeActions_DataAsset.h"
class FGIS_AssetTypeAction : public FAssetTypeActions_DataAsset
{
public:
virtual uint32 GetCategories() override;
virtual FColor GetTypeColor() const override;
};
#define DEFINE_GIS_ASSET_ACTION(ActionName) \
class FGIS_AssetTypeAction_##ActionName final : public FGIS_AssetTypeAction \
{ \
public: \
virtual FText GetName() const override; \
virtual FText GetAssetDescription(const FAssetData& AssetData) const override; \
virtual UClass* GetSupportedClass() const override; \
};
DEFINE_GIS_ASSET_ACTION(ItemDefinition)
DEFINE_GIS_ASSET_ACTION(ItemDefinitionSchema)
DEFINE_GIS_ASSET_ACTION(ItemCollectionDefinition)
DEFINE_GIS_ASSET_ACTION(ItemSlotCollectionDefinition)
DEFINE_GIS_ASSET_ACTION(ItemMultiStackCollectionDefinition)
DEFINE_GIS_ASSET_ACTION(CurrencyDefinition)

View File

@@ -0,0 +1,69 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "UObject/ObjectMacros.h"
#include "Templates/SubclassOf.h"
#include "Factories/Factory.h"
#include "GIS_DataAssetsFactories.generated.h"
UCLASS(Abstract)
class UGIS_Factory : public UFactory
{
GENERATED_BODY()
public:
UGIS_Factory(const FObjectInitializer& ObjectInitializer);
virtual uint32 GetMenuCategories() const override;
virtual const TArray<FText>& GetMenuCategorySubMenus() const override;
};
UCLASS()
class UGIS_Factory_ItemDefinition : public UGIS_Factory
{
GENERATED_BODY()
public:
UGIS_Factory_ItemDefinition(const FObjectInitializer& ObjectInitializer);
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
};
UCLASS()
class UGIS_Factory_ItemDefinitionSchema : public UGIS_Factory
{
GENERATED_BODY()
public:
UGIS_Factory_ItemDefinitionSchema(const FObjectInitializer& ObjectInitializer);
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
};
UCLASS()
class UGIS_Factory_ItemCollectionDefinition : public UGIS_Factory
{
GENERATED_BODY()
public:
UGIS_Factory_ItemCollectionDefinition(const FObjectInitializer& ObjectInitializer);
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
};
UCLASS()
class UGIS_Factory_ItemSlotCollectionDefinition : public UGIS_Factory
{
GENERATED_BODY()
public:
UGIS_Factory_ItemSlotCollectionDefinition(const FObjectInitializer& ObjectInitializer);
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
};
UCLASS()
class UGIS_Factory_ItemMultiStackCollectionDefinition : public UGIS_Factory
{
GENERATED_BODY()
public:
UGIS_Factory_ItemMultiStackCollectionDefinition(const FObjectInitializer& ObjectInitializer);
virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
};

View File

@@ -0,0 +1,23 @@
#pragma once
#include "CoreMinimal.h"
#include "AssetTypeCategories.h"
#include "Modules/ModuleManager.h"
class IAssetTypeActions;
class FGenericInventoryEditorModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
static EAssetTypeCategories::Type GetAssetsCategory()
{
return AssetsCategory;
}
private:
static TArray<TSharedPtr<IAssetTypeActions>> AssetTypeActions;
static EAssetTypeCategories::Type AssetsCategory;
};

View File

@@ -0,0 +1,73 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
using System.IO;
using UnrealBuildTool;
public class GenericInventorySystem : ModuleRules
{
public GenericInventorySystem(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
CppCompileWarningSettings.NonInlinedGenCppWarningLevel = WarningLevel.Warning;
PublicIncludePaths.AddRange(
new[]
{
Path.Combine(ModuleDirectory, "Public/Core"),
Path.Combine(ModuleDirectory, "Public/Core/Items"),
Path.Combine(ModuleDirectory, "Public/Core/Attributes"),
Path.Combine(ModuleDirectory, "Public/Core/Collections"),
Path.Combine(ModuleDirectory, "Public/Core/Fragments"),
Path.Combine(ModuleDirectory, "Public/Equipping"),
Path.Combine(ModuleDirectory, "Public/Crafting"),
Path.Combine(ModuleDirectory, "Public/Exchange"),
Path.Combine(ModuleDirectory, "Public/Serialization")
// ... add public include paths required here ...
}
);
PrivateIncludePaths.AddRange(
new string[]
{
// ... add other private include paths required here ...
}
);
PublicDependencyModuleNames.AddRange(
new[]
{
"Core",
"GameplayTags",
"DeveloperSettings",
"ModularGameplay"
// ... add other public dependencies that you statically link with here ...
}
);
PrivateDependencyModuleNames.AddRange(
new[]
{
"CoreUObject",
"Engine",
"Slate",
"UMG",
"SlateCore",
"NetCore",
"AssetRegistry"
// ... add private dependencies that you statically link with here ...
}
);
SetupIrisSupport(Target);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
// ... add any modules that your module loads dynamically here ...
}
);
}
}

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

View File

@@ -0,0 +1,239 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GIS_LogChannels.h"
#include "Engine/TimerHandle.h"
#include "Engine/Engine.h"
#include "UObject/Object.h"
#include "Engine/CancellableAsyncAction.h"
#include "GIS_AsyncAction_Wait.generated.h"
/**
* Delegate triggered when an async wait action completes or is cancelled.
* 异步等待动作完成或取消时触发的委托。
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FGIS_AsyncAction_WaitSignature);
/**
* Base class for asynchronous wait actions on an actor.
* 在演员上执行异步等待动作的基类。
* @details Provides functionality to wait for specific conditions on an actor, with timer-based checks.
* @细节 提供在演员上等待特定条件的功能,通过定时器检查。
*/
UCLASS(Abstract)
class GENERICINVENTORYSYSTEM_API UGIS_AsyncAction_Wait : public UCancellableAsyncAction
{
GENERATED_BODY()
public:
/**
* Constructor for the async wait action.
* 异步等待动作的构造函数。
*/
UGIS_AsyncAction_Wait();
/**
* Delegate triggered when the wait action completes successfully.
* 等待动作成功完成时触发的委托。
*/
UPROPERTY(BlueprintAssignable)
FGIS_AsyncAction_WaitSignature OnCompleted;
/**
* Delegate triggered when the wait action is cancelled.
* 等待动作取消时触发的委托。
*/
UPROPERTY(BlueprintAssignable)
FGIS_AsyncAction_WaitSignature OnCancelled;
/**
* Gets the world associated with this action.
* 获取与此动作关联的世界。
* @return The world object, or nullptr if not set. 世界对象如果未设置则返回nullptr。
*/
virtual UWorld* GetWorld() const override;
/**
* Gets the target actor for the wait action.
* 获取等待动作的目标演员。
* @return The target actor, or nullptr if not set. 目标演员如果未设置则返回nullptr。
*/
virtual AActor* GetActor() const;
/**
* Activates the wait action, starting the timer.
* 激活等待动作,启动定时器。
*/
virtual void Activate() override;
/**
* Completes the wait action, triggering the OnCompleted delegate.
* 完成等待动作触发OnCompleted委托。
*/
virtual void Complete();
/**
* Cancels the wait action, triggering the OnCancelled delegate.
* 取消等待动作触发OnCancelled委托。
*/
virtual void Cancel() override;
/**
* Determines whether delegates should be broadcast.
* 确定是否应广播委托。
* @return True if delegates should be broadcast, false otherwise. 如果应广播委托则返回true否则返回false。
*/
virtual bool ShouldBroadcastDelegates() const override;
/**
* Called when the target actor is destroyed.
* 目标演员销毁时调用。
* @param DestroyedActor The actor that was destroyed. 被销毁的演员。
*/
UFUNCTION()
virtual void OnTargetDestroyed(AActor* DestroyedActor);
protected:
/**
* Creates a new wait action instance.
* 创建新的等待动作实例。
* @param WorldContext The world context object to get the world reference. 用于获取世界引用的世界上下文对象。
* @param TargetActor The target actor to wait for. 要等待的目标演员。
* @param WaitInterval The interval between checks (in seconds). 检查间隔(以秒为单位)。
* @param MaxWaitTimes The maximum number of checks before timeout (-1 for no limit). 最大检查次数,超时前(-1表示无限制
* @return The created wait action, or nullptr if invalid parameters. 创建的等待动作如果参数无效则返回nullptr。
* @details Logs warnings if the world context, world, target actor, or wait interval is invalid.
* @细节 如果世界上下文、世界、目标演员或等待间隔无效,则记录警告。
*/
template <typename ActionType = UGIS_AsyncAction_Wait>
static ActionType* CreateWaitAction(UObject* WorldContext, AActor* TargetActor, float WaitInterval, int32 MaxWaitTimes)
{
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(TargetActor))
{
GIS_LOG(Warning, "invalid target actor.");
return nullptr;
}
if (WaitInterval <= 0.f)
{
GIS_LOG(Warning, "WaitInterval %f must be greater than zero!", WaitInterval);
return nullptr;
}
ActionType* NewAction = Cast<ActionType>(NewObject<UGIS_AsyncAction_Wait>(GetTransientPackage(), ActionType::StaticClass()));
NewAction->SetWorld(World);
NewAction->SetTargetActor(TargetActor);
NewAction->SetWaitInterval(WaitInterval);
NewAction->SetMaxWaitTimes(MaxWaitTimes);
NewAction->RegisterWithGameInstance(World->GetGameInstance());
return NewAction;
}
/**
* Sets the world for the wait action.
* 设置等待动作的世界。
* @param NewWorld The world to set. 要设置的世界。
*/
void SetWorld(UWorld* NewWorld);
/**
* Sets the target actor for the wait action.
* 设置等待动作的目标演员。
* @param NewTargetActor The target actor to set. 要设置的目标演员。
*/
void SetTargetActor(AActor* NewTargetActor);
/**
* Sets the interval between checks.
* 设置检查间隔。
* @param NewWaitInterval The interval (in seconds). 间隔(以秒为单位)。
*/
void SetWaitInterval(float NewWaitInterval);
/**
* Sets the maximum number of checks before timeout.
* 设置超时前的最大检查次数。
* @param NewMaxWaitTimes The maximum number of checks (-1 for no limit). 最大检查次数(-1表示无限制
*/
void SetMaxWaitTimes(int32 NewMaxWaitTimes);
/**
* Stops the timer for the wait action.
* 停止等待动作的定时器。
*/
void StopWaiting();
/**
* Called when the timer ticks to check the wait condition.
* 定时器触发时调用以检查等待条件。
*/
UFUNCTION()
void OnTimer();
/**
* Cleans up resources used by the wait action.
* 清理等待动作使用的资源。
*/
virtual void Cleanup();
/**
* Executes the specific wait condition check.
* 执行特定的等待条件检查。
*/
UFUNCTION()
virtual void OnExecutionAction();
private:
/**
* Weak reference to the world for the wait action.
* 等待动作的世界的弱引用。
*/
TWeakObjectPtr<UWorld> WorldPtr{nullptr};
/**
* Weak reference to the target actor for the wait action.
* 等待动作的目标演员的弱引用。
*/
UPROPERTY()
TWeakObjectPtr<AActor> TargetActorPtr{nullptr};
/**
* Handle for the timer used to check the wait condition.
* 用于检查等待条件的定时器句柄。
*/
UPROPERTY()
FTimerHandle TimerHandle;
/**
* Interval between checks (in seconds).
* 检查间隔(以秒为单位)。
*/
float WaitInterval = 0.2f;
/**
* Current number of checks performed.
* 当前执行的检查次数。
*/
int32 WaitTimes = 0;
/**
* Maximum number of checks before timeout (-1 for no limit).
* 超时前的最大检查次数(-1表示无限制
*/
int32 MaxWaitTimes{-1};
};

View File

@@ -0,0 +1,84 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GIS_AsyncAction_Wait.h"
#include "GIS_AsyncAction_WaitEquipmentSystem.generated.h"
class UGIS_EquipmentSystemComponent;
/**
* Async action to wait for a valid equipment system component on an actor.
* 在演员上等待有效装备系统组件的异步动作。
*/
UCLASS()
class GENERICINVENTORYSYSTEM_API UGIS_AsyncAction_WaitEquipmentSystem : public UGIS_AsyncAction_Wait
{
GENERATED_BODY()
public:
/**
* Waits for a valid equipment system component on the target actor.
* 在目标演员上等待有效的装备系统组件。
* @param WorldContext The world context object to get the world reference. 用于获取世界引用的世界上下文对象。
* @param TargetActor The target actor to wait for. 要等待的目标演员。
* @return The created wait action. 创建的等待动作。
*/
UFUNCTION(BlueprintCallable, Category="GIS|Async", meta = (WorldContext = "WorldContext", DefaultToSelf="TargetActor", BlueprintInternalUseOnly = "true"))
static UGIS_AsyncAction_WaitEquipmentSystem* WaitEquipmentSystem(UObject* WorldContext, AActor* TargetActor);
protected:
/**
* Checks for the presence of a valid equipment system component.
* 检查是否存在有效的装备系统组件。
*/
virtual void OnExecutionAction() override;
};
/**
* Async action to wait for a valid and initialized equipment system component on an actor.
* 在演员上等待有效且已初始化的装备系统组件的异步动作。
*/
UCLASS()
class GENERICINVENTORYSYSTEM_API UGIS_AsyncAction_WaitEquipmentSystemInitialized : public UGIS_AsyncAction_WaitEquipmentSystem
{
GENERATED_BODY()
public:
/**
* Waits for a valid and initialized equipment system component on the target actor.
* 在目标演员上等待有效且已初始化的装备系统组件。
* @param WorldContext The world context object to get the world reference. 用于获取世界引用的世界上下文对象。
* @param TargetActor The target actor to wait for. 要等待的目标演员。
* @return The created wait action. 创建的等待动作。
*/
UFUNCTION(BlueprintCallable, Category="GIS|Async", meta = (WorldContext = "WorldContext", DefaultToSelf="TargetActor", BlueprintInternalUseOnly = "true"))
static UGIS_AsyncAction_WaitEquipmentSystem* WaitEquipmentSystemInitialized(UObject* WorldContext, AActor* TargetActor);
protected:
/**
* Checks for the presence and initialization of the equipment system component.
* 检查装备系统组件的存在和初始化状态。
*/
virtual void OnExecutionAction() override;
/**
* Cleans up resources and event bindings.
* 清理资源和事件绑定。
*/
virtual void Cleanup() override;
/**
* Called when the equipment system component is initialized.
* 装备系统组件初始化时调用。
*/
UFUNCTION()
virtual void OnSystemInitialized();
/**
* Weak reference to the equipment system component being waited for.
* 等待的装备系统组件的弱引用。
*/
TWeakObjectPtr<UGIS_EquipmentSystemComponent> EquipmentSystemPtr;
};

View File

@@ -0,0 +1,83 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GIS_AsyncAction_Wait.h"
#include "GIS_InventoryMeesages.h"
#include "GIS_AsyncAction_WaitInventorySystem.generated.h"
/**
* Async action to wait for a valid inventory system component on an actor.
* 在演员上等待有效库存系统组件的异步动作。
*/
UCLASS()
class GENERICINVENTORYSYSTEM_API UGIS_AsyncAction_WaitInventorySystem : public UGIS_AsyncAction_Wait
{
GENERATED_BODY()
public:
/**
* Waits for a valid inventory system component on the target actor.
* 在目标演员上等待有效的库存系统组件。
* @param WorldContext The world context object to get the world reference. 用于获取世界引用的世界上下文对象。
* @param TargetActor The target actor to wait for. 要等待的目标演员。
* @return The created wait action. 创建的等待动作。
*/
UFUNCTION(BlueprintCallable, Category="GIS|Async", meta = (WorldContext = "WorldContext", DefaultToSelf="TargetActor", BlueprintInternalUseOnly = "true"))
static UGIS_AsyncAction_WaitInventorySystem* WaitInventorySystem(UObject* WorldContext, AActor* TargetActor);
protected:
/**
* Checks for the presence of a valid inventory system component.
* 检查是否存在有效的库存系统组件。
*/
virtual void OnExecutionAction() override;
};
/**
* Async action to wait for a valid and initialized inventory system component on an actor.
* 在演员上等待有效且已初始化的库存系统组件的异步动作。
*/
UCLASS()
class GENERICINVENTORYSYSTEM_API UGIS_AsyncAction_WaitInventorySystemInitialized : public UGIS_AsyncAction_WaitInventorySystem
{
GENERATED_BODY()
public:
/**
* Waits for a valid and initialized inventory system component on the target actor.
* 在目标演员上等待有效且已初始化的库存系统组件。
* @param WorldContext The world context object to get the world reference. 用于获取世界引用的世界上下文对象。
* @param TargetActor The target actor to wait for. 要等待的目标演员。
* @return The created wait action. 创建的等待动作。
*/
UFUNCTION(BlueprintCallable, Category="GIS|Async", meta = (WorldContext = "WorldContext", DefaultToSelf="TargetActor", BlueprintInternalUseOnly = "true"))
static UGIS_AsyncAction_WaitInventorySystem* WaitInventorySystemInitialized(UObject* WorldContext, AActor* TargetActor);
protected:
/**
* Checks for the presence and initialization of the inventory system component.
* 检查库存系统组件的存在和初始化状态。
*/
virtual void OnExecutionAction() override;
/**
* Cleans up resources and event bindings.
* 清理资源和事件绑定。
*/
virtual void Cleanup() override;
/**
* Called when the inventory system component is initialized.
* 库存系统组件初始化时调用。
*/
UFUNCTION()
virtual void OnSystemInitialized();
/**
* Weak reference to the inventory system component being waited for.
* 等待的库存系统组件的弱引用。
*/
TWeakObjectPtr<UGIS_InventorySystemComponent> InventorySysPtr;
};

View File

@@ -0,0 +1,56 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/CancellableAsyncAction.h"
#include "Templates/SubclassOf.h"
#include "Runtime/Launch/Resources/Version.h"
#if ENGINE_MINOR_VERSION < 5
#include "InstancedStruct.h"
#else
#include "StructUtils/InstancedStruct.h"
#endif
#include "GIS_AsyncAction_WaitItemFragmentDataChanged.generated.h"
class UGIS_ItemFragment;
class UGIS_ItemInstance;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FGIS_WaitFragmentStateChangedSignature, const UGIS_ItemFragment*, Fragment, const FInstancedStruct&, Data);
/**
* Async action to wait for a fragment data changed on an item instance.
* 在道具实例上等待指定道具片段的运行时数据变更。
*/
UCLASS()
class GENERICINVENTORYSYSTEM_API UGIS_AsyncAction_WaitItemFragmentDataChanged : public UCancellableAsyncAction
{
GENERATED_BODY()
public:
/**
* Wait for a fragment data changed on an item instance.
* 在道具实例上等待指定道具片段的运行时数据变更。
* @param WorldContext The world context object to get the world reference. 用于获取世界引用的世界上下文对象。
* @param ItemInstance The target item instance to wait for. 要等待的目标道具。
* @param FragmentClass The fragment type to wait for. 要等待的片段类型。
* @return The created wait action. 创建的等待动作。
*/
UFUNCTION(BlueprintCallable, Category="GIS|Async", meta = (WorldContext = "WorldContext", DefaultToSelf="ItemInstnace", BlueprintInternalUseOnly = "true"))
static UGIS_AsyncAction_WaitItemFragmentDataChanged* WaitItemFragmentStateChanged(UObject* WorldContext, UGIS_ItemInstance* ItemInstance, TSoftClassPtr<UGIS_ItemFragment> FragmentClass);
virtual void Activate() override;
virtual void Cancel() override;
UPROPERTY(BlueprintAssignable, Category="GIS|Async")
FGIS_WaitFragmentStateChangedSignature OnStateChanged;
protected:
UFUNCTION()
void OnFragmentStateChanged(const UGIS_ItemFragment* Fragment, const FInstancedStruct& State);
TWeakObjectPtr<UGIS_ItemInstance> ItemInstance;
TSubclassOf<UGIS_ItemFragment> FragmentClass;
};

View File

@@ -0,0 +1,259 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Net/Serialization/FastArraySerializer.h"
#include "UObject/Interface.h"
#include "GameplayTagContainer.h"
#include "GIS_GameplayTagFloat.generated.h"
struct FGIS_GameplayTagFloatContainer;
/**
* Interface for objects that own a gameplay tag float container.
* 拥有游戏标签浮点容器对象的接口。
*/
UINTERFACE(meta=(CannotImplementInterfaceInBlueprint))
class GENERICINVENTORYSYSTEM_API UGIS_GameplayTagFloatContainerOwner : public UInterface
{
GENERATED_BODY()
};
/**
* Interface class for handling updates to float attributes in a gameplay tag container.
* 处理游戏标签容器中浮点属性更新的接口类。
*/
class GENERICINVENTORYSYSTEM_API IGIS_GameplayTagFloatContainerOwner
{
GENERATED_BODY()
public:
/**
* Called when a float attribute associated with a gameplay tag is updated.
* 当与游戏标签关联的浮点属性更新时调用。
* @param Tag The gameplay tag identifying the attribute. 标识属性的游戏标签。
* @param OldValue The previous value of the attribute. 属性之前的值。
* @param NewValue The new value of the attribute. 属性的新值。
*/
virtual void OnTagFloatUpdate(const FGameplayTag& Tag, float OldValue, float NewValue) = 0;
};
/**
* Represents a gameplay tag and float value pair.
* 表示一个游戏标签和浮点值的键值对。
*/
USTRUCT(BlueprintType)
struct GENERICINVENTORYSYSTEM_API FGIS_GameplayTagFloat : public FFastArraySerializerItem
{
GENERATED_BODY()
/**
* Default constructor for the gameplay tag float pair.
* 游戏标签浮点对的默认构造函数。
*/
FGIS_GameplayTagFloat()
{
}
/**
* Constructor for the gameplay tag float pair with initial values.
* 使用初始值构造游戏标签浮点对。
* @param InTag The gameplay tag for the pair. 键值对的游戏标签。
* @param InValue The float value for the pair. 键值对的浮点值。
*/
FGIS_GameplayTagFloat(FGameplayTag InTag, float InValue)
: Tag(InTag)
, Value(InValue)
{
}
/**
* Gets a debug string representation of the tag-value pair.
* 获取标签-值对的调试字符串表示。
* @return The debug string. 调试字符串。
*/
FString GetDebugString() const;
friend FGIS_GameplayTagFloatContainer;
/**
* The gameplay tag identifying the attribute.
* 标识属性的游戏标签。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS")
FGameplayTag Tag;
/**
* The float value associated with the tag.
* 与标签关联的浮点值。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS")
float Value = 0;
/**
* The previous float value of the attribute (not replicated).
* 属性的前一个浮点值(不复制)。
*/
UPROPERTY(NotReplicated)
float PrevValue = 0;
};
/**
* Container for storing gameplay tag float pairs.
* 存储游戏标签浮点对的容器。
*/
USTRUCT(BlueprintType)
struct GENERICINVENTORYSYSTEM_API FGIS_GameplayTagFloatContainer : public FFastArraySerializer
{
GENERATED_BODY()
/**
* Default constructor for the float container.
* 浮点容器的默认构造函数。
*/
FGIS_GameplayTagFloatContainer()
// : Owner(nullptr)
{
}
/**
* Constructor for the float container with an owning object.
* 使用拥有对象构造浮点容器。
* @param InObject The object that owns this container. 拥有此容器的对象。
*/
FGIS_GameplayTagFloatContainer(UObject* InObject)
: ContainerOwner(InObject)
{
}
/**
* Adds a new tag-value pair to the container.
* 向容器添加新的标签-值对。
* @param Tag The gameplay tag to add. 要添加的游戏标签。
* @param Value The float value to associate with the tag. 与标签关联的浮点值。
*/
void AddItem(FGameplayTag Tag, float Value);
/**
* Sets the value for an existing tag or adds a new tag-value pair.
* 为现有标签设置值或添加新的标签-值对。
* @param Tag The gameplay tag to set. 要设置的游戏标签。
* @param Value The float value to set. 要设置的浮点值。
*/
void SetItem(FGameplayTag Tag, float Value);
/**
* Removes a specified amount from a tag-value pair.
* 从标签-值对中移除指定数量。
* @param Tag The gameplay tag to remove value from. 要移除值的游戏标签。
* @param Value The amount to remove. 要移除的数量。
*/
void RemoveItem(FGameplayTag Tag, float Value);
/**
* Sets all items in the container.
* 设置容器中的所有条目。
* @param NewItems The array of tag-value pairs to set. 要设置的标签-值对数组。
*/
void SetItems(const TArray<FGIS_GameplayTagFloat>& NewItems);
/**
* Clears all items from the container.
* 清空容器中的所有条目。
*/
void EmptyItems();
/**
* Gets the value associated with a specific tag.
* 获取与指定标签关联的值。
* @param Tag The gameplay tag to query. 要查询的游戏标签。
* @return The float value associated with the tag. 与标签关联的浮点值。
*/
float GetValue(FGameplayTag Tag) const
{
return TagToValueMap.FindRef(Tag);
}
/**
* Checks if the container contains a specific tag.
* 检查容器是否包含指定标签。
* @param Tag The gameplay tag to check. 要检查的游戏标签。
* @return True if the tag exists in the container, false otherwise. 如果标签存在于容器中则返回true否则返回false。
*/
bool ContainsTag(FGameplayTag Tag) const
{
return TagToValueMap.Contains(Tag);
}
//~FFastArraySerializer contract
/**
* Called before items are removed during replication.
* 复制期间移除条目前调用。
* @param RemovedIndices The indices of items to remove. 要移除的条目索引。
* @param FinalSize The final size of the items array after removal. 移除后条目数组的最终大小。
*/
void PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize);
/**
* Called after items are added during replication.
* 复制期间添加条目后调用。
* @param AddedIndices The indices of added items. 添加的条目索引。
* @param FinalSize The final size of the items array after addition. 添加后条目数组的最终大小。
*/
void PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize);
/**
* Called after items are changed during replication.
* 复制期间条目更改后调用。
* @param ChangedIndices The indices of changed items. 更改的条目索引。
* @param FinalSize The final size of the items array after change. 更改后条目数组的最终大小。
*/
void PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize);
//~End of FFastArraySerializer contract
/**
* Handles delta serialization for network replication.
* 处理网络复制的增量序列化。
* @param DeltaParms The serialization parameters. 序列化参数。
* @return True if serialization was successful, false otherwise. 如果序列化成功则返回true否则返回false。
*/
bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms)
{
return FastArrayDeltaSerialize<FGIS_GameplayTagFloat, FGIS_GameplayTagFloatContainer>(Items, DeltaParms, *this);
}
/**
* The object that owns this container.
* 拥有此容器的对象。
*/
UPROPERTY()
TObjectPtr<UObject> ContainerOwner{nullptr};
/**
* Replicated list of gameplay tag float pairs.
* 游戏标签浮点对的复制列表。
*/
UPROPERTY(EditAnywhere, SaveGame, BlueprintReadWrite, Category="GIS", meta=(TitleProperty="{Tag}->{Value}"))
TArray<FGIS_GameplayTagFloat> Items;
/**
* Accelerated map of tags to values for efficient queries.
* 标签到值的加速映射,用于高效查询。
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, NotReplicated, SaveGame, Category="GIS", meta=(ForceInlineRow))
TMap<FGameplayTag, float> TagToValueMap;
};
/**
* Traits for the float container to enable network delta serialization.
* 浮点容器的特性,用于启用网络增量序列化。
*/
template <>
struct TStructOpsTypeTraits<FGIS_GameplayTagFloatContainer> : TStructOpsTypeTraitsBase2<FGIS_GameplayTagFloatContainer>
{
enum
{
WithNetDeltaSerializer = true,
};
};

View File

@@ -0,0 +1,248 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Net/Serialization/FastArraySerializer.h"
#include "UObject/Interface.h"
#include "GameplayTagContainer.h"
#include "GIS_GameplayTagInteger.generated.h"
struct FGIS_GameplayTagIntegerContainer;
/**
* Interface for objects that own a gameplay tag integer container.
* 拥有游戏标签整型容器对象的接口。
*/
UINTERFACE(meta=(CannotImplementInterfaceInBlueprint))
class GENERICINVENTORYSYSTEM_API UGIS_GameplayTagIntegerContainerOwner : public UInterface
{
GENERATED_BODY()
};
/**
* Interface class for handling updates to integer attributes in a gameplay tag container.
* 处理游戏标签容器中整型属性更新的接口类。
*/
class GENERICINVENTORYSYSTEM_API IGIS_GameplayTagIntegerContainerOwner
{
GENERATED_BODY()
public:
/**
* Called when an integer attribute associated with a gameplay tag is updated.
* 当与游戏标签关联的整型属性更新时调用。
* @param Tag The gameplay tag identifying the attribute. 标识属性的游戏标签。
* @param OldValue The previous value of the attribute. 属性之前的值。
* @param NewValue The new value of the attribute. 属性的新值。
*/
virtual void OnTagIntegerUpdate(const FGameplayTag& Tag, int32 OldValue, int32 NewValue) = 0;
};
/**
* Represents a gameplay tag and integer value pair.
* 表示一个游戏标签和整型值的键值对。
*/
USTRUCT(BlueprintType)
struct GENERICINVENTORYSYSTEM_API FGIS_GameplayTagInteger : public FFastArraySerializerItem
{
GENERATED_BODY()
/**
* Default constructor for the gameplay tag integer pair.
* 游戏标签整型对的默认构造函数。
*/
FGIS_GameplayTagInteger()
{
}
/**
* Constructor for the gameplay tag integer pair with initial values.
* 使用初始值构造游戏标签整型对。
* @param InTag The gameplay tag for the pair. 键值对的游戏标签。
* @param InValue The integer value for the pair. 键值对的整型值。
*/
FGIS_GameplayTagInteger(FGameplayTag InTag, int32 InValue)
: Tag(InTag)
, Value(InValue)
{
}
/**
* Gets a debug string representation of the tag-value pair.
* 获取标签-值对的调试字符串表示。
* @return The debug string. 调试字符串。
*/
FString GetDebugString() const;
friend FGIS_GameplayTagIntegerContainer;
/**
* The gameplay tag identifying the attribute.
* 标识属性的游戏标签。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS")
FGameplayTag Tag;
/**
* The integer value associated with the tag.
* 与标签关联的整型值。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS")
int32 Value = 0;
/**
* The previous integer value of the attribute (not replicated).
* 属性的前一个整型值(不复制)。
* @attention Likely a typo in the original code; should be int32 instead of float.
* @注意 原始代码中可能有误应为int32而非float。
*/
UPROPERTY(NotReplicated)
float PrevValue = 0;
};
/**
* Container for storing gameplay tag integer pairs.
* 存储游戏标签整型对的容器。
*/
USTRUCT(BlueprintType)
struct GENERICINVENTORYSYSTEM_API FGIS_GameplayTagIntegerContainer : public FFastArraySerializer
{
GENERATED_BODY()
/**
* Default constructor for the integer container.
* 整型容器的默认构造函数。
*/
FGIS_GameplayTagIntegerContainer()
// : Owner(nullptr)
{
}
/**
* Constructor for the integer container with an owning object.
* 使用拥有对象构造整型容器。
* @param InObject The object that owns this container. 拥有此容器的对象。
*/
FGIS_GameplayTagIntegerContainer(UObject* InObject)
: ContainerOwner(InObject)
{
}
/**
* Adds a new tag-value pair to the container.
* 向容器添加新的标签-值对。
* @param Tag The gameplay tag to add. 要添加的游戏标签。
* @param Value The integer value to associate with the tag. 与标签关联的整型值。
*/
void AddItem(FGameplayTag Tag, int32 Value);
/**
* Sets the value for an existing tag or adds a new tag-value pair.
* 为现有标签设置值或添加新的标签-值对。
* @param Tag The gameplay tag to set. 要设置的游戏标签。
* @param Value The integer value to set. 要设置的整型值。
*/
void SetItem(FGameplayTag Tag, int32 Value);
/**
* Removes a specified amount from a tag-value pair.
* 从标签-值对中移除指定数量。
* @param Tag The gameplay tag to remove value from. 要移除值的游戏标签。
* @param Value The amount to remove. 要移除的数量。
*/
void RemoveItem(FGameplayTag Tag, int32 Value);
/**
* Gets the value associated with a specific tag.
* 获取与指定标签关联的值。
* @param Tag The gameplay tag to query. 要查询的游戏标签。
* @return The integer value associated with the tag. 与标签关联的整型值。
*/
int32 GetValue(FGameplayTag Tag) const
{
return TagToValueMap.FindRef(Tag);
}
/**
* Checks if the container contains a specific tag.
* 检查容器是否包含指定标签。
* @param Tag The gameplay tag to check. 要检查的游戏标签。
* @return True if the tag exists in the container, false otherwise. 如果标签存在于容器中则返回true否则返回false。
*/
bool ContainsTag(FGameplayTag Tag) const
{
return TagToValueMap.Contains(Tag);
}
//~FFastArraySerializer contract
/**
* Called before items are removed during replication.
* 复制期间移除条目前调用。
* @param RemovedIndices The indices of items to remove. 要移除的条目索引。
* @param FinalSize The final size of the items array after removal. 移除后条目数组的最终大小。
*/
void PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize);
/**
* Called after items are added during replication.
* 复制期间添加条目后调用。
* @param AddedIndices The indices of added items. 添加的条目索引。
* @param FinalSize The final size of the items array after addition. 添加后条目数组的最终大小。
*/
void PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize);
/**
* Called after items are changed during replication.
* 复制期间条目更改后调用。
* @param ChangedIndices The indices of changed items. 更改的条目索引。
* @param FinalSize The final size of the items array after change. 更改后条目数组的最终大小。
*/
void PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize);
//~End of FFastArraySerializer contract
/**
* Handles delta serialization for network replication.
* 处理网络复制的增量序列化。
* @param DeltaParms The serialization parameters. 序列化参数。
* @return True if serialization was successful, false otherwise. 如果序列化成功则返回true否则返回false。
*/
bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms)
{
return FastArrayDeltaSerialize<FGIS_GameplayTagInteger, FGIS_GameplayTagIntegerContainer>(Items, DeltaParms, *this);
}
/**
* The object that owns this container.
* 拥有此容器的对象。
*/
UPROPERTY()
TObjectPtr<UObject> ContainerOwner{nullptr};
/**
* Replicated list of gameplay tag integer pairs.
* 游戏标签整型对的复制列表。
*/
UPROPERTY(EditAnywhere, SaveGame, BlueprintReadWrite, Category="GIS", meta=(TitleProperty="{Tag}->{Value}"))
TArray<FGIS_GameplayTagInteger> Items;
/**
* Accelerated map of tags to values for efficient queries.
* 标签到值的加速映射,用于高效查询。
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, NotReplicated, SaveGame, Category="GIS", meta=(ForceInlineRow))
TMap<FGameplayTag, int32> TagToValueMap;
};
/**
* Traits for the integer container to enable network delta serialization.
* 整型容器的特性,用于启用网络增量序列化。
*/
template <>
struct TStructOpsTypeTraits<FGIS_GameplayTagIntegerContainer> : TStructOpsTypeTraitsBase2<FGIS_GameplayTagIntegerContainer>
{
enum
{
WithNetDeltaSerializer = true,
};
};

View File

@@ -0,0 +1,145 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Net/Serialization/FastArraySerializer.h"
#include "GIS_CollectionContainer.generated.h"
class UGIS_ItemCollectionDefinition;
class UGIS_InventorySystemComponent;
class UGIS_ItemCollection;
struct FGIS_CollectionContainer;
/**
* Structure representing an entry in the collection container.
* 表示集合容器中条目的结构体。
*/
USTRUCT()
struct GENERICINVENTORYSYSTEM_API FGIS_CollectionEntry : public FFastArraySerializerItem
{
GENERATED_BODY()
/**
* Default constructor for collection entry.
* 集合条目的默认构造函数。
*/
FGIS_CollectionEntry() : Instance(nullptr)
{
}
/**
* Unique ID of the collection entry.
* 集合条目的唯一ID。
*/
UPROPERTY(VisibleAnywhere, Category="GIS")
FGuid Id = FGuid();
/**
* The collection definition associated with this entry.
* 与此条目关联的集合定义。
*/
UPROPERTY(VisibleAnywhere, Category="GIS")
TObjectPtr<const UGIS_ItemCollectionDefinition> Definition;
/**
* The collection instance associated with this entry.
* 与此条目关联的集合实例。
*/
UPROPERTY(VisibleAnywhere, Category="GIS", meta=(ShowInnerProperties))
TObjectPtr<UGIS_ItemCollection> Instance = nullptr;
/**
* Checks if the collection entry is valid.
* 检查集合条目是否有效。
* @return True if the entry is valid, false otherwise. 如果条目有效则返回true否则返回false。
*/
bool IsValidEntry() const;
};
/**
* Container for storing a list of item collections with replication support.
* 用于存储道具集合列表的容器,支持复制。
*/
USTRUCT()
struct GENERICINVENTORYSYSTEM_API FGIS_CollectionContainer : public FFastArraySerializer
{
GENERATED_BODY()
/**
* Default constructor for collection container.
* 集合容器的默认构造函数。
*/
FGIS_CollectionContainer()
: OwningComponent(nullptr)
{
}
/**
* Constructor for collection container with an owning inventory component.
* 使用所属库存组件构造集合容器。
* @param InInventory The owning inventory system component. 所属的库存系统组件。
*/
FGIS_CollectionContainer(UGIS_InventorySystemComponent* InInventory);
//~FFastArraySerializer contract
/**
* Called before collection entries are removed during replication.
* 复制期间在移除集合条目前调用。
* @param RemovedIndices The indices of removed entries. 移除条目的索引。
* @param FinalSize The final size of the array after removal. 移除后数组的最终大小。
*/
void PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize);
/**
* Called after collection entries are added during replication.
* 复制期间在添加集合条目后调用。
* @param AddedIndices The indices of added entries. 添加条目的索引。
* @param FinalSize The final size of the array after addition. 添加后数组的最终大小。
*/
void PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize);
/**
* Called after collection entries are changed during replication.
* 复制期间在集合条目更改后调用。
* @param ChangedIndices The indices of changed entries. 更改条目的索引。
* @param FinalSize The final size of the array after changes. 更改后数组的最终大小。
*/
void PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize);
//~End of FFastArraySerializer contract
/**
* Handles delta serialization for the collection container.
* 处理集合容器的增量序列化。
* @param DeltaParms The serialization parameters. 序列化参数。
* @return True if serialization was successful, false otherwise. 如果序列化成功则返回true否则返回false。
*/
bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParms)
{
return FastArrayDeltaSerialize<FGIS_CollectionEntry, FGIS_CollectionContainer>(Entries, DeltaParms, *this);
}
/**
* Replicated list of collection entries.
* 复制的集合条目列表。
*/
UPROPERTY(VisibleAnywhere, Category="InventorySystem", meta=(ShowOnlyInnerProperties, DisplayName="Collections"))
TArray<FGIS_CollectionEntry> Entries;
/**
* The inventory system component that owns this container.
* 拥有此容器的库存系统组件。
*/
UPROPERTY()
TObjectPtr<UGIS_InventorySystemComponent> OwningComponent;
};
/**
* Template specialization to enable network delta serialization for the collection container.
* 为集合容器启用网络增量序列化的模板特化。
*/
template <>
struct TStructOpsTypeTraits<FGIS_CollectionContainer> : TStructOpsTypeTraitsBase2<FGIS_CollectionContainer>
{
enum { WithNetDeltaSerializer = true };
};

View File

@@ -0,0 +1,651 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "Engine/EngineTypes.h"
#include "Engine/DataAsset.h"
#include "GIS_CoreStructLibray.h"
#include "GIS_InventoryMeesages.h"
#include "Items/GIS_ItemInfo.h"
#include "Items/GIS_ItemStack.h"
#include "GIS_ItemCollection.generated.h"
class UGIS_ItemRestriction;
class UGIS_SerializationFunctionLibrary;
class UGIS_ItemRestrictionSet;
class UGIS_InventorySubsystem;
class UGIS_ItemCollection;
class UGIS_InventorySystemComponent;
/**
* Delegate triggered when the item collection is updated.
* 道具集合更新时触发的委托。
* @param Message The update message containing collection changes. 包含集合变更的更新消息。
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGIS_Collection_UpdateSignature, const FGIS_InventoryCollectionUpdateMessage&, Message);
/**
* Holds static configuration for an item collection.
* 存储道具集合的静态配置。
*/
UCLASS(BlueprintType, NotBlueprintable)
class GENERICINVENTORYSYSTEM_API UGIS_ItemCollectionDefinition : public UDataAsset
{
GENERATED_BODY()
public:
/**
* Checks if the collection definition supports networking.
* 检查集合定义是否支持网络。
* @return True if networking is supported, false otherwise. 如果支持网络则返回true否则返回false。
*/
virtual bool IsSupportedForNetworking() const override;
/**
* The unique tag of the item collection for querying in the inventory.
* 道具集合的唯一标签,用于在库存中查询。
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Common", meta=(Categories="GIS.Collection"))
FGameplayTag CollectionTag;
/**
* Restrictions applied to the collection for complex use cases.
* 为复杂用例应用于集合的限制。
* @details Restrictions perform pre-checks to determine if items can be added or removed.
* @细节 限制会在道具操作前执行检查,以决定是否可以添加或移除道具。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Instanced, Category="Common")
TArray<TObjectPtr<UGIS_ItemRestriction>> Restrictions;
/**
* Options for handling overflow when an item cannot fit in the collection.
* 当道具无法放入集合时处理溢出的选项。
*/
UPROPERTY(EditAnywhere, Category="Common")
FGIS_ItemOverflowOptions OverflowOptions;
/**
* Gets the class for instantiating the collection.
* 获取用于实例化集合的类。
* @return The collection instance class. 集合实例类。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="Common")
virtual TSubclassOf<UGIS_ItemCollection> GetCollectionInstanceClass() const;
};
/**
* Base class for a normal item collection.
* 常规道具集合的基类。
*/
UCLASS(BlueprintType, DefaultToInstanced, EditInlineNew, CollapseCategories, DisplayName="GIS Collection (Normal)")
class GENERICINVENTORYSYSTEM_API UGIS_ItemCollection : public UObject
{
GENERATED_BODY()
friend UGIS_ItemCollectionDefinition;
friend UGIS_InventorySystemComponent;
friend FGIS_ItemStackContainer;
public:
/**
* Helper struct to lock notifications during collection updates.
* 用于在集合更新期间锁定通知的辅助结构体。
*/
struct GIS_CollectionNotifyLocker
{
/**
* Constructor that locks notifications for the collection.
* 为集合锁定通知的构造函数。
* @param InItemCollection The collection to lock notifications for. 要锁定通知的集合。
*/
GIS_CollectionNotifyLocker(UGIS_ItemCollection& InItemCollection);
/**
* Destructor that unlocks notifications.
* 解锁通知的析构函数。
*/
~GIS_CollectionNotifyLocker();
/**
* The collection being managed.
* 被管理的集合。
*/
UGIS_ItemCollection& ItemCollection;
};
/**
* Constructor for the item collection.
* 道具集合的构造函数。
* @param ObjectInitializer The object initializer. 对象初始化器。
*/
UGIS_ItemCollection(const FObjectInitializer& ObjectInitializer);
//~UObject interface
/**
* Gets the properties that should be replicated for this object.
* 获取需要为此对象复制的属性。
* @param OutLifetimeProps Array to store the replicated properties. 存储复制属性的数组。
*/
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
/**
* Calls a remote function on the object.
* 在对象上调用远程函数。
* @param Function The function to call. 要调用的函数。
* @param Parms The function parameters. 函数参数。
* @param OutParms The output parameters. 输出参数。
* @param Stack The function call stack. 函数调用堆栈。
* @return True if the function call was successful, false otherwise. 如果函数调用成功则返回true否则返回false。
*/
virtual bool CallRemoteFunction(UFunction* Function, void* Parms, FOutParmRec* OutParms, FFrame* Stack) override;
/**
* Gets the function call space for the object.
* 获取对象的函数调用空间。
* @param Function The function to query. 要查询的函数。
* @param Stack The function call stack. 函数调用堆栈。
* @return The call space identifier. 调用空间标识符。
*/
virtual int32 GetFunctionCallspace(UFunction* Function, FFrame* Stack) override;
/**
* Checks if the object supports networking.
* 检查对象是否支持网络。
* @return True if networking is supported, false otherwise. 如果支持网络则返回true否则返回false。
*/
virtual bool IsSupportedForNetworking() const override { return true; }
//~End of UObject interface
/**
* Checks if the collection is initialized.
* 检查集合是否已初始化。
* @return True if the collection is initialized, false otherwise. 如果集合已初始化则返回true否则返回false。
*/
bool IsInitialized() const;
/**
* Gets the inventory that owns this collection.
* 获取拥有此集合的库存。
* @return The owning inventory, or nullptr if not set. 所属库存如果未设置则返回nullptr。
*/
UFUNCTION(BlueprintCallable, Category="GIS|ItemCollection")
UGIS_InventorySystemComponent* GetOwningInventory() const { return OwningInventory; };
/**
* Gets the unique tag of this collection.
* 获取此集合的唯一标签。
* @return The collection tag. 集合标签。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection")
FGameplayTag GetCollectionTag() const { return CollectionTag; };
/**
* Gets the unique ID of this collection.
* 获取此集合的唯一ID。
* @return The collection ID. 集合ID。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection")
FGuid GetCollectionId() const { return CollectionId; };
/**
* Gets the definition from which this collection was created.
* 获取此集合的源定义。
* @return The collection definition, or nullptr if not set. 集合定义如果未设置则返回nullptr。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection")
const UGIS_ItemCollectionDefinition* GetDefinition() const { return Definition; };
/**
* Gets the display name of the collection.
* 获取集合的显示名称。
* @return The display name. 显示名称。
*/
FString GetCollectionName() const;
/**
* Gets a debug string representation of the collection.
* 获取集合的调试字符串表示。
* @return The debug string. 调试字符串。
*/
FString GetDebugString() const;
#pragma region HasRegion
/**
* Checks if the collection contains a specified item.
* 检查集合是否包含指定道具。
* @param Item The item instance to check. 要检查的道具实例。
* @param Amount The amount to check for. 要检查的数量。
* @param SimilarItem Whether to check for similar items or exact matches. 是否检查相似道具或精确匹配。
* @return True if the collection contains at least the specified amount, false otherwise. 如果集合包含至少指定数量则返回true否则返回false。
*/
virtual bool HasItem(const UGIS_ItemInstance* Item, int32 Amount, bool SimilarItem = true) const;
#pragma endregion HasRegion
#pragma region Add&Remove
/**
* Adds an item to the collection.
* 将道具添加到集合。
* @param ItemInfo The item information to add. 要添加的道具信息。
* @return The item that was not added or empty if added successfully. 未添加的道具,如果成功添加则为空。
*/
virtual FGIS_ItemInfo AddItem(const FGIS_ItemInfo& ItemInfo);
/**
* Adds multiple items to the collection.
* 将多个道具添加到集合。
* @param ItemInfos The array of item information to add. 要添加的道具信息数组。
* @return The number of items added. 添加的道具数量。
*/
virtual int32 AddItems(const TArray<FGIS_ItemInfo>& ItemInfos);
/**
* Adds a specific amount of an item to the collection.
* 将指定数量的道具添加到集合。
* @param Item The item instance to add. 要添加的道具实例。
* @param Amount The amount to add. 要添加的数量。
* @return The item that was actually added. 实际添加的道具。
*/
FGIS_ItemInfo AddItem(UGIS_ItemInstance* Item, int32 Amount);
/**
* Checks if an item can be added to the collection.
* 检查道具是否可以添加到集合。
* @param InItemInfo The item information to check. 要检查的道具信息。
* @param OutItemInfo The item information that can be added (output). 可添加的道具信息(输出)。
* @return True if at least one item can be added, false otherwise. 如果至少可以添加一个道具则返回true否则返回false。
*/
virtual bool CanAddItem(const FGIS_ItemInfo& InItemInfo, FGIS_ItemInfo& OutItemInfo);
/**
* Checks if an item can be stacked with an existing stack.
* 检查道具是否可以与现有栈堆叠。
* @param ItemInfo The item information to check. 要检查的道具信息。
* @param ItemStack The item stack to check against. 要检查的道具栈。
* @return True if the item can be stacked, false otherwise. 如果道具可以堆叠则返回true否则返回false。
*/
virtual bool CanItemStack(const FGIS_ItemInfo& ItemInfo, const FGIS_ItemStack& ItemStack) const;
/**
* Checks conditions for removing an item from the collection.
* 检查从集合移除道具的条件。
* @param ItemInfo The item information to remove. 要移除的道具信息。
* @param OutItemInfo The item information that can be removed (output). 可移除的道具信息(输出)。
* @return True if the item can be removed, false otherwise. 如果道具可以移除则返回true否则返回false。
*/
virtual bool RemoveItemCondition(const FGIS_ItemInfo& ItemInfo, FGIS_ItemInfo& OutItemInfo);
/**
* Removes an item from the collection.
* 从集合中移除道具。
* @param ItemInfo The item information to remove. 要移除的道具信息。
* @return The item that was removed, or empty if nothing was removed. 移除的道具,如果未移除则为空。
*/
virtual FGIS_ItemInfo RemoveItem(const FGIS_ItemInfo& ItemInfo);
/**
* Removes all items from the collection.
* 从集合中移除所有道具。
*/
virtual void RemoveAll();
#pragma endregion Add&Remove
#pragma region GetReference
/**
* Gets item information by stack ID.
* 通过栈ID获取道具信息。
* @param InStackId The stack ID to query. 要查询的栈ID。
* @param OutItemInfo The item information (output). 道具信息(输出)。
* @return True if item information was found, false otherwise. 如果找到道具信息则返回true否则返回false。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection")
bool GetItemInfoByStackId(FGuid InStackId, FGIS_ItemInfo& OutItemInfo) const;
/**
* Finds item information by stack ID.
* 通过栈ID查找道具信息。
* @param InStackId The stack ID to query. 要查询的栈ID。
* @param OutItemInfo The item information (output). 道具信息(输出)。
* @return True if item information was found, false otherwise. 如果找到道具信息则返回true否则返回false。
*/
UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|ItemCollection", meta=(ExpandBoolAsExecs="ReturnValue"))
bool FindItemInfoByStackId(FGuid InStackId, FGIS_ItemInfo& OutItemInfo) const;
/**
* Retrieves information about a specific item instance.
* 获取指定道具实例的信息。
* @param Item The item instance to query. 要查询的道具实例。
* @param OutItemInfo The item information (output). 道具信息(输出)。
* @return True if information was found, false otherwise. 如果找到信息则返回true否则返回false。
*/
virtual bool GetItemInfo(const UGIS_ItemInstance* Item, FGIS_ItemInfo& OutItemInfo) const;
/**
* Gets item information by item definition.
* 通过道具定义获取道具信息。
* @param ItemDefinition The item definition to query. 要查询的道具定义。
* @param OutItemInfo The item information (output). 道具信息(输出)。
* @return True if information was found, false otherwise. 如果找到信息则返回true否则返回false。
*/
bool GetItemInfoByDefinition(const TSoftObjectPtr<UGIS_ItemDefinition>& ItemDefinition, FGIS_ItemInfo& OutItemInfo);
/**
* Gets all item information for a specific item definition.
* 获取指定道具定义的所有道具信息。
* @param ItemDefinition The item definition to query. 要查询的道具定义。
* @param OutItemInfos The array of item information (output). 道具信息数组(输出)。
* @return True if information was found, false otherwise. 如果找到信息则返回true否则返回false。
*/
bool GetItemInfosByDefinition(const TSoftObjectPtr<UGIS_ItemDefinition>& ItemDefinition, TArray<FGIS_ItemInfo>& OutItemInfos);
/**
* Gets the amount of a specific item in the collection.
* 获取集合中指定道具的数量。
* @param Item The item instance to query. 要查询的道具实例。
* @param SimilarItem Whether to count similar items or exact matches. 是否计数相似道具或精确匹配。
* @return The amount of the item in the collection. 集合中的道具数量。
*/
int32 GetItemAmount(const UGIS_ItemInstance* Item, bool SimilarItem = true) const;
/**
* Gets the amount of items with a specific definition in the collection.
* 获取集合中具有指定定义的道具数量。
* @param ItemDefinition The item definition to query. 要查询的道具定义。
* @param CountStacks Whether to count the number of stacks instead of total amount. 是否计数栈数量而不是总数量。
* @return The number of items or stacks in the collection. 集合中的道具或栈数量。
*/
UFUNCTION(BlueprintCallable, Category="GIS|ItemCollection")
int32 GetItemAmount(TSoftObjectPtr<UGIS_ItemDefinition> ItemDefinition, bool CountStacks = false) const;
/**
* Gets all item information in the collection.
* 获取集合中的所有道具信息。
* @return Array of item information. 道具信息数组。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection")
TArray<FGIS_ItemInfo> GetAllItemInfos() const;
/**
* Gets all item instances in the collection.
* 获取集合中的所有道具实例。
* @return Array of item instances. 道具实例数组。
*/
TArray<UGIS_ItemInstance*> GetAllItems() const;
#pragma endregion GetReference
#pragma region Giver
/**
* Gives an item to another collection.
* 将道具给予另一个集合。
* @param ItemInfo The item information to give. 要给予的道具信息。
* @param ItemCollection The target collection to receive the item. 接收道具的目标集合。
* @return The item information that was given. 给予的道具信息。
*/
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemCollection")
virtual FGIS_ItemInfo GiveItem(const FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ItemCollection);
/**
* Server function to give an item to another collection.
* 服务器函数,将道具给予另一个集合。
* @param ItemInfo The item information to give. 要给予的道具信息。
* @param ItemCollection The target collection to receive the item. 接收道具的目标集合。
*/
UFUNCTION(Server, Reliable, BlueprintCallable, Category="GIS|ItemCollection")
void ServerGiveItem(const FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ItemCollection);
/**
* Implementation of ServerGiveItem.
* ServerGiveItem 的实现。
* @param ItemInfo The item information to give. 要给予的道具信息。
* @param ItemCollection The target collection to receive the item. 接收道具的目标集合。
*/
virtual void ServerGiveItem_Implementation(const FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ItemCollection);
/**
* Gives all items in this collection to another collection.
* 将此集合中的所有道具给予另一个集合。
* @param OtherItemCollection The target collection to receive the items. 接收道具的目标集合。
*/
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemCollection")
virtual void GiveAllItems(UGIS_ItemCollection* OtherItemCollection);
/**
* Server function to give all items to another collection.
* 服务器函数,将所有道具给予另一个集合。
* @param OtherItemCollection The target collection to receive the items. 接收道具的目标集合。
*/
UFUNCTION(Server, Reliable, BlueprintCallable, Category="GIS|ItemCollection")
void ServerGiveAllItems(UGIS_ItemCollection* OtherItemCollection);
/**
* Calculates how many items can fit given a limited number of additional stacks.
* 计算在有限额外栈数下可以容纳的道具数量。
* @param ItemInfo The item information to check. 要检查的道具信息。
* @param AvailableAdditionalStacks The number of additional stacks allowed. 允许的额外栈数。
* @return The number of items that can fit. 可容纳的道具数量。
*/
virtual int32 GetItemAmountFittingInLimitedAdditionalStacks(const FGIS_ItemInfo& ItemInfo, int32 AvailableAdditionalStacks) const;
#pragma endregion Giver
/**
* Event triggered when the item collection is updated.
* 道具集合更新时触发的事件。
*/
UPROPERTY(BlueprintAssignable)
FGIS_Collection_UpdateSignature OnItemCollectionUpdate;
#pragma region ItemStacks
/**
* Gets all item stacks in the collection.
* 获取集合中的所有道具栈。
* @return Array of item stacks. 道具栈数组。
*/
const TArray<FGIS_ItemStack>& GetAllItemStacks() const;
/**
* Gets the number of item stacks in the collection.
* 获取集合中的道具栈数量。
* @return The number of item stacks. 道具栈数量。
*/
int32 GetItemStacksNum() const;
protected:
/**
* Adds an item stack to the collection.
* 将道具栈添加到集合。
* @param Stack The item stack to add. 要添加的道具栈。
*/
virtual void AddItemStack(const FGIS_ItemStack& Stack);
/**
* Removes an item stack at a specific index.
* 在指定索引移除道具栈。
* @param Idx The index of the stack to remove. 要移除的栈索引。
* @param bRemoveFromCollection Whether to remove the item instance from the collection. 是否从集合中移除道具实例。
*/
virtual void RemoveItemStackAtIndex(int32 Idx, bool bRemoveFromCollection = true);
/**
* Updates the amount of an item stack at a specific index.
* 更新指定索引的道具栈数量。
* @param Idx The index of the stack to update. 要更新的栈索引。
* @param NewAmount The new amount for the stack. 栈的新数量。
*/
virtual void UpdateItemStackAmountAtIndex(int32 Idx, int32 NewAmount);
/**
* Called before an item stack is added (server-side only).
* 在添加道具栈之前调用(仅限服务器端)。
* @param Stack The item stack to add. 要添加的道具栈。
* @param Idx The index where the stack will be added. 栈将添加的索引。
*/
virtual void OnPreItemStackAdded(const FGIS_ItemStack& Stack, int32 Idx);
/**
* Called when an item stack is added (client and server).
* 道具栈添加时调用(客户端和服务器)。
* @param Stack The added item stack. 添加的道具栈。
*/
virtual void OnItemStackAdded(const FGIS_ItemStack& Stack);
/**
* Called when an item stack is removed (client and server).
* 道具栈移除时调用(客户端和服务器)。
* @param Stack The removed item stack. 移除的道具栈。
*/
virtual void OnItemStackRemoved(const FGIS_ItemStack& Stack);
/**
* Called when an item stack is updated (client and server).
* 道具栈更新时调用(客户端和服务器)。
* @param Stack The updated item stack. 更新的道具栈。
*/
virtual void OnItemStackUpdated(const FGIS_ItemStack& Stack);
/**
* Processes pending item stacks.
* 处理待处理的道具栈。
*/
void ProcessPendingItemStacks();
/**
* Store the stack id and position mapping within this collection.
* 存储在此集合中Stack id到位置的映射关系。
*/
UPROPERTY(VisibleInstanceOnly, Category="ItemCollection", Transient, meta=(ForceInlineRow))
TMap<FGuid, int32> StackToIdxMap;
/**
* Store the stack id and position mapping within this collection.
* 存储在此集合中Stack id到位置的映射关系。
*/
// UPROPERTY(VisibleInstanceOnly, Category="ItemCollection", Transient)
// TMap<FGuid, int32> IdxToStackMap;
private:
/**
* Temporary storage for pending item stacks.
* 待处理道具栈的临时存储。
*/
UPROPERTY(VisibleInstanceOnly, Category="ItemCollection", Transient)
TMap<FGuid, FGIS_ItemStack> PendingItemStacks;
#pragma endregion
protected:
/**
* Internal function to add an item to the collection.
* 内部函数,将道具添加到集合。
* @param ItemInfo The item information to add. 要添加的道具信息。
* @return The item that was actually added. 实际添加的道具。
*/
virtual FGIS_ItemInfo AddInternal(const FGIS_ItemInfo& ItemInfo);
/**
* Handles overflow when an item cannot be fully added.
* 处理道具无法完全添加时的溢出。
* @param OriginalItemInfo The original item information. 原始道具信息。
* @param ItemInfoAdded The item information that was added. 已添加的道具信息。
*/
virtual void HandleItemOverflow(const FGIS_ItemInfo& OriginalItemInfo, const FGIS_ItemInfo& ItemInfoAdded);
/**
* Internal function to remove an item from the collection.
* 内部函数,从集合中移除道具。
* @param ItemInfo The item information to remove. 要移除的道具信息。
* @return The item that was removed. 移除的道具。
*/
virtual FGIS_ItemInfo RemoveInternal(const FGIS_ItemInfo& ItemInfo);
/**
* Simplifies internal item removal logic.
* 简化内部道具移除逻辑。
* @param ItemInfo The item information to remove. 要移除的道具信息。
* @param AlreadyRemoved The amount already removed (modified). 已移除的数量(可修改)。
* @param StackIndex The stack index to remove from. 要移除的栈索引。
* @return The item stack was actually removed. 实际被移除的道具栈。
*/
virtual FGIS_ItemStack SimpleInternalItemRemove(const FGIS_ItemInfo& ItemInfo, int32& AlreadyRemoved, int32 StackIndex);
/**
* Indicates whether the collection is initialized.
* 指示集合是否已初始化。
*/
UPROPERTY(Transient)
bool bInitialized = false;
/**
* Counter for locking notifications.
* 用于锁定通知的计数器。
*/
int32 NotifyLocker = 0;
/**
* Container for item stacks.
* 道具栈的容器。
*/
UPROPERTY(VisibleInstanceOnly, Category="ItemCollection", Replicated, meta=(ShowOnlyInnerProperties))
FGIS_ItemStackContainer Container;
/**
* The unique tag of the item collection for querying in the inventory.
* 道具集合的唯一标签,用于在库存中查询。
*/
UPROPERTY(Transient)
FGameplayTag CollectionTag;
/**
* The collection definition.
* 集合定义。
*/
UPROPERTY(Transient)
TObjectPtr<const UGIS_ItemCollectionDefinition> Definition{nullptr};
/**
* The unique ID of the collection.
* 集合的唯一ID。
*/
UPROPERTY(Transient)
FGuid CollectionId;
/**
* The inventory that owns this collection.
* 拥有此集合的库存。
*/
UPROPERTY()
TObjectPtr<UGIS_InventorySystemComponent> OwningInventory;
/**
* Sets the collection definition.
* 设置集合定义。
* @param NewDefinition The new collection definition. 新的集合定义。
*/
virtual void SetDefinition(const UGIS_ItemCollectionDefinition* NewDefinition);
/**
* Sets the unique tag of the collection.
* 设置集合的唯一标签。
* @param NewTag The new collection tag. 新的集合标签。
*/
void SetCollectionTag(FGameplayTag NewTag);
/**
* Sets the unique ID of the collection.
* 设置集合的唯一ID。
* @param NewId The new collection ID. 新的集合ID。
*/
void SetCollectionId(FGuid NewId);
/**
* Sets the owning inventory for the collection.
* 设置集合的所属库存。
* @param NewInventory The new owning inventory. 新的所属库存。
*/
void SetInventory(UGIS_InventorySystemComponent* NewInventory) { OwningInventory = NewInventory; };
};

View File

@@ -0,0 +1,133 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GIS_ItemCollection.h"
#include "GIS_ItemMultiStackCollection.generated.h"
class UGIS_ItemMultiStackCollection;
/**
* Definition for a multi-stack item collection.
* 多栈道具集合的定义。
*/
UCLASS(BlueprintType)
class UGIS_ItemMultiStackCollectionDefinition : public UGIS_ItemCollectionDefinition
{
GENERATED_BODY()
public:
/**
* Constructor for the multi-stack collection definition.
* 多栈集合定义的构造函数。
*/
UGIS_ItemMultiStackCollectionDefinition();
/**
* Gets the class for instantiating the collection.
* 获取用于实例化集合的类。
* @return The collection instance class. 集合实例类。
*/
virtual TSubclassOf<UGIS_ItemCollection> GetCollectionInstanceClass() const override;
/**
* Default stack size limit for items without a StackSizeLimitAttribute.
* 没有StackSizeLimitAttribute的道具的默认栈大小限制。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StackSettings")
int32 DefaultStackSizeLimit = 99;
/**
* The integer attribute in the item definition to determine stack size (optional).
* 道具定义中用于确定栈大小的整型属性(可选)。
* @attention This is optional.
* @注意 这是可选的。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="StackSettings")
FGameplayTag StackSizeLimitAttribute;
};
/**
* An item collection that supports multiple stacks of the same item.
* 支持相同道具多栈的道具集合。
*/
UCLASS(DisplayName="GIS Item Collection (Multi Stack)")
class GENERICINVENTORYSYSTEM_API UGIS_ItemMultiStackCollection : public UGIS_ItemCollection
{
GENERATED_BODY()
public:
/**
* Constructor for the multi-stack item collection.
* 多栈道具集合的构造函数。
* @param ObjectInitializer The object initializer. 对象初始化器。
*/
UGIS_ItemMultiStackCollection(const FObjectInitializer& ObjectInitializer);
/**
* Retrieves information about an item instance in the collection.
* 获取集合中道具实例的信息。
* @param Item The item instance to query. 要查询的道具实例。
* @param OutItemInfo The item information (output). 道具信息(输出)。
* @return True if information was found, false otherwise. 如果找到信息则返回true否则返回false。
*/
virtual bool GetItemInfo(const UGIS_ItemInstance* Item, FGIS_ItemInfo& OutItemInfo) const override;
protected:
/**
* Calculates how many items can fit given a limited number of additional stacks.
* 计算在有限额外栈数下可以容纳的道具数量。
* @param ItemInfo The item information to check. 要检查的道具信息。
* @param AvailableAdditionalStacks The number of additional stacks allowed. 允许的额外栈数。
* @return The number of items that can fit. 可容纳的道具数量。
*/
virtual int32 GetItemAmountFittingInLimitedAdditionalStacks(const FGIS_ItemInfo& ItemInfo, int32 AvailableAdditionalStacks) const override;
/**
* Internal function to add an item to the collection.
* 内部函数,将道具添加到集合。
* @param ItemInfo The item information to add. 要添加的道具信息。
* @return The item that was actually added. 实际添加的道具。
*/
virtual FGIS_ItemInfo AddInternal(const FGIS_ItemInfo& ItemInfo) override;
/**
* Gets the maximum stack size for an item.
* 获取道具的最大栈大小。
* @param Item The item instance to query. 要查询的道具实例。
* @return The maximum stack size for the item. 道具的最大栈大小。
*/
int32 GetMaxStackSize(UGIS_ItemInstance* Item) const;
private:
/**
* Internal function to remove an item from the collection.
* 内部函数,从集合中移除道具。
* @param ItemInfo The item information to remove. 要移除的道具信息。
* @return The item that was removed. 移除的道具。
*/
virtual FGIS_ItemInfo RemoveInternal(const FGIS_ItemInfo& ItemInfo) override;
/**
* Removes items from a specific stack.
* 从指定栈移除道具。
* @param Index The index of the stack to remove from. 要移除的栈索引。
* @param PrevStackIndexWithSameItem The previous stack index with the same item. 具有相同道具的前一个栈索引。
* @param MaxStackSize The maximum stack size. 最大栈大小。
* @param AmountToRemove The amount to remove (modified). 要移除的数量(可修改)。
* @param AlreadyRemoved The amount already removed (modified). 已移除的数量(可修改)。
* @return The stack index with the item. 包含道具的栈索引。
*/
int32 RemoveItemFromStack(int32 Index, int32 PrevStackIndexWithSameItem, int32 MaxStackSize, int32& AmountToRemove, int32& AlreadyRemoved);
/**
* Increases the amount in a specific stack.
* 增加指定栈中的数量。
* @param StackIdx The stack index to increase. 要增加的栈索引。
* @param MaxStackSize The maximum stack size. 最大栈大小。
* @param AmountToAdd The amount to add (modified). 要添加的数量(可修改)。
* @return The amount added to the stack. 添加到栈的数量。
*/
int32 IncreaseStackAmount(int32 StackIdx, int32 MaxStackSize, int32& AmountToAdd);
};

View File

@@ -0,0 +1,120 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Items/GIS_ItemInfo.h"
#include "GIS_ItemRestriction.generated.h"
/**
* Base class for item restrictions used to limit item collection operations.
* 用于限制道具集合操作的道具限制基类。
* @details Subclasses can extend this to implement custom restrictions.
* @细节 子类可以扩展此类以实现自定义限制。
*/
UCLASS(Blueprintable, BlueprintType, DefaultToInstanced, EditInlineNew, CollapseCategories, Abstract, Const)
class GENERICINVENTORYSYSTEM_API UGIS_ItemRestriction : public UObject
{
GENERATED_BODY()
public:
/**
* Checks if an item can be added to the collection.
* 检查道具是否可以添加到集合。
* @param ItemInfo The item information to check (modifiable). 要检查的道具信息(可修改)。
* @param ReceivingCollection The collection to add the item to. 要添加到的集合。
* @return True if any valid item can be added, false otherwise. 如果可以添加任何有效道具则返回true否则返回false。
*/
UFUNCTION(BlueprintCallable, Category=ItemRestriction)
bool CanAddItem(UPARAM(ref) FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const;
/**
* Checks if an item can be removed from the collection.
* 检查道具是否可以从集合中移除。
* @param ItemInfo The item information to check (modifiable). 要检查的道具信息(可修改)。
* @return True if any valid item can be removed, false otherwise. 如果可以移除任何有效道具则返回true否则返回false。
*/
UFUNCTION(BlueprintCallable, Category=ItemRestriction)
bool CanRemoveItem(UPARAM(ref) FGIS_ItemInfo& ItemInfo) const;
/**
* Gets the reason for rejecting an add operation.
* 获取拒绝添加操作的原因。
* @return The rejection reason text. 拒绝原因文本。
*/
UFUNCTION(BlueprintPure, BlueprintNativeEvent, Category=ItemRestriction)
FText GetRejectAddReason() const;
/**
* Implementation of GetRejectAddReason.
* GetRejectAddReason 的实现。
* @return The rejection reason text. 拒绝原因文本。
*/
virtual FText GetRejectAddReason_Implementation() const { return RejectAddReason; }
/**
* Gets the reason for rejecting a remove operation.
* 获取拒绝移除操作的原因。
* @return The rejection reason text. 拒绝原因文本。
*/
UFUNCTION(BlueprintPure, BlueprintNativeEvent, Category=ItemRestriction)
FText GetRejectRemoveReason() const;
/**
* Implementation of GetRejectRemoveReason.
* GetRejectRemoveReason 的实现。
* @return The rejection reason text. 拒绝原因文本。
*/
virtual FText GetRejectRemoveReason_Implementation() const { return RejectAddReason; }
protected:
/**
* Internal check for adding an item to the collection.
* 检查道具是否可以添加到集合的内部函数。
* @param ItemInfo The item information to check (modifiable). 要检查的道具信息(可修改)。
* @param ReceivingCollection The collection to add the item to. 要添加到的集合。
* @return True if the item can be added, false otherwise. 如果道具可以添加则返回true否则返回false。
*/
UFUNCTION(BlueprintNativeEvent, Category=ItemRestriction, meta=(DisplayName=CanAddItem))
bool CanAddItemInternal(UPARAM(ref) FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const;
/**
* Implementation of CanAddItemInternal.
* CanAddItemInternal 的实现。
* @param ItemInfo The item information to check (modifiable). 要检查的道具信息(可修改)。
* @param ReceivingCollection The collection to add the item to. 要添加到的集合。
* @return True if the item can be added, false otherwise. 如果道具可以添加则返回true否则返回false。
*/
virtual bool CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const;
/**
* Internal check for removing an item from the collection.
* 检查道具是否可以从集合移除的内部函数。
* @param ItemInfo The item information to check (modifiable). 要检查的道具信息(可修改)。
* @return True if the item can be removed, false otherwise. 如果道具可以移除则返回true否则返回false。
*/
UFUNCTION(BlueprintNativeEvent, Category=ItemRestriction, meta=(DisplayName=CanRemoveItem))
bool CanRemoveItemInternal(UPARAM(ref) FGIS_ItemInfo& ItemInfo) const;
/**
* Implementation of CanRemoveItemInternal.
* CanRemoveItemInternal 的实现。
* @param ItemInfo The item information to check (modifiable). 要检查的道具信息(可修改)。
* @return True if the item can be removed, false otherwise. 如果道具可以移除则返回true否则返回false。
*/
virtual bool CanRemoveItemInternal_Implementation(FGIS_ItemInfo& ItemInfo) const;
/**
* The reason for rejecting an add operation.
* 拒绝添加操作的原因。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=ItemRestriction)
FText RejectAddReason;
/**
* The reason for rejecting a remove operation.
* 拒绝移除操作的原因。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=ItemRestriction)
FText RejectRemoveReason;
};

View File

@@ -0,0 +1,60 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GIS_ItemRestriction.h"
#include "GIS_ItemRestriction_StackSizeLimit.generated.h"
/**
* Restricts the maximum amount of an item within a specified collection.
* 限制指定集合中道具的最大数量。
* @attention Not suitable for MultiStackItemCollection.
* @注意 不适用于MultiStackItemCollection。
*/
UCLASS(NotBlueprintable, DisplayName=ItemRestriction_StackSizeLimit)
class GENERICINVENTORYSYSTEM_API UGIS_ItemRestriction_StackSizeLimit final : public UGIS_ItemRestriction
{
GENERATED_BODY()
public:
/**
* Constructor for the stack size limit restriction.
* 栈大小限制的构造函数。
*/
UGIS_ItemRestriction_StackSizeLimit();
protected:
/**
* Internal check for adding an item to the collection.
* 检查道具是否可以添加到集合的内部函数。
* @param ItemInfo The item information to check (modifiable). 要检查的道具信息(可修改)。
* @param ReceivingCollection The collection to add the item to. 要添加到的集合。
* @return True if the item can be added within the stack size limit, false otherwise. 如果道具可以在栈大小限制内添加则返回true否则返回false。
*/
virtual bool CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const override;
/**
* Gets the stack size limit for a specific item.
* 获取指定道具的栈大小限制。
* @param Item The item instance to query. 要查询的道具实例。
* @return The stack size limit for the item. 道具的栈大小限制。
*/
int32 GetStackSizeLimit(const UGIS_ItemInstance* Item) const;
/**
* The default stack size limit for any items.
* 针对所有道具的默认栈大小限制。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemRestriction)
int32 DefaultStackSizeLimit;
/**
* The integer attribute in the item definition used to limit the stack size for non-unique items (unique items cannot stack).
* 道具定义中用于限制非唯一道具栈大小的整型属性(唯一道具无法堆叠)。
* @attention This is optional.
* @注意 这是可选的。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemRestriction)
FGameplayTag StackSizeLimitAttributeTag;
};

View File

@@ -0,0 +1,34 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GIS_ItemRestriction.h"
#include "GIS_ItemRestriction_StacksNumLimit.generated.h"
/**
* Restricts the total number of stacks for an item collection.
* 限制道具集合的总堆叠数量。
*/
UCLASS(NotBlueprintable, DisplayName=ItemRestriction_StacksNumLimit)
class GENERICINVENTORYSYSTEM_API UGIS_ItemRestriction_StacksNumLimit final : public UGIS_ItemRestriction
{
GENERATED_BODY()
protected:
/**
* Checks if an item can be added to the collection based on stack limits.
* 检查是否可以根据堆叠限制将道具添加到集合。
* @param ItemInfo Information about the item to add. 要添加的道具信息。
* @param ReceivingCollection The collection receiving the item. 接收道具的集合。
* @return True if the item can be added, false otherwise. 如果可以添加道具则返回true否则返回false。
*/
virtual bool CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const override;
/**
* Maximum number of stacks allowed in the collection.
* 集合中允许的最大堆叠数量。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemRestriction)
int32 MaxStacksNum = 30;
};

View File

@@ -0,0 +1,34 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GIS_ItemRestriction.h"
#include "GIS_ItemRestriction_TagRequirements.generated.h"
/**
* Restricts items to those that satisfy a specific tag query for addition to a collection.
* 限制只有满足特定标签查询的道具可以添加到集合。
*/
UCLASS(NotBlueprintable, DisplayName=ItemRestriction_TagRequirements)
class GENERICINVENTORYSYSTEM_API UGIS_ItemRestriction_TagRequirements final : public UGIS_ItemRestriction
{
GENERATED_BODY()
protected:
/**
* Tag query that items must satisfy to be added to the collection.
* 道具必须满足的标签查询才能添加到集合。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemRestriction)
FGameplayTagQuery TagQuery;
/**
* Checks if an item satisfies the tag query for addition to the collection.
* 检查道具是否满足标签查询以添加到集合。
* @param ItemInfo Information about the item to add. 要添加的道具信息。
* @param ReceivingCollection The collection receiving the item. 接收道具的集合。
* @return True if the item can be added, false otherwise. 如果可以添加道具则返回true否则返回false。
*/
virtual bool CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const override;
};

View File

@@ -0,0 +1,29 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GIS_ItemRestriction.h"
#include "GIS_ItemRestriction_UniqueOnly.generated.h"
/**
* Restricts a collection to only accept unique items.
* 限制集合仅接受唯一道具。
* @details Prevents duplicate items from being added to the collection.
* @细节 防止重复道具被添加到集合。
*/
UCLASS(NotBlueprintable, DisplayName=ItemRestriction_UniqueOnly)
class GENERICINVENTORYSYSTEM_API UGIS_ItemRestriction_UniqueOnly final : public UGIS_ItemRestriction
{
GENERATED_BODY()
protected:
/**
* Checks if an item is unique before adding it to the collection.
* 在将道具添加到集合之前检查其是否唯一。
* @param ItemInfo Information about the item to add. 要添加的道具信息。
* @param ReceivingCollection The collection receiving the item. 接收道具的集合。
* @return True if the item is unique and can be added, false otherwise. 如果道具唯一且可添加则返回true否则返回false。
*/
virtual bool CanAddItemInternal_Implementation(FGIS_ItemInfo& ItemInfo, UGIS_ItemCollection* ReceivingCollection) const override;
};

View File

@@ -0,0 +1,418 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GIS_ItemCollection.h"
#include "GIS_ItemSlotCollection.generated.h"
/**
* A slot-based item collection definition, suitable for equipment or skill bars.
* 基于槽的道具集合定义,适合用于装备或技能栏。
* @details Stores items in a slot-to-item style, ideal for equipment, skill bars, etc.
* @细节 以槽对应道具的方式存储,适合装备、技能栏等场景。
*/
UCLASS(BlueprintType)
class GENERICINVENTORYSYSTEM_API UGIS_ItemSlotCollectionDefinition : public UGIS_ItemCollectionDefinition
{
GENERATED_BODY()
public:
/**
* Gets the class for instantiating the collection.
* 获取用于实例化集合的类。
* @return The collection instance class. 集合实例类。
*/
virtual TSubclassOf<UGIS_ItemCollection> GetCollectionInstanceClass() const override;
/**
* Checks if the specified slot index is valid within SlotDefinitions.
* 检查指定槽索引在SlotDefinitions中是否有效。
* @param SlotIndex The slot index to check. 要检查的槽索引。
* @return True if the slot index is valid, false otherwise. 如果槽索引有效则返回true否则返回false。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|CollectionDefinition")
virtual bool IsValidSlotIndex(int32 SlotIndex) const;
/**
* Gets the index of a slot based on its tag within SlotDefinitions.
* 根据槽标签获取SlotDefinitions中的槽索引。
* @param SlotName The slot tag to query. 要查询的槽标签。
* @return The index of the slot, or INDEX_NONE if not found. 槽的索引如果未找到则返回INDEX_NONE。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|CollectionDefinition")
virtual int32 GetIndexOfSlot(UPARAM(meta=(Categories="GIS.Slots"))
const FGameplayTag& SlotName) const;
/**
* Gets the slot tag for a given slot index within SlotDefinitions.
* 根据槽索引获取SlotDefinitions中的槽标签。
* @param SlotIndex The slot index to query. 要查询的槽索引。
* @return The slot tag, or invalid tag if not found. 槽标签,如果未找到则返回无效标签。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|CollectionDefinition")
virtual FGameplayTag GetSlotOfIndex(int32 SlotIndex) const;
/**
* Gets all slot definitions within this collection definition.
* 获取此集合定义中的所有槽定义。
* @return Array of slot definitions. 槽定义数组。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|CollectionDefinition")
const TArray<FGIS_ItemSlotDefinition>& GetSlotDefinitions() const;
/**
* Gets the slot definition for a given slot index.
* 获取指定槽索引的槽定义。
* @param SlotIndex The slot index to query. 要查询的槽索引。
* @param OutDefinition The slot definition (output). 槽定义(输出)。
* @return True if the slot definition was found, false otherwise. 如果找到槽定义则返回true否则返回false。
*/
bool GetSlotDefinition(int32 SlotIndex, FGIS_ItemSlotDefinition& OutDefinition) const;
/**
* Gets the slot definition for a given slot tag.
* 获取指定槽标签的槽定义。
* @param SlotName The slot tag to query. 要查询的槽标签。
* @param OutDefinition The slot definition (output). 槽定义(输出)。
* @return True if the slot definition was found, false otherwise. 如果找到槽定义则返回true否则返回false。
*/
bool GetSlotDefinition(const FGameplayTag& SlotName, FGIS_ItemSlotDefinition& OutDefinition) const;
/**
* Gets the index of a slot within a specified group.
* 获取指定槽在指定槽组内的索引。
* @param GroupTag The group tag to query. 要查询的组标签。
* @param SlotTag The slot tag to query. 要查询的槽标签。
* @return The index of the slot within the group, or -1 if not found. 槽在组内的索引,如果未找到则返回-1。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|CollectionDefinition")
int32 GetSlotIndexWithinGroup(UPARAM(meta=(Categories="GIS.Slots"))
FGameplayTag GroupTag, FGameplayTag SlotTag) const;
/**
* Whether newly added items replace existing items if they are not stackable.
* 新添加的道具如果无法堆叠,是否替换现有道具。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Common", meta=(DisplayAfter="OverflowOptions"))
bool bNewItemPriority = true;
/**
* Whether replaced items are returned to the source collection of the new item.
* 被替换的道具是否返回到新道具的源集合。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Common", meta=(EditCondition="bNewItemPriority"))
bool bTryGivePrevItemToNewItemCollection = true;
/**
* Defines all available item slots in the collection.
* 定义集合中的所有可用道具槽。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category="SlotSettings", meta=(TitleProperty="SlotName:{Name}"))
TArray<FGIS_ItemSlotDefinition> SlotDefinitions;
/**
* List of parent slot tags for grouping slots; only one slot per group can be active at a time.
* 用于分组的父级槽标签列表,每个组同时只能激活一个槽。
* @attention Used by the equipment system.
* @注意 由装备系统使用。
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="SlotSettings", meta=(AllowPrivateAccess=True, Categories="GIS.Slots"))
TArray<FGameplayTag> SlotGroups;
/**
* Cached mapping of slot indices to slot tags for faster access, updated on save in the editor.
* 槽索引到槽标签的缓存映射,用于快速访问,在编辑器保存时更新。
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Advanced", meta=(ForceInlineRow))
TMap<int32, FGameplayTag> IndexToTagMap;
/**
* Cached mapping of slot tags to slot indices for faster access, updated on save in the editor.
* 槽标签到槽索引的缓存映射,用于快速访问,在编辑器保存时更新。
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Advanced", meta=(ForceInlineRow))
TMap<FGameplayTag, int32> TagToIndexMap;
/**
* Cached grouping of slot definitions by slot groups, updated on save in the editor.
* 按槽组分组的槽定义缓存,在编辑器保存时更新。
*/
UPROPERTY(VisibleAnywhere, Category="Advanced", meta=(ForceInlineRow))
TMap<FGameplayTag, FGIS_ItemSlotGroup> SlotGroupMap;
#if WITH_EDITOR
/**
* Called before saving the object in the editor.
* 在编辑器中保存对象前调用。
* @param SaveContext The save context. 保存上下文。
*/
virtual void PreSave(FObjectPreSaveContext SaveContext) override;
#endif
};
/**
* A slot-based item collection that organizes items in slots, each holding an item stack.
* 基于槽的道具集合,按槽组织道具,每个槽可持有道具栈。
*/
UCLASS(BlueprintType, EditInlineNew, CollapseCategories, DisplayName="GIS Item Collection (Slotted)")
class GENERICINVENTORYSYSTEM_API UGIS_ItemSlotCollection : public UGIS_ItemCollection
{
GENERATED_BODY()
friend UGIS_ItemSlotCollectionDefinition;
public:
/**
* Constructor for the slot-based item collection.
* 基于槽的道具集合的构造函数。
* @param ObjectInitializer The object initializer. 对象初始化器。
*/
UGIS_ItemSlotCollection(const FObjectInitializer& ObjectInitializer);
/**
* Gets the properties that should be replicated for this object.
* 获取需要为此对象复制的属性。
* @param OutLifetimeProps Array to store the replicated properties. 存储复制属性的数组。
*/
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
/**
* Gets the definition of this slot-based collection.
* 获取此基于槽的集合的定义。
* @return The collection definition, or nullptr if not set. 集合定义如果未设置则返回nullptr。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection")
const UGIS_ItemSlotCollectionDefinition* GetMyDefinition() const;
/**
* Adds an item to the collection, typically for unique items.
* 将道具添加到集合,通常用于唯一道具。
* @param ItemInfo The item information to add. 要添加的道具信息。
* @return The item that was not added, the previously equipped item if replaced, or empty if added successfully. 未添加的道具、替换的先前道具或成功添加时为空。
*/
virtual FGIS_ItemInfo AddItem(const FGIS_ItemInfo& ItemInfo) override;
/**
* Adds an item to the specified slot (authority only).
* 将道具添加到指定槽(仅限权限)。
* @param ItemInfo The item information to add. 要添加的道具信息。
* @param SlotIndex The index of the slot to add the item to. 要添加到的槽索引。
* @return The item that was actually added. 实际添加的道具。
*/
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemCollection")
virtual FGIS_ItemInfo AddItem(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex);
/**
* Server function to add an item to the specified slot.
* 服务器函数,将道具添加到指定槽。
* @param ItemInfo The item information to add. 要添加的道具信息。
* @param SlotIndex The index of the slot to add the item to. 要添加到的槽索引。
*/
UFUNCTION(Server, Reliable, BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemCollection")
void ServerAddItem(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex);
/**
* Adds an item to the slot with the specified tag (authority only).
* 将道具添加到指定标签的槽(仅限权限)。
* @param ItemInfo The item information to add. 要添加的道具信息。
* @param SlotName The tag of the slot to add the item to. 要添加到的槽标签。
* @return The item that was actually added. 实际添加的道具。
*/
UFUNCTION(BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemCollection")
FGIS_ItemInfo AddItemBySlotName(const FGIS_ItemInfo& ItemInfo, FGameplayTag SlotName);
/**
* Server function to add an item to the slot with the specified tag.
* 服务器函数,将道具添加到指定标签的槽。
* @param ItemInfo The item information to add. 要添加的道具信息。
* @param SlotName The tag of the slot to add the item to. 要添加到的槽标签。
*/
UFUNCTION(Server, Reliable, BlueprintCallable, BlueprintAuthorityOnly, Category="GIS|ItemCollection")
void ServerAddItemBySlotName(const FGIS_ItemInfo& ItemInfo, FGameplayTag SlotName);
/**
* Removes an item from the collection.
* 从集合中移除道具。
* @param ItemInfo The item information to remove. 要移除的道具信息。
* @return The item that was removed, or empty if nothing was removed. 移除的道具,如果未移除则为空。
*/
virtual FGIS_ItemInfo RemoveItem(const FGIS_ItemInfo& ItemInfo) override;
/**
* Removes an item from the specified slot.
* 从指定槽移除道具。
* @param SlotIndex The index of the slot to remove the item from. 要移除道具的槽索引。
* @param Amount The amount to remove (-1 to remove all). 要移除的数量(-1表示全部移除
* @return The item information that was removed. 移除的道具信息。
*/
virtual FGIS_ItemInfo RemoveItem(int32 SlotIndex, int32 Amount = -1);
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection")
bool IsItemFitWithSlot(const UGIS_ItemInstance* Item, int32 SlotIndex) const;
/**
* Gets a suitable slot index for an incoming item.
* 获取适合传入道具的槽索引。
* @param Item The item instance to check. 要检查的道具实例。
* @return A valid slot index, or INDEX_NONE if none is suitable. 有效槽索引如果没有合适的槽则返回INDEX_NONE。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection")
int32 GetTargetSlotIndex(const UGIS_ItemInstance* Item) const;
/**
* Gets the item information at a specific slot by tag.
* 通过槽标签获取指定槽的道具信息。
* @param SlotTag The slot tag to query. 要查询的槽标签。
* @param OutItemInfo The item information (output). 道具信息(输出)。
* @return True if item information was found, false otherwise. 如果找到道具信息则返回true否则返回false。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection")
virtual bool GetItemInfoAtSlot(FGameplayTag SlotTag, FGIS_ItemInfo& OutItemInfo) const;
/**
* Finds the item information at a specific slot by tag.
* 通过槽标签查找指定槽的道具信息。
* @param SlotTag The slot tag to query. 要查询的槽标签。
* @param OutItemInfo The item information (output). 道具信息(输出)。
* @return True if item information was found, false otherwise. 如果找到道具信息则返回true否则返回false。
*/
UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|ItemCollection", meta=(ExpandBoolAsExecs="ReturnValue"))
virtual bool FindItemInfoAtSlot(FGameplayTag SlotTag, FGIS_ItemInfo& OutItemInfo) const;
/**
* Gets the item stack at a specific slot by tag (commented out).
* 通过槽标签获取指定槽的道具栈(已注释)。
*/
virtual bool GetItemStackAtSlot(FGameplayTag SlotTag, FGIS_ItemStack& OutItemStack) const;
/**
* Finds the item stack at a specific slot by tag (commented out).
* 通过槽标签查找指定槽的道具栈(已注释)。
*/
virtual bool FindItemStackAtSlot(FGameplayTag SlotTag, FGIS_ItemStack& OutItemStack) const;
/**
* Gets the item information at a specific slot by index.
* 通过槽索引获取指定槽的道具信息。
* @param SlotIndex The slot index to query. 要查询的槽索引。
* @return The item information at the slot. 槽中的道具信息。
*/
FGIS_ItemInfo GetItemInfoAtSlot(int32 SlotIndex) const;
/**
* Gets the item stack at a specific slot by index.
* 通过槽索引获取指定槽的道具栈。
* @param SlotIndex The slot index to query. 要查询的槽索引。
* @return The item stack at the slot. 槽中的道具栈。
*/
FGIS_ItemStack GetItemStackAtSlot(int32 SlotIndex) const;
/**
* Gets the slot tag where an item is equipped.
* 获取道具装备的槽标签。
* @param Item The item instance to query. 要查询的道具实例。
* @return The slot tag, or invalid if not equipped. 槽标签,如果未装备则返回无效标签。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection")
FGameplayTag GetItemSlotName(const UGIS_ItemInstance* Item) const;
/**
* Gets the slot index where an item is equipped.
* 获取道具装备的槽索引。
* @param Item The item instance to query. 要查询的道具实例。
* @return The slot index, or -1 if not equipped. 槽索引,如果未装备则返回-1。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection")
int32 GetItemSlotIndex(const UGIS_ItemInstance* Item) const;
/**
* Internal function to add an item to a specific slot.
* 内部函数,将道具添加到指定槽。
* @param ItemInfo The item information to add. 要添加的道具信息。
* @param SlotIndex The index of the slot to add the item to. 要添加到的槽索引。
* @return The item that was actually added. 实际添加的道具。
*/
virtual FGIS_ItemInfo AddItemInternal(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex);
/**
* Converts a stack ID to a slot index.
* 将栈ID转换为槽索引。
* @param InStackId The stack ID to query. 要查询的栈ID。
* @return The corresponding slot index, or -1 if not found. 对应的槽索引,如果未找到则返回-1。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection")
int32 StackIdToSlotIndex(FGuid InStackId) const;
/**
* Converts a slot index to a stack index.
* 将槽索引转换为栈索引。
* @param InSlotIndex The slot index to query. 要查询的槽索引。
* @return The corresponding stack index, or -1 if not found. 对应的栈索引,如果未找到则返回-1。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection")
int32 SlotIndexToStackIndex(int32 InSlotIndex) const;
/**
* Converts a slot index to a stack ID.
* 将槽索引转换为栈ID。
* @param InSlotIndex The slot index to query. 要查询的槽索引。
* @return The corresponding stack ID, or invalid if not found. 对应的栈ID如果未找到则返回无效ID。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemCollection")
FGuid SlotIndexToStackId(int32 InSlotIndex) const;
/**
* Mapping of slot indices to stack IDs for equipped items.
* 槽索引到装备道具栈ID的映射。
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="ItemCollection", ReplicatedUsing=OnRep_ItemsBySlot)
TArray<FGuid> ItemBySlots;
UPROPERTY(VisibleInstanceOnly, Category="ItemCollection", Transient)
TMap<int32, FGuid> SlotToStackMap;
/**
* The definition of this slot-based collection.
* 此基于槽的集合的定义。
*/
UPROPERTY()
TObjectPtr<const UGIS_ItemSlotCollectionDefinition> MyDefinition;
/**
* Called when the ItemBySlots array is replicated.
* ItemBySlots数组复制时调用。
*/
UFUNCTION()
void OnRep_ItemsBySlot();
protected:
/**
* Sets the collection definition.
* 设置集合定义。
* @param NewDefinition The new collection definition. 新的集合定义。
*/
virtual void SetDefinition(const UGIS_ItemCollectionDefinition* NewDefinition) override;
/**
* Called before an item stack is added to the collection.
* 在集合添加道具栈之前调用。
* @param Stack The item stack to add. 要添加的道具栈。
* @param Idx The index where the stack will be added. 栈将添加的索引。
*/
virtual void OnPreItemStackAdded(const FGIS_ItemStack& Stack, int32 Idx) override;
virtual void OnItemStackAdded(const FGIS_ItemStack& Stack) override;
virtual void OnItemStackRemoved(const FGIS_ItemStack& Stack) override;
/**
* Sets the amount for an item in a specific slot.
* 在指定槽中设置道具数量。
* @param ItemInfo The item information to set. 要设置的道具信息。
* @param SlotIndex The slot index to set the item in. 要设置的槽索引。
* @param RemovePreviousItem Whether to replace the previous item if the slot is at capacity. 如果槽已满,是否替换之前的道具。
* @param ItemInfoAdded The item information that was set (output). 已设置的道具信息(输出)。
* @return True if the item was set successfully, false otherwise. 如果道具设置成功则返回true否则返回false。
*/
virtual bool SetItemAmount(const FGIS_ItemInfo& ItemInfo, int32 SlotIndex, bool RemovePreviousItem, FGIS_ItemInfo& ItemInfoAdded);
};

View File

@@ -0,0 +1,103 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GIS_MixinTargetInterface.h"
#include "UObject/Object.h"
#include "GIS_ItemFragment.generated.h"
class UGIS_ItemInstance;
/**
* Base class for item fragments, which are data objects attached to item definitions.
* 道具片段,即道具定义上附加的数据对象的基类。
* @details Allows users to create custom item fragment classes for extending item functionality.
* @细节 允许用户创建自定义道具片段类以扩展道具功能。
*/
UCLASS(BlueprintType, Blueprintable, DefaultToInstanced, EditInlineNew, Abstract, CollapseCategories, HideDropdown, Const)
class GENERICINVENTORYSYSTEM_API UGIS_ItemFragment : public UObject, public IGIS_MixinTargetInterface
{
GENERATED_BODY()
public:
/**
* Called when an item instance is created, allowing fragment-specific initialization.
* 在道具实例创建时调用,允许特定片段的初始化。
* @param ItemInstance The item instance being created. 被创建的道具实例。
*/
virtual void OnInstanceCreated(UGIS_ItemInstance* ItemInstance) const
{
Bp_OnInstancedCrated(ItemInstance);
}
/**
* Determines if the fragment's runtime data can be serialized.
* 确定片段的运行时数据是否可以序列化。
* @return True if the data is serializable, false otherwise. 如果数据可序列化则返回true否则返回false。
*/
virtual bool IsMixinDataSerializable() const override;
/**
* Retrieves the compatible data structure for serialization or replication.
* 获取用于序列化或复制的兼容数据结构。
* @return The script struct compatible with this fragment. 与此片段兼容的脚本结构。
*/
virtual TObjectPtr<const UScriptStruct> GetCompatibleMixinDataType() const override;
/**
* Generate default runtime data for compatible data type.
* 生成兼容的混合数据类型的默认值。
* @param DefaultState The default value of compatible data. 兼容的数据结构默认值。
* @return If has default value. 是否具备默认值?
*/
virtual bool MakeDefaultMixinData(FInstancedStruct& DefaultState) const override;
protected:
/**
* Blueprint event for handling instance creation logic.
* 处理实例创建逻辑的蓝图事件。
* @param ItemInstance The item instance being created. 被创建的道具实例。
*/
UFUNCTION(BlueprintImplementableEvent, Category="ItemFragment", meta=(DisplayName="On Instance Created"))
void Bp_OnInstancedCrated(UGIS_ItemInstance* ItemInstance) const;
/**
* Returns the struct type for runtime data serialization or replication.
* 返回运行时数据序列化或复制的结构类型。
* @return The compatible runtime data struct type. 兼容的运行时数据结构类型。
*/
UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category="ItemFragment")
const UScriptStruct* GetCompatibleStateType() const;
/**
* Provide default values for fragment's runtime data type.
* 针对运行时数据类型提供默认值。
* @param DefaultState The default data. 默认数据
* @return If has default value. 是否具备默认值。
*/
UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category="ItemFragment")
bool MakeDefaultState(FInstancedStruct& DefaultState) const;
/**
* Determines if the fragment's runtime data supports serialization to disk.
* 确定片段的运行时数据是否支持序列化到磁盘。
* @details Runtime data is replicated over the network, but serialization to disk is optional.
* @细节 运行时数据通过网络复制,但序列化到磁盘是可选的。
* @return True if the data supports serialization, false otherwise. 如果数据支持序列化则返回true否则返回false。
*/
UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category="ItemFragment")
bool IsStateSerializable() const;
public:
#if WITH_EDITOR
/**
* Override this to add your custom data validation logic on save.
* @note Only called on editor, not runtime.
* @param OutMessage The output message.
* @return return true if no any data validation errors.
*/
UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category="ItemFragment")
bool FragmentDataValidation(FText& OutMessage) const;
#endif
};

View File

@@ -0,0 +1,89 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GIS_ItemFragment.h"
#include "Attributes/GIS_GameplayTagFloat.h"
#include "Attributes/GIS_GameplayTagInteger.h"
#include "GIS_ItemFragment_DynamicAttributes.generated.h"
/**
* Item fragment for adding dynamic attributes to an item instance.
* 为道具实例添加动态属性的道具片段。
* @details Initial attributes are applied to the item instance upon creation.
* @细节 初始属性在道具实例创建时应用。
* @attention Use static attributes in the item definition for attributes that don't change at runtime.
* @注意 对于运行时不更改的属性,请使用道具定义中的静态属性。
*/
UCLASS(DisplayName="Dynamic Attribute Settings", Category="BuiltIn")
class GENERICINVENTORYSYSTEM_API UGIS_ItemFragment_DynamicAttributes : public UGIS_ItemFragment
{
GENERATED_BODY()
protected:
/**
* Array of initial float attributes applied to
System: the item instance.
* 应用于道具实例的初始浮点属性数组。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Attribute, meta=(TitleProperty="{Tag} -> {Value}"))
TArray<FGIS_GameplayTagFloat> InitialFloatAttributes;
/**
* Array of initial integer attributes applied to the item instance.
* 应用于道具实例的初始整型属性数组。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Attribute, meta=(TitleProperty="{Tag} -> {Value}"))
TArray<FGIS_GameplayTagInteger> InitialIntegerAttributes;
/**
* Cached map for faster access to integer attributes.
* 用于快速访问整型属性的缓存映射。
*/
UPROPERTY()
TMap<FGameplayTag, int32> IntegerAttributeMap;
/**
* Cached map for faster access to float attributes.
* 用于快速访问浮点属性的缓存映射。
*/
UPROPERTY()
TMap<FGameplayTag, float> FloatAttributeMap;
public:
/**
* Initializes the item instance with dynamic attributes upon creation.
* 在道具实例创建时使用动态属性进行初始化。
* @param Instance The item instance to initialize. 要初始化的道具实例。
*/
virtual void OnInstanceCreated(UGIS_ItemInstance* Instance) const override;
/**
* Retrieves the default value of a float attribute by tag.
* 通过标签获取浮点属性的默认值。
* @param AttributeTag The tag identifying the attribute. 标识属性的标签。
* @return The default value of the float attribute. 浮点属性的默认值。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemFragment|DynamicAttributes")
float GetFloatAttributeDefault(FGameplayTag AttributeTag) const;
/**
* Retrieves the default value of an integer attribute by tag.
* 通过标签获取整型属性的默认值。
* @param AttributeTag The tag identifying the attribute. 标识属性的标签。
* @return The default value of the integer attribute. 整型属性的默认值。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemFragment|DynamicAttributes")
int32 GetIntegerAttributeDefault(FGameplayTag AttributeTag) const;
#if WITH_EDITOR
/**
* Called before saving the object to perform editor-specific operations.
* 在保存对象之前调用以执行编辑器特定的操作。
* @param SaveContext The context for the save operation. 保存操作的上下文。
*/
virtual void PreSave(FObjectPreSaveContext SaveContext) override;
#endif
};

View File

@@ -0,0 +1,66 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GIS_EquipmentStructLibrary.h"
#include "GIS_ItemFragment.h"
#include "GIS_ItemFragment_Equippable.generated.h"
class UGIS_EquipmentInstance;
/**
* Item fragment for defining equippable item behavior.
* 定义可装备道具行为的道具片段。
* @details Configures equipment instance and spawning behavior for equipped items.
* @细节 配置装备实例和已装备道具的生成行为。
*/
UCLASS(DisplayName="Equippable Settings", Category="BuiltIn")
class GENERICINVENTORYSYSTEM_API UGIS_ItemFragment_Equippable : public UGIS_ItemFragment
{
GENERATED_BODY()
public:
/**
* Constructor for the equippable item fragment.
* 可装备道具片段的构造函数。
*/
UGIS_ItemFragment_Equippable();
/**
* Specifies the equipment instance class handling equipment logic.
* 指定处理装备逻辑的装备实例类。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Equipment, NoClear, meta = (MustImplement = "/Script/GenericInventorySystem.GIS_EquipmentInterface", AllowAbstract = "false"))
TSoftClassPtr<UObject> InstanceType;
/**
* Indicates if the equipment instance is actor-based, set automatically based on InstanceType.
* 指示装备实例是否基于Actor根据InstanceType自动设置。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Equipment, meta=(EditCondition="false"))
bool bActorBased{false};
/**
* Determines if the equipment instance is automatically activated upon equipping.
* 确定装备实例在装备时是否自动激活。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Equipment)
bool bAutoActivate{false};
/**
* List of actors to spawn on the pawn when the item is equipped, such as weapons or armor.
* 在道具装备时在Pawn上生成的Actor列表如武器或盔甲。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Equipment, meta=(EditCondition="!bActorBased", EditConditionHides))
TArray<FGIS_EquipmentActorToSpawn> ActorsToSpawn;
#if WITH_EDITOR
/**
* Called before saving the object to perform editor-specific operations.
* 在保存对象之前调用以执行编辑器特定的操作。
* @param SaveContext The context for the save operation. 保存操作的上下文。
*/
virtual void PreSave(FObjectPreSaveContext SaveContext) override;
#endif
};

View File

@@ -0,0 +1,42 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GIS_CurrencyEntry.h"
#include "GIS_ItemFragment.h"
#include "GIS_ItemFragment_Shoppable.generated.h"
/**
* Item fragment for defining shop-related properties of an item.
* 定义道具商店相关属性的道具片段。
* @details Specifies buy and sell prices for the item in various currencies.
* @细节 指定道具在不同货币中的购买和出售价格。
*/
UCLASS(DisplayName="Shoppable Settings", Category="BuiltIn")
class GENERICINVENTORYSYSTEM_API UGIS_ItemFragment_Shoppable : public UGIS_ItemFragment
{
GENERATED_BODY()
public:
/**
* Initializes shop-related data for the item instance upon creation.
* 在道具实例创建时初始化商店相关数据。
* @param Instance The item instance to initialize. 要初始化的道具实例。
*/
virtual void OnInstanceCreated(UGIS_ItemInstance* Instance) const override;
/**
* List of currencies and amounts required to purchase the item.
* 购买道具所需的货币和金额列表。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category=Shoppable, meta=(TitleProperty="Definition"))
TArray<FGIS_CurrencyEntry> BuyCurrencyAmounts;
/**
* List of currencies and amounts received when selling the item.
* 出售道具时获得的货币和金额列表。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category=Shoppable, meta=(TitleProperty="Definition"))
TArray<FGIS_CurrencyEntry> SellCurrencyAmounts;
};

View File

@@ -0,0 +1,188 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Engine/Texture2D.h"
#include "Styling/SlateBrush.h"
#include "GameplayTagContainer.h"
#include "UObject/Object.h"
#include "GIS_CoreStructLibray.generated.h"
class UGIS_ItemInstance;
class UGIS_ItemDefinition;
/**
* Structure representing an item definition with an associated amount.
* 表示道具定义及其关联数量的结构体。
*/
USTRUCT(BlueprintType)
struct GENERICINVENTORYSYSTEM_API FGIS_ItemDefinitionAmount
{
GENERATED_BODY()
FGIS_ItemDefinitionAmount();
FGIS_ItemDefinitionAmount(TSoftObjectPtr<UGIS_ItemDefinition> InDefinition, int32 InAmount);
/**
* The item definition.
* 道具定义。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS")
TSoftObjectPtr<UGIS_ItemDefinition> Definition;
/**
* The amount of the item.
* 道具数量。
* @attention Minimum value is 1.
* @注意 最小值为1。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS", meta=(ClampMin="1"))
int32 Amount = 1;
#if WITH_EDITORONLY_DATA
/**
* Friendly name for displaying in the editor.
* 在编辑器中显示的友好名称。
*/
UPROPERTY(VisibleAnywhere, Category=AlwaysHidden, Meta=(EditCondition=False, EditConditionHides))
FString EditorFriendlyName;
#endif
};
/**
* Structure representing a default loadout with a tag and associated items.
* 表示默认装备配置的结构体,包含标签和关联的道具。
*/
USTRUCT(BlueprintType)
struct GENERICINVENTORYSYSTEM_API FGIS_DefaultLoadout
{
GENERATED_BODY()
/**
* The gameplay tag for the loadout.
* 装备配置的游戏标签。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS")
FGameplayTag Tag;
/**
* List of default items for the loadout.
* 装备配置的默认道具列表。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS", meta=(TitleProperty="EditorFriendlyName"))
TArray<FGIS_ItemDefinitionAmount> DefaultItems;
};
/**
* Structure defining options for handling item overflow in an inventory.
* 定义库存中道具溢出处理选项的结构体。
*/
USTRUCT()
struct GENERICINVENTORYSYSTEM_API FGIS_ItemOverflowOptions
{
GENERATED_BODY()
/**
* Whether to return overflow items to their source (if specified) when the inventory is full.
* 当库存满时,是否将溢出的道具返回其来源(如果指定)。
*/
UPROPERTY(EditAnywhere, Category=Overflow)
bool bReturnOverflow = true;
/**
* Whether to send a rejection message when items cannot be added due to overflow.
* 当道具因溢出无法添加时,是否发送拒绝消息。
*/
UPROPERTY(EditAnywhere, Category=Overflow)
bool bSendRejectedMessage = true;
};
/**
* Structure representing an item slot definition, primarily used in item slot collections.
* 表示道具槽定义的结构体,主要用于道具槽集合。
*/
USTRUCT(BlueprintType)
struct GENERICINVENTORYSYSTEM_API FGIS_ItemSlotDefinition
{
GENERATED_BODY()
/**
* The gameplay tag for the item slot.
* 道具槽的游戏标签。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemSlot, meta=(Categories="GIS.Slots"))
FGameplayTag Tag;
/**
* The icon for the item slot.
* 道具槽的图标。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemSlot)
TSoftObjectPtr<UTexture2D> Icon;
/**
* The Slate brush for the item slot icon.
* 道具槽图标的Slate画刷。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemSlot)
FSlateBrush IconBrush;
/**
* The display name of the item slot.
* 道具槽的显示名称。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemSlot)
FText Name;
/**
* The description of the item slot.
* 道具槽的描述。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemSlot)
FText Desc;
/**
* Gameplay tag query that item tags must match to be equipped in this slot.
* 道具标签必须匹配的游戏标签查询,才能装备到此槽。
* @attention If empty, no items can be equipped in this slot.
* @注意 如果为空,则无法将任何道具装备到此槽。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=ItemSlot)
FGameplayTagQuery TagQuery;
/**
* Checks if an item can be equipped in this slot based on its tags.
* 检查道具是否可以根据其标签装备到此槽。
* @param Item The item instance to check. 要检查的道具实例。
* @return True if the item matches the slot's tag query, false otherwise. 如果道具匹配槽的标签查询则返回true否则返回false。
*/
bool MatchItem(const UGIS_ItemInstance* Item) const;
};
/**
* Structure representing a group of item slots with index-to-slot and slot-to-index mappings.
* 表示一组道具槽的结构体,包含索引到槽和槽到索引的映射。
*/
USTRUCT()
struct GENERICINVENTORYSYSTEM_API FGIS_ItemSlotGroup
{
GENERATED_BODY()
/**
* Mapping of indices to slot tags in the group.
* 组内索引到槽标签的映射。
*/
UPROPERTY(VisibleAnywhere, Category=ItemSlot, meta=(ForceInlineRow))
TMap<int32, FGameplayTag> IndexToSlotMap;
/**
* Mapping of slot tags to indices in the group.
* 组内槽标签到索引的映射。
*/
UPROPERTY(VisibleAnywhere, Category=ItemSlot, meta=(ForceInlineRow))
TMap<FGameplayTag, int32> SlotToIndexMap;
};

View File

@@ -0,0 +1,383 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Net/Serialization/FastArraySerializer.h"
#include "Templates/SubclassOf.h"
#include "UObject/Object.h"
#include "Runtime/Launch/Resources/Version.h"
#if ENGINE_MINOR_VERSION < 5
#include "InstancedStruct.h"
#else
#include "StructUtils/InstancedStruct.h"
#endif
#include "GIS_MixinContainer.generated.h"
/**
* Record of mixin for serialization.
* 用于序列化的混合数据记录。
*/
USTRUCT(BlueprintType)
struct GENERICINVENTORYSYSTEM_API FGIS_MixinRecord
{
GENERATED_BODY()
/**
* The asset path of target object.
* 目标对象的资产路径。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS")
FString TargetPath;
/**
* The runtime state of this fragment.
* 片段的运行时数据。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, SaveGame, Category="GIS")
FInstancedStruct Data;
/**
* Equality operator to compare mixin records.
* 比较混合数据记录的相等性运算符。
* @param Other The another mixin record to compare with. 要比较的其他Mixin记录。
* @return True if the record's target path are equal, false otherwise. 如果目标路径相等则返回true否则返回false。
*/
bool operator==(const FGIS_MixinRecord& Other) const;
/**
* Checks if the mixin record is valid.
* 检查Mixin记录是否有效。
* @return True if the target path and data are valid, false otherwise. 如果片段类型和状态有效则返回true否则返回false。
*/
bool IsValid() const;
};
/**
* Stores the mixed-in data of a const target object (attaches additional runtime data to a const object).
* 针对常量目标对象存储其混合数据(将额外的运行时数据附加到常量对象)。
* @details A wrapper of InstancedStruct, allowing storage of any struct data. For save games, ensure struct fields are serializable (marked with SaveGame).
* @细节 这是实例化结构的包装允许存储任意结构体数据。对于存档应确保结构体字段标记为SaveGame并受虚幻序列化系统支持。
*/
USTRUCT(BlueprintType)
struct FGIS_Mixin : public FFastArraySerializerItem
{
GENERATED_BODY()
/**
* The target object to which the data is attached.
* 数据附加的目标对象。
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Mixin")
TObjectPtr<const UObject> Target;
/**
* The mixed-in data attached to the target object.
* 附加到目标对象的混合数据。
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Mixin")
FInstancedStruct Data;
/**
* Timestamp of the last data change.
* 数据最后更改的时间戳。
*/
UPROPERTY()
float Timestamp;
/**
* Timestamp of the last replication (not replicated).
* 最后一次复制的时间戳(不复制)。
*/
UPROPERTY(NotReplicated)
float LastReplicatedTimestamp;
/**
* Default constructor for mixin.
* 混合数据的默认构造函数。
*/
FGIS_Mixin()
: FGIS_Mixin(nullptr, FInstancedStruct())
{
}
/**
* Constructor for mixin with target class and data.
* 使用目标类和数据构造混合数据。
* @param InClass The target class for the mixin. 混合数据的目标类。
* @param InData The instanced struct data to mix in. 要混合的实例化结构数据。
*/
FGIS_Mixin(const TSubclassOf<UObject>& InClass, const FInstancedStruct& InData)
: Target(InClass), Data(InData)
{
Timestamp = 0.f;
LastReplicatedTimestamp = 0.f;
}
/**
* Destructor for mixin.
* 混合数据的析构函数。
*/
~FGIS_Mixin()
{
Reset();
}
/**
* Checks if the mixin is valid.
* 检查混合数据是否有效。
* @return True if the mixin is valid (target and data are set), false otherwise. 如果混合数据有效目标和数据已设置则返回true否则返回false。
*/
bool IsValid() const
{
return Target != nullptr && Data.IsValid();
}
/**
* Resets the mixin to its default state.
* 将混合数据重置为默认状态。
*/
void Reset()
{
Target = nullptr;
Data.Reset();
}
/**
* Computes the hash value for the mixin entry.
* 计算混合数据条目的哈希值。
* @param Entry The mixin entry to hash. 要哈希的混合数据条目。
* @return The hash value. 哈希值。
*/
friend uint32 GetTypeHash(const FGIS_Mixin& Entry);
};
/**
* Template specialization to enable copying for FGIS_Mixin.
* 为 FGIS_Mixin 启用复制的模板特化。
*/
template <>
struct TStructOpsTypeTraits<FGIS_Mixin> : TStructOpsTypeTraitsBase2<FGIS_Mixin>
{
enum
{
WithCopy = true
};
};
/**
* Container for storing runtime data for various const objects within the owning object.
* 用于存储拥有对象中多个常量对象的运行时数据的容器。
* @details For example, ItemInstance uses this container to store runtime data for each fragment, as fragments themselves are const (cannot be modified at runtime). Only necessary runtime data is serialized/replicated instead of the entire fragments.
* @细节 例如ItemInstance 使用此容器存储每个片段的运行时数据,因为片段本身是常量(运行时无法修改)。仅序列化/复制必要的运行时数据,而不是整个片段。
*/
USTRUCT(BlueprintType)
struct GENERICINVENTORYSYSTEM_API FGIS_MixinContainer : public FFastArraySerializer
{
GENERATED_BODY()
/**
* Default constructor for mixin container.
* 混合数据容器的默认构造函数。
*/
FGIS_MixinContainer()
: OwningObject(nullptr)
{
}
/**
* Constructor for mixin container with an owning object.
* 使用所属对象构造混合数据容器。
* @param NewObject The object that owns this container. 拥有此容器的对象。
*/
explicit FGIS_MixinContainer(UObject* NewObject)
: OwningObject(NewObject)
{
}
/**
* Retrieves data for a target class.
* 获取目标类的数据。
* @param TargetClass The class of the target object. 目标对象的类。
* @param OutData The retrieved instanced struct data (output). 检索到的实例化结构数据(输出)。
* @return True if data was found, false otherwise. 如果找到数据则返回true否则返回false。
*/
// bool GetDataByTargetClass(const TSubclassOf<UObject>& TargetClass, FInstancedStruct& OutData) const;
/**
* Retrieves data for a target object.
* 获取目标对象的数据。
* @param Target The target object which the data attached to. 数据所附加到的目标对象。
* @param OutData The retrieved instanced struct data (output). 检索到的实例化结构数据(输出)。
* @return True if data was found, false otherwise. 如果找到数据则返回true否则返回false。
*/
bool GetDataByTarget(const UObject* Target, FInstancedStruct& OutData) const;
/**
* Finds the index of a target object in the container.
* 查找容器中目标对象的索引。
* @param Target The target object to find. 要查找的目标对象。
* @return The index of the target object, or INDEX_NONE if not found. 目标对象的索引如果未找到则返回INDEX_NONE。
*/
int32 IndexOfTarget(const UObject* Target) const;
/**
* Finds the index of a target class in the container.
* 查找容器中目标类的索引。
* @param TargetClass The class of the target object. 目标对象的类。
* @return The index of the target class, or INDEX_NONE if not found. 目标类的索引如果未找到则返回INDEX_NONE。
*/
int32 IndexOfTargetByClass(const TSubclassOf<UObject>& TargetClass) const;
/**
* Sets data for a target object.
* 为目标对象设置数据。
* @param Target The target object. 目标对象。
* @param Data The instanced struct data to set. 要设置的实例化结构数据。
* @return The index of the updated or added entry. 更新或添加条目的索引。
*/
int32 SetDataForTarget(const TObjectPtr<const UObject>& Target, const FInstancedStruct& Data);
bool IsObjectLoadedFromDisk(const UObject* Object) const;
/**
* Updates data for a target class.
* 更新目标类的数据。
* @param TargetClass The class of the target object. 目标对象的类。
* @param Data The instanced struct data to update. 要更新的实例化结构数据。
* @return The index of the updated entry, or INDEX_NONE if not found. 更新条目的索引如果未找到则返回INDEX_NONE。
*/
int32 UpdateDataByTargetClass(const TSubclassOf<UObject>& TargetClass, const FInstancedStruct& Data);
/**
* Removes data associated with a target class.
* 移除与目标类关联的数据。
* @param TargetClass The class of the target object. 目标对象的类。
*/
void RemoveDataByTargetClass(const TSubclassOf<UObject>& TargetClass);
/**
* Checks if the data is compatible with the target object.
* 检查数据是否与目标对象兼容。
* @param Target The target object. 目标对象。
* @param Data The instanced struct data to check. 要检查的实例化结构数据。
* @return True if the data is compatible, false otherwise. 如果数据兼容则返回true否则返回false。
*/
bool CheckCompatibility(const UObject* Target, const FInstancedStruct& Data) const;
/**
* Retrieves all data stored in the container.
* 获取容器中存储的所有数据。
* @return Array of instanced struct data. 实例化结构数据的数组。
*/
TArray<FInstancedStruct> GetAllData() const;
/**
* Retrieves all serializable mixin in the container.
* 获取容器中所有可序列化的mixin
* @return Array of serializable mixin. 可序列化的Mixin数组
*/
TArray<FGIS_Mixin> GetSerializableMixins() const;
TArray<FGIS_MixinRecord> GetSerializableMixinRecords() const;
void RestoreFromRecords(const TArray<FGIS_MixinRecord>& Records);
static TArray<FGIS_Mixin> ConvertRecordsToMixins(const TArray<FGIS_MixinRecord>& Records);
/**
* Retrieves all serializable data stored in the container.
* 获取容器中存储的所有可序列化数据。
* @return Array of serializable instanced struct data. 可序列化实例化结构数据的数组。
*/
TArray<FInstancedStruct> GetAllSerializableData() const;
// -- Begin FFastArraySerializer implementation
/**
* Called after items are added during replication.
* 复制期间在添加项目后调用。
* @param AddedIndices The indices of added items. 添加项目的索引。
* @param FinalSize The final size of the array after addition. 添加后数组的最终大小。
*/
void PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize);
/**
* Called after items are changed during replication.
* 复制期间在项目更改后调用。
* @param ChangedIndices The indices of changed items. 更改项目的索引。
* @param FinalSize The final size of the array after changes. 更改后数组的最终大小。
*/
void PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize);
/**
* Called before items are removed during replication.
* 复制期间在移除项目前调用。
* @param RemovedIndices The indices of removed items. 移除项目的索引。
* @param FinalSize The final size of the array after removal. 移除后数组的最终大小。
*/
void PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize);
/**
* Handles delta serialization for the mixin container.
* 处理混合数据容器的增量序列化。
* @param DeltaParams The serialization parameters. 序列化参数。
* @return True if serialization was successful, false otherwise. 如果序列化成功则返回true否则返回false。
*/
bool NetDeltaSerialize(FNetDeltaSerializeInfo& DeltaParams);
// -- End FFastArraySerializer implementation
protected:
/**
* Caches mixin data for faster access.
* 缓存混合数据以加速访问。
*/
void CacheMixins();
/**
* Updates data at a specific index.
* 在指定索引更新数据。
* @param Idx The index of the entry to update. 要更新的条目索引。
* @param Data The instanced struct data to set. 要设置的实例化结构数据。
* @return The index of the updated entry. 更新条目的索引。
*/
int32 UpdateDataAt(int32 Idx, const FInstancedStruct& Data);
/**
* The object that owns this container.
* 拥有此容器的对象。
*/
UPROPERTY()
TObjectPtr<UObject> OwningObject;
/**
* List of a target object to data pairs.
* 目标对象到数据的键值对列表。
*/
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="Mixin")
TArray<FGIS_Mixin> Mixins;
/**
* Cached map for faster access to mixin data (index may vary between client and server).
* 用于加速访问混合数据的缓存映射(客户端和服务器的索引可能不同)。
*/
UPROPERTY(NotReplicated)
TMap<TObjectPtr<const UObject>, int32> AcceleratedMap;
/**
* Hash of the last cached state.
* 最后缓存状态的哈希值。
*/
UPROPERTY()
uint32 LastCachedHash = INDEX_NONE;
};
/**
* Template specialization to enable network delta serialization for the mixin container.
* 为混合数据容器启用网络增量序列化的模板特化。
*/
template <>
struct TStructOpsTypeTraits<FGIS_MixinContainer> : TStructOpsTypeTraitsBase2<FGIS_MixinContainer>
{
enum { WithNetDeltaSerializer = true };
};

View File

@@ -0,0 +1,59 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Runtime/Launch/Resources/Version.h"
#if ENGINE_MINOR_VERSION < 5
#include "InstancedStruct.h"
#else
#include "StructUtils/InstancedStruct.h"
#endif
#include "UObject/Interface.h"
#include "GIS_MixinOwnerInterface.generated.h"
/**
* Interface class for objects that own a mixin container.
* 拥有混合数据容器的对象的接口类。
*/
UINTERFACE()
class GENERICINVENTORYSYSTEM_API UGIS_MixinOwnerInterface : public UInterface
{
GENERATED_BODY()
};
/**
* Interface for objects that own a mixin container, to be notified of mixin data changes.
* 拥有混合数据容器的对象应实现的接口,用于接收混合数据变更通知。
* @details For example, an item instance will implement this interface to get notified when data attached to different fragment classes is added, changed, or removed.
* @细节 例如,道具实例将实现此接口,以在附加到不同片段类的数据被添加、更改或移除时收到通知。
*/
class GENERICINVENTORYSYSTEM_API IGIS_MixinOwnerInterface
{
GENERATED_BODY()
public:
/**
* Called when mixin data is added to the owning object.
* 当混合数据被添加到拥有对象时调用。
* @param Target The target object to which the data is attached. 数据附加到的对象。
* @param Data The added instanced struct data. 添加的实例化结构数据。
*/
virtual void OnMixinDataAdded(const TObjectPtr<const UObject>& Target, const FInstancedStruct& Data) = 0;
/**
* Called when mixin data is updated in the owning object.
* 当拥有对象中的混合数据被更新时调用。
* @param Target The target object to which the data is attached. 数据附加到的对象。
* @param Data The updated instanced struct data. 更新的实例化结构数据。
*/
virtual void OnMixinDataUpdated(const TObjectPtr<const UObject>& Target, const FInstancedStruct& Data) = 0;
/**
* Called when mixin data is removed from the owning object.
* 当从拥有对象中移除混合数据时调用。
* @param Target The target object to which the data was attached. 数据附加到的对象。
* @param Data The removed instanced struct data. 移除的实例化结构数据。
*/
virtual void OnMixinDataRemoved(const TObjectPtr<const UObject>& Target, const FInstancedStruct& Data) = 0;
};

View File

@@ -0,0 +1,53 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Runtime/Launch/Resources/Version.h"
#if ENGINE_MINOR_VERSION < 5
#include "InstancedStruct.h"
#else
#include "StructUtils/InstancedStruct.h"
#endif
#include "UObject/Interface.h"
#include "GIS_MixinTargetInterface.generated.h"
UINTERFACE(meta=(CannotImplementInterfaceInBlueprint))
class UGIS_MixinTargetInterface : public UInterface
{
GENERATED_BODY()
};
/**
* Interface for objects to handle data mixin functionality.
* 处理数据混合功能的接口。
* @details Defines methods for serialization and compatibility of mixin data.
* @细节 定义了混合数据的序列化和兼容性方法。
*/
class GENERICINVENTORYSYSTEM_API IGIS_MixinTargetInterface
{
GENERATED_BODY()
public:
/**
* Determines if the mixin data can be serialized.
* 确定混合数据是否可以序列化。
* @return True if the data is serializable, false otherwise. 如果数据可序列化则返回true否则返回false。
*/
virtual bool IsMixinDataSerializable() const = 0;
/**
* Retrieves the compatible data structure for the mixin.
* 获取混合数据的兼容数据结构。
* @return The script struct compatible with the mixin data. 与混合数据兼容的脚本结构。
*/
virtual TObjectPtr<const UScriptStruct> GetCompatibleMixinDataType() const = 0;
/**
* Generate default runtime data for compatible data type.
* 生成兼容的混合数据类型的默认值。
* @param DefaultState The default value of compatible data. 兼容的数据结构默认值。
* @return If has default value. 是否具备默认值?
*/
virtual bool MakeDefaultMixinData(FInstancedStruct& DefaultState) const = 0;
};

View File

@@ -0,0 +1,188 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "GIS_GameplayTagFloat.h"
#include "GIS_GameplayTagInteger.h"
#include "Engine/DataAsset.h"
#include "GIS_ItemDefinition.generated.h"
class UGIS_ItemDefinitionSchema;
class UGIS_ItemFragment;
class UGIS_ItemInstance;
class UTexture2D;
/**
* An item definition is a data asset. Creating a new item definition is equivalent to creating a new instance of the item definition class.
* Essentially, item definitions are static data that can incorporate item data fragments to achieve complex data structures.
* 道具定义是一个数据资产,创建一个新道具定义相当于新建一个类型为道具定义的资产。道具定义本质上是静态数据,可以添加道具数据片段,以实现复杂的数据结构。
*/
UCLASS(BlueprintType, Const)
class GENERICINVENTORYSYSTEM_API UGIS_ItemDefinition : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
/**
* Constructor for the item definition.
* 道具定义的构造函数。
* @param ObjectInitializer The object initializer. 对象初始化器。
*/
UGIS_ItemDefinition(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
/**
* Display name of the item on the UI.
* 道具在UI上的显示名称。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Display)
FText DisplayName;
/**
* Description of the item on the UI.
* 道具在UI上的描述。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Display)
FText Description;
/**
* Icon of the item on the UI.
* 道具在UI上的图标。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Display)
TObjectPtr<UTexture2D> Icon;
/**
* Indicates whether the item is unique, preventing it from being stacked.
* 表示道具是否唯一,唯一道具不可堆叠。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Common)
bool bUnique;
/**
* Gameplay tags associated with the item for querying, categorization, and validation.
* 与道具关联的游戏标签,用于查询、分类和验证。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Common, meta=(Categories="GIS.Item"))
FGameplayTagContainer ItemTags;
/**
* Static tag-to-float attributes for the item definition.
* 道具定义的静态标签到浮点数的属性映射。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Settings, meta=(TitleProperty="{Tag} -> {Value}", Categories="GIS.Attribute"))
TArray<FGIS_GameplayTagFloat> StaticFloatAttributes;
/**
* Static tag-to-integer attributes for the item definition.
* 道具定义的静态标签到整数的属性映射。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Settings, meta=(TitleProperty="{Tag} -> {Value}", Categories="GIS.Attribute"))
TArray<FGIS_GameplayTagInteger> StaticIntegerAttributes;
/**
* Array of item data fragments to form simple or complex data structures. No duplicate fragment types are allowed.
* 数据片段数组,用于构成简单或复杂的道具数据结构。不允许重复的片段类型。
*/
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category=Settings, Instanced, meta=(NoElementDuplicate))
TArray<TObjectPtr<UGIS_ItemFragment>> Fragments;
/**
* Gets a fragment by its class.
* 通过类获取数据片段。
* @param FragmentClass The class of the fragment to retrieve. 要检索的片段类。
* @return The fragment instance, or nullptr if not found. 片段实例如果未找到则返回nullptr。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemDefinition")
const UGIS_ItemFragment* GetFragment(TSubclassOf<UGIS_ItemFragment> FragmentClass) const;
/**
* Checks if the item definition has a specific float attribute.
* 检查道具定义是否具有特定的浮点属性。
* @param AttributeTag The tag of the attribute to check. 要检查的属性标签。
* @return True if the attribute exists, false otherwise. 如果属性存在则返回true否则返回false。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemDefinition")
bool HasFloatAttribute(FGameplayTag AttributeTag) const;
/**
* Gets the value of a static float attribute.
* 获取静态浮点属性的值。
* @param AttributeTag The tag of the attribute to retrieve. 要检索的属性标签。
* @return The value of the float attribute, or 0 if not found. 浮点属性的值如果未找到则返回0。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemDefinition")
float GetFloatAttribute(FGameplayTag AttributeTag) const;
/**
* Checks if the item definition has a specific integer attribute.
* 检查道具定义是否具有特定的整数属性。
* @param AttributeTag The tag of the attribute to check. 要检查的属性标签。
* @return True if the attribute exists, false otherwise. 如果属性存在则返回true否则返回false。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemDefinition")
bool HasIntegerAttribute(FGameplayTag AttributeTag) const;
/**
* Gets the value of a static integer attribute.
* 获取静态整数属性的值。
* @param AttributeTag The tag of the attribute to retrieve. 要检索的属性标签。
* @return The value of the integer attribute, or 0 if not found. 整数属性的值如果未找到则返回0。
*/
UFUNCTION(BlueprintCallable, BlueprintPure, Category="GIS|ItemDefinition")
int32 GetIntegerAttribute(FGameplayTag AttributeTag) const;
/**
* Template function to find a fragment by its class.
* 模板函数,通过类查找数据片段。
* @param ResultClass The class of the fragment to find. 要查找的片段类。
* @return The fragment instance cast to the specified class, or nullptr if not found. 转换为指定类的片段实例如果未找到则返回nullptr。
*/
template <typename ResultClass>
const ResultClass* FindFragment() const
{
return static_cast<const ResultClass*>(GetFragment(ResultClass::StaticClass()));
}
/**
* Cached map for faster access to integer attributes.
* 用于快速访问整数属性的缓存映射。
*/
UPROPERTY()
TMap<FGameplayTag, int32> IntegerAttributeMap;
/**
* Cached map for faster access to float attributes.
* 用于快速访问浮点属性的缓存映射。
*/
UPROPERTY()
TMap<FGameplayTag, float> FloatAttributeMap;
/**
* Finds a fragment in an item definition by its class.
* 在道具定义中通过类查找数据片段。
* @param ItemDefinition The item definition to query. 要查询的道具定义。
* @param FragmentClass The class of the fragment to find. 要查找的片段类。
* @return The fragment instance, or nullptr if not found. 片段实例如果未找到则返回nullptr。
*/
UFUNCTION(BlueprintCallable, BlueprintPure=false, Category="GIS|ItemDefinition", meta=(DeterminesOutputType=FragmentClass))
static const UGIS_ItemFragment* FindFragmentOfItemDefinition(TSoftObjectPtr<UGIS_ItemDefinition> ItemDefinition, TSubclassOf<UGIS_ItemFragment> FragmentClass);
#if WITH_EDITOR
/**
* Called before the item definition is saved in the editor.
* 编辑器中道具定义保存前调用。
* @param SaveContext The save context. 保存上下文。
*/
virtual void PreSave(FObjectPreSaveContext SaveContext) override;
/**
* Validates the item definition's data in the editor.
* 在编辑器中验证道具定义的数据。
* @param Context The validation context. 验证上下文。
* @return The result of the data validation. 数据验证的结果。
*/
virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
#endif
};

View File

@@ -0,0 +1,171 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "GameplayTagContainer.h"
#include "GIS_GameplayTagFloat.h"
#include "GIS_GameplayTagInteger.h"
#include "Engine/DataAsset.h"
#include "GIS_ItemDefinitionSchema.generated.h"
class UGIS_ItemInstance;
class UGIS_ItemDefinition;
class UGIS_ItemFragment;
/**
* Structure representing a validation entry for item definitions.
* 表示道具定义验证条目的结构体。
*/
USTRUCT()
struct GENERICINVENTORYSYSTEM_API FGIS_ItemDefinitionValidationEntry
{
GENERATED_BODY()
/**
* Gameplay tag query to match items for this validation.
* 用于匹配此验证的游戏标签查询。
*/
UPROPERTY(EditDefaultsOnly, Category = Schema, meta=(Categories="GIS.Item"))
FGameplayTagQuery ItemTagQuery;
/**
* The required item instance class for matching items.
* 匹配道具所需的道具实例类。
*/
UPROPERTY(EditDefaultsOnly, Category = Schema)
TSoftClassPtr<UGIS_ItemInstance> InstanceClass;
/**
* List of required fragment classes for the item definition.
* 道具定义所需的片段类列表。
*/
UPROPERTY(EditDefaultsOnly, Category = Schema)
TArray<TSoftClassPtr<UGIS_ItemFragment>> RequiredFragments;
/**
* List of forbidden fragment classes for the item definition.
* 道具定义禁止的片段类列表。
*/
UPROPERTY(EditDefaultsOnly, Category = Schema)
TArray<TSoftClassPtr<UGIS_ItemFragment>> ForbiddenFragments;
/**
* List of required float attributes with default values.
* 必需的浮点属性列表,包含默认值。
*/
UPROPERTY(EditDefaultsOnly, Category = Schema, meta=(TitleProperty="{Tag} -> {Value}"))
TArray<FGIS_GameplayTagFloat> RequiredFloatAttributes;
/**
* List of required integer attributes with default values.
* 必需的整数属性列表,包含默认值。
*/
UPROPERTY(EditDefaultsOnly, Category = Schema, meta=(TitleProperty="{Tag} -> {Value}"))
TArray<FGIS_GameplayTagInteger> RequiredIntegerAttributes;
/**
* Whether to enforce the bUnique property.
* 是否强制执行 bUnique 属性。
*/
UPROPERTY(EditDefaultsOnly, Category = Schema)
bool bEnforceUnique = false;
/**
* The required value for bUnique if enforced.
* 如果强制执行 bUnique则指定其值。
*/
UPROPERTY(EditDefaultsOnly, Category = Schema, meta=(EditCondition="bEnforceUnique"))
bool RequiredUniqueValue = false;
};
/**
* The item definition schema is a data asset used in the editor for item definition data validation.
* 道具定义模式是一种数据资产,用于在编辑器中进行道具定义的数据验证。
*/
UCLASS(NotBlueprintable)
class GENERICINVENTORYSYSTEM_API UGIS_ItemDefinitionSchema : public UDataAsset
{
GENERATED_BODY()
public:
/**
* Validates an item definition against the schema.
* 针对模式验证道具定义。
* @param Definition The item definition to validate. 要验证的道具定义。
* @param OutError The error message if validation fails. 如果验证失败,则返回错误消息。
* @return True if the validation passes, false otherwise. 如果验证通过则返回true否则返回false。
*/
static bool TryValidateItemDefinition(const UGIS_ItemDefinition* Definition, FText& OutError);
/**
* Performs pre-save validation and auto-fixes for an item definition.
* 对道具定义进行保存前验证和自动修复。
* @param Definition The item definition to validate and fix. 要验证和修复的道具定义。
* @param OutError The error message if validation fails. 如果验证失败,则返回错误消息。
*/
static void TryPreSaveItemDefinition(UGIS_ItemDefinition* Definition, FText& OutError);
/**
* Validates an item definition against the schema (instance method).
* 针对模式验证道具定义(实例方法)。
* @param Definition The item definition to validate. 要验证的道具定义。
* @param OutError The error message if validation fails. 如果验证失败,则返回错误消息。
* @return True if the validation passes, false otherwise. 如果验证通过则返回true否则返回false。
*/
virtual bool TryValidate(const UGIS_ItemDefinition* Definition, FText& OutError) const;
/**
* Performs pre-save validation and auto-fixes for an item definition (instance method).
* 对道具定义进行保存前验证和自动修复(实例方法)。
* @param Definition The item definition to validate and fix. 要验证和修复的道具定义。
* @param OutError The error message if validation fails. 如果验证失败,则返回错误消息。
*/
virtual void TryPreSave(UGIS_ItemDefinition* Definition, FText& OutError) const;
#if WITH_EDITOR
/**
* Called before the schema is saved in the editor.
* 编辑器中模式保存前调用。
* @param SaveContext The save context. 保存上下文。
*/
virtual void PreSave(FObjectPreSaveContext SaveContext) override;
/**
* Validates the schema's data in the editor.
* 在编辑器中验证模式的数据。
* @param Context The validation context. 验证上下文。
* @return The result of the data validation. 数据验证的结果。
*/
virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override;
#endif
protected:
/**
* Array of validation entries for the schema.
* 模式的验证条目数组。
*/
UPROPERTY(EditDefaultsOnly, Category = Schema)
TArray<FGIS_ItemDefinitionValidationEntry> ValidationEntries;
/**
* List of required fragment classes for all item definitions.
* 所有道具定义所需的常规片段类列表。
*/
UPROPERTY(EditDefaultsOnly, Category = Schema)
TArray<TSoftClassPtr<UGIS_ItemFragment>> CommonRequiredFragments;
/**
* The required parent tag for all item definitions' ItemTags.
* 所有道具定义的 ItemTags 必须包含的父级标签。
*/
UPROPERTY(EditDefaultsOnly, Category = Schema)
FGameplayTag RequiredParentTag;
/**
* Global fragment order for all item definitions.
* 所有道具定义的全局片段排序。
*/
UPROPERTY(EditDefaultsOnly, Category = Schema)
TArray<TSoftClassPtr<UGIS_ItemFragment>> FragmentOrder;
};

View File

@@ -0,0 +1,187 @@
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Items/GIS_ItemStack.h"
#include "UObject/Object.h"
#include "GIS_ItemInfo.generated.h"
struct FGIS_ItemStack;
class UGIS_ItemCollection;
class UGIS_ItemInstance;
/**
* Item information is a temporary structure used to pass item-related information across different operations, such as its belonging collection, amount, and corresponding item instance.
* 道具信息是一个临时的结构体,用于在不同的操作中传递道具相关信息,例如其所属集合、数量及对应的道具实例等。
*/
USTRUCT(BlueprintType, DisplayName="GIS Item Information")
struct GENERICINVENTORYSYSTEM_API FGIS_ItemInfo
{
GENERATED_BODY()
/**
* Default constructor for item information.
* 道具信息的默认构造函数。
*/
FGIS_ItemInfo();
/**
* Constructor for item information with item, amount, and collection.
* 使用道具、数量和集合构造道具信息。
* @param InItem The item instance. 道具实例。
* @param InAmount The amount of the item. 道具数量。
* @param InCollection The item collection where the item originates. 道具来源的集合。
*/
FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, UGIS_ItemCollection* InCollection);
/**
* Constructor for item information with item, amount, collection, and stack ID.
* 使用道具、数量、集合和堆栈ID构造道具信息。
* @param InItem The item instance. 道具实例。
* @param InAmount The amount of the item. 道具数量。
* @param InCollection The item collection where the item originates. 道具来源的集合。
* @param InStackId The stack ID associated with the item. 与道具关联的堆栈ID。
*/
FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, UGIS_ItemCollection* InCollection, FGuid InStackId);
/**
* Constructor for item information with item and amount.
* 使用道具和数量构造道具信息。
* @param InItem The item instance. 道具实例。
* @param InAmount The amount of the item. 道具数量。
*/
FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount);
/**
* Constructor for item information with item, amount, and collection ID.
* 使用道具、数量和集合ID构造道具信息。
* @param InItem The item instance. 道具实例。
* @param InAmount The amount of the item. 道具数量。
* @param InCollectionId The collection ID associated with the item. 与道具关联的集合ID。
*/
FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, FGuid InCollectionId);
/**
* Constructor for item information with item, amount, and collection tag.
* 使用道具、数量和集合标签构造道具信息。
* @param InItem The item instance. 道具实例。
* @param InAmount The amount of the item. 道具数量。
* @param InCollectionTag The collection tag associated with the item. 与道具关联的集合标签。
*/
FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, FGameplayTag InCollectionTag);
/**
* Constructor for item information by copying another info and changing the amount.
* 通过复制其他道具信息并更改数量构造道具信息。
* @param InAmount The new item amount. 新的道具数量。
* @param OtherInfo The item info to copy. 要复制的道具信息。
*/
FGIS_ItemInfo(int32 InAmount, const FGIS_ItemInfo& OtherInfo);
FGIS_ItemInfo(int32 InAmount, int32 InIndex, const FGIS_ItemInfo& OtherInfo);
/**
* Constructor for item information by copying another info with a new item and amount.
* 通过复制其他道具信息并指定新道具和数量构造道具信息。
* @param InItem The new item instance. 新的道具实例。
* @param InAmount The new item amount. 新的道具数量。
* @param OtherInfo The item info to copy. 要复制的道具信息。
*/
FGIS_ItemInfo(UGIS_ItemInstance* InItem, int32 InAmount, const FGIS_ItemInfo& OtherInfo);
/**
* Constructor for item information from an item stack.
* 从道具堆栈构造道具信息。
* @param ItemStack The item stack to convert from. 要转换的道具堆栈。
*/
FGIS_ItemInfo(const FGIS_ItemStack& ItemStack);
/**
* Gets a debug string representation of the item information.
* 获取道具信息的调试字符串表示。
* @return The debug string. 调试字符串。
*/
FString GetDebugString() const;
/**
* Compares this item info with another for equality.
* 将此道具信息与另一个比较以判断是否相等。
* @param Other The other item info to compare with. 要比较的其他道具信息。
* @return True if the item infos are equal, false otherwise. 如果道具信息相等则返回true否则返回false。
*/
bool operator==(const FGIS_ItemInfo& Other) const;
/**
* Checks if the item information is valid.
* 检查道具信息是否有效。
* @return True if the item info is valid, false otherwise. 如果道具信息有效则返回true否则返回false。
*/
bool IsValid() const;
/**
* Gets the inventory system component associated with the item collection.
* 获取与道具集合关联的库存系统组件。
* @return The inventory system component, or nullptr if not found. 库存系统组件如果未找到则返回nullptr。
*/
UGIS_InventorySystemComponent* GetInventory() const;
/**
* Static constant representing an invalid or empty item info.
* 表示无效或空道具信息的静态常量。
*/
static FGIS_ItemInfo None;
/**
* The item instance associated with this information, with context-dependent usage.
* 与此信息关联的道具实例,用途因上下文而异。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS", meta=(DisplayName="Item Instance"))
TObjectPtr<UGIS_ItemInstance> Item;
/**
* The amount of the item instance in this information.
* 此信息中道具实例的数量。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS", meta=(DisplayName="Item Amount"))
int32 Amount;
/**
* The collection instance where the information originates, with context-dependent usage.
* 此信息的源集合,用途因上下文而异。
* @attention For adding items, it specifies the source collection for overflow return; for retrieving items, it indicates the source collection.
* @注意 添加道具时,指定溢出返回的源集合;取回道具时,表示来源集合。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS")
TObjectPtr<UGIS_ItemCollection> ItemCollection;
/**
* The stack ID associated with this information, with context-dependent usage.
* 与此信息关联的堆栈ID用途因上下文而异。
* @attention For adding/removing items, it determines the target stack; for retrieving items, it indicates the source stack.
* @注意 添加/移除道具时,用于确定目标堆栈;取回道具时,表示来源堆栈。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS")
FGuid StackId;
/**
* The collection ID associated with this information, with context-dependent usage.
* 与此信息关联的集合ID用途因上下文而异。
* @attention For adding/removing items, it determines the target collection; for retrieving items, it is usually null.
* @注意 添加/移除道具时,用于确定目标集合;取回道具时,通常为空。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS")
FGuid CollectionId;
/**
* The collection tag associated with this information, with context-dependent usage.
* 与此信息关联的集合标签,用途因上下文而异。
* @attention For adding/removing items, it determines the target collection (lower priority than CollectionId); for retrieving items, it is usually null.
* @注意 添加/移除道具时用于确定目标集合优先级低于CollectionId取回道具时通常为空。
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS", meta=(Categories="GIS.Collection"))
FGameplayTag CollectionTag;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="GIS")
int32 Index{INDEX_NONE};
};

Some files were not shown because too many files have changed in this diff Show More