Initial commit: Item Interaction Ecosystem plugin (UE5.7)
Server-authoritative, data-driven item/inventory/interaction/carry/equipment/ world-slot/placement/crafting framework. 8 modules, compiles against UE 5.7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
26
Source/ItemDatabase/ItemDatabase.Build.cs
Normal file
26
Source/ItemDatabase/ItemDatabase.Build.cs
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
using UnrealBuildTool;
|
||||
|
||||
public class ItemDatabase : ModuleRules
|
||||
{
|
||||
public ItemDatabase(ReadOnlyTargetRules Target) : base(Target)
|
||||
{
|
||||
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
|
||||
IWYUSupport = IWYUSupport.Full;
|
||||
|
||||
PublicDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"Core",
|
||||
"CoreUObject",
|
||||
"Engine",
|
||||
"GameplayTags",
|
||||
"ItemCore"
|
||||
});
|
||||
|
||||
PrivateDependencyModuleNames.AddRange(new string[]
|
||||
{
|
||||
"DeveloperSettings"
|
||||
});
|
||||
}
|
||||
}
|
||||
8
Source/ItemDatabase/Private/ItemDatabaseModule.cpp
Normal file
8
Source/ItemDatabase/Private/ItemDatabaseModule.cpp
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "Modules/ModuleManager.h"
|
||||
#include "ItemDatabaseLog.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogItemDatabase);
|
||||
|
||||
IMPLEMENT_MODULE(FDefaultModuleImpl, ItemDatabase)
|
||||
555
Source/ItemDatabase/Private/ItemDatabaseSubsystem.cpp
Normal file
555
Source/ItemDatabase/Private/ItemDatabaseSubsystem.cpp
Normal file
@ -0,0 +1,555 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "ItemDatabaseSubsystem.h"
|
||||
#include "ItemEcosystemSettings.h"
|
||||
#include "ItemDatabaseLog.h"
|
||||
#include "ItemNativeTags.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
|
||||
void UItemDatabaseSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
Super::Initialize(Collection);
|
||||
RebuildCache();
|
||||
|
||||
#if !UE_BUILD_SHIPPING
|
||||
TArray<FItemValidationIssue> Issues;
|
||||
ValidateData(Issues);
|
||||
for (const FItemValidationIssue& Issue : Issues)
|
||||
{
|
||||
if (Issue.bIsError)
|
||||
{
|
||||
UE_LOG(LogItemDatabase, Error, TEXT("[Validate] %s: %s"), *Issue.ItemId.ToString(), *Issue.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogItemDatabase, Warning, TEXT("[Validate] %s: %s"), *Issue.ItemId.ToString(), *Issue.Message);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::Deinitialize()
|
||||
{
|
||||
Definitions.Empty();
|
||||
PropertiesByItem.Empty();
|
||||
FeaturesByItem.Empty();
|
||||
InteractionsByItem.Empty();
|
||||
Redirects.Empty();
|
||||
ContainerDefs.Empty();
|
||||
WorldSlotDefs.Empty();
|
||||
LootByTable.Empty();
|
||||
RecipesById.Empty();
|
||||
bCacheBuilt = false;
|
||||
Super::Deinitialize();
|
||||
}
|
||||
|
||||
UItemDatabaseSubsystem* UItemDatabaseSubsystem::Get(const UObject* WorldContext)
|
||||
{
|
||||
if (!WorldContext)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
if (const UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContext, EGetWorldErrorMode::ReturnNull) : nullptr)
|
||||
{
|
||||
if (UGameInstance* GI = World->GetGameInstance())
|
||||
{
|
||||
return GI->GetSubsystem<UItemDatabaseSubsystem>();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::RebuildCache()
|
||||
{
|
||||
Definitions.Empty();
|
||||
PropertiesByItem.Empty();
|
||||
FeaturesByItem.Empty();
|
||||
InteractionsByItem.Empty();
|
||||
Redirects.Empty();
|
||||
|
||||
const UItemEcosystemSettings* Settings = UItemEcosystemSettings::Get();
|
||||
if (!Settings)
|
||||
{
|
||||
UE_LOG(LogItemDatabase, Warning, TEXT("No ItemEcosystem settings found."));
|
||||
return;
|
||||
}
|
||||
|
||||
LoadDefinitions(Settings->ItemsTable.LoadSynchronous());
|
||||
LoadProperties(Settings->PropertiesTable.LoadSynchronous());
|
||||
LoadFeatures(Settings->FeaturesTable.LoadSynchronous());
|
||||
LoadInteractions(Settings->InteractionsTable.LoadSynchronous());
|
||||
LoadRedirects(Settings->RedirectsTable.LoadSynchronous());
|
||||
LoadContainers(Settings->ContainersTable.LoadSynchronous());
|
||||
LoadWorldSlots(Settings->WorldSlotsTable.LoadSynchronous());
|
||||
LoadLoot(Settings->LootTable.LoadSynchronous());
|
||||
LoadRecipes(Settings->RecipesTable.LoadSynchronous());
|
||||
|
||||
bCacheBuilt = true;
|
||||
UE_LOG(LogItemDatabase, Log, TEXT("Item database built: %d items, %d redirects."),
|
||||
Definitions.Num(), Redirects.Num());
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::LoadDefinitions(UDataTable* Table)
|
||||
{
|
||||
if (!Table)
|
||||
{
|
||||
UE_LOG(LogItemDatabase, Warning, TEXT("DT_Items not assigned in Project Settings -> Item Ecosystem."));
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<FItemDefinitionRow*> Rows;
|
||||
Table->GetAllRows<FItemDefinitionRow>(TEXT("ItemDatabase.LoadDefinitions"), Rows);
|
||||
for (const FItemDefinitionRow* Row : Rows)
|
||||
{
|
||||
if (!Row || Row->ItemId.IsNone())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Definitions.Add(Row->ItemId, *Row);
|
||||
}
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::LoadProperties(UDataTable* Table)
|
||||
{
|
||||
if (!Table)
|
||||
{
|
||||
return;
|
||||
}
|
||||
TArray<FItemPropertyRow*> Rows;
|
||||
Table->GetAllRows<FItemPropertyRow>(TEXT("ItemDatabase.LoadProperties"), Rows);
|
||||
for (const FItemPropertyRow* Row : Rows)
|
||||
{
|
||||
if (Row && !Row->ItemId.IsNone())
|
||||
{
|
||||
PropertiesByItem.FindOrAdd(Row->ItemId).Add(*Row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::LoadFeatures(UDataTable* Table)
|
||||
{
|
||||
if (!Table)
|
||||
{
|
||||
return;
|
||||
}
|
||||
TArray<FItemFeatureRow*> Rows;
|
||||
Table->GetAllRows<FItemFeatureRow>(TEXT("ItemDatabase.LoadFeatures"), Rows);
|
||||
for (const FItemFeatureRow* Row : Rows)
|
||||
{
|
||||
if (Row && !Row->ItemId.IsNone())
|
||||
{
|
||||
FeaturesByItem.FindOrAdd(Row->ItemId).Add(*Row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::LoadInteractions(UDataTable* Table)
|
||||
{
|
||||
if (!Table)
|
||||
{
|
||||
return;
|
||||
}
|
||||
TArray<FItemInteractionRow*> Rows;
|
||||
Table->GetAllRows<FItemInteractionRow>(TEXT("ItemDatabase.LoadInteractions"), Rows);
|
||||
for (const FItemInteractionRow* Row : Rows)
|
||||
{
|
||||
if (Row && !Row->ItemId.IsNone())
|
||||
{
|
||||
InteractionsByItem.FindOrAdd(Row->ItemId).Add(*Row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::LoadRedirects(UDataTable* Table)
|
||||
{
|
||||
if (!Table)
|
||||
{
|
||||
return;
|
||||
}
|
||||
TArray<FItemRedirectRow*> Rows;
|
||||
Table->GetAllRows<FItemRedirectRow>(TEXT("ItemDatabase.LoadRedirects"), Rows);
|
||||
for (const FItemRedirectRow* Row : Rows)
|
||||
{
|
||||
if (Row && !Row->OldItemId.IsNone() && !Row->NewItemId.IsNone())
|
||||
{
|
||||
Redirects.Add(Row->OldItemId, Row->NewItemId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::LoadContainers(UDataTable* Table)
|
||||
{
|
||||
if (!Table) { return; }
|
||||
TArray<FContainerDefRow*> Rows;
|
||||
Table->GetAllRows<FContainerDefRow>(TEXT("ItemDatabase.LoadContainers"), Rows);
|
||||
for (const FContainerDefRow* Row : Rows)
|
||||
{
|
||||
if (Row && Row->ContainerTag.IsValid()) { ContainerDefs.Add(Row->ContainerTag, *Row); }
|
||||
}
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::LoadWorldSlots(UDataTable* Table)
|
||||
{
|
||||
if (!Table) { return; }
|
||||
TArray<FWorldSlotDefRow*> Rows;
|
||||
Table->GetAllRows<FWorldSlotDefRow>(TEXT("ItemDatabase.LoadWorldSlots"), Rows);
|
||||
for (const FWorldSlotDefRow* Row : Rows)
|
||||
{
|
||||
if (Row && Row->SlotTag.IsValid()) { WorldSlotDefs.Add(Row->SlotTag, *Row); }
|
||||
}
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::LoadLoot(UDataTable* Table)
|
||||
{
|
||||
if (!Table) { return; }
|
||||
TArray<FLootEntryRow*> Rows;
|
||||
Table->GetAllRows<FLootEntryRow>(TEXT("ItemDatabase.LoadLoot"), Rows);
|
||||
for (const FLootEntryRow* Row : Rows)
|
||||
{
|
||||
if (Row && !Row->TableId.IsNone()) { LootByTable.FindOrAdd(Row->TableId).Add(*Row); }
|
||||
}
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::LoadRecipes(UDataTable* Table)
|
||||
{
|
||||
if (!Table) { return; }
|
||||
TArray<FCraftingRecipeRow*> Rows;
|
||||
Table->GetAllRows<FCraftingRecipeRow>(TEXT("ItemDatabase.LoadRecipes"), Rows);
|
||||
for (const FCraftingRecipeRow* Row : Rows)
|
||||
{
|
||||
if (Row && !Row->RecipeId.IsNone()) { RecipesById.Add(Row->RecipeId, *Row); }
|
||||
}
|
||||
}
|
||||
|
||||
EItemPropertyReplicationPolicy UItemDatabaseSubsystem::GetPropertyPolicy(FName ItemId, const FGameplayTag& PropertyTag) const
|
||||
{
|
||||
if (const TArray<FItemPropertyRow>* Rows = PropertiesByItem.Find(ResolveRedirect(ItemId)))
|
||||
{
|
||||
for (const FItemPropertyRow& R : *Rows)
|
||||
{
|
||||
if (R.PropertyTag == PropertyTag) { return R.ReplicationPolicy; }
|
||||
}
|
||||
}
|
||||
return EItemPropertyReplicationPolicy::None;
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::GetServerOnlyProperties(FName ItemId, TArray<FGameplayTag>& Out) const
|
||||
{
|
||||
if (const TArray<FItemPropertyRow>* Rows = PropertiesByItem.Find(ResolveRedirect(ItemId)))
|
||||
{
|
||||
for (const FItemPropertyRow& R : *Rows)
|
||||
{
|
||||
if (R.ReplicationPolicy == EItemPropertyReplicationPolicy::ServerOnly) { Out.Add(R.PropertyTag); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool UItemDatabaseSubsystem::FindContainerDef(FGameplayTag ContainerTag, FContainerDefRow& OutDef) const
|
||||
{
|
||||
if (const FContainerDefRow* Found = ContainerDefs.Find(ContainerTag)) { OutDef = *Found; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UItemDatabaseSubsystem::FindWorldSlotDef(FGameplayTag SlotTag, FWorldSlotDefRow& OutDef) const
|
||||
{
|
||||
if (const FWorldSlotDefRow* Found = WorldSlotDefs.Find(SlotTag)) { OutDef = *Found; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UItemDatabaseSubsystem::FindRecipe(FName RecipeId, FCraftingRecipeRow& OutRecipe) const
|
||||
{
|
||||
if (const FCraftingRecipeRow* Found = RecipesById.Find(RecipeId)) { OutRecipe = *Found; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::GetAllRecipes(TArray<FCraftingRecipeRow>& OutRecipes) const
|
||||
{
|
||||
RecipesById.GenerateValueArray(OutRecipes);
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::GetLootEntries(FName TableId, TArray<FLootEntryRow>& Out) const
|
||||
{
|
||||
if (const TArray<FLootEntryRow>* Found = LootByTable.Find(TableId)) { Out = *Found; }
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::RollLoot(FName TableId, int32 RollCount, TArray<FItemInstanceData>& OutItems) const
|
||||
{
|
||||
const TArray<FLootEntryRow>* Entries = LootByTable.Find(TableId);
|
||||
if (!Entries || Entries->Num() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float TotalWeight = 0.f;
|
||||
for (const FLootEntryRow& E : *Entries) { TotalWeight += FMath::Max(0.f, E.Weight); }
|
||||
if (TotalWeight <= 0.f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int32 Roll = 0; Roll < FMath::Max(1, RollCount); ++Roll)
|
||||
{
|
||||
float Pick = FMath::FRandRange(0.f, TotalWeight);
|
||||
for (const FLootEntryRow& E : *Entries)
|
||||
{
|
||||
Pick -= FMath::Max(0.f, E.Weight);
|
||||
if (Pick <= 0.f)
|
||||
{
|
||||
if (FMath::FRand() <= E.Chance && HasItem(E.ItemId))
|
||||
{
|
||||
const int32 Qty = FMath::RandRange(FMath::Max(1, E.MinQuantity), FMath::Max(E.MinQuantity, E.MaxQuantity));
|
||||
OutItems.Add(MakeItemInstance(E.ItemId, Qty));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FName UItemDatabaseSubsystem::ResolveRedirect(FName ItemId) const
|
||||
{
|
||||
// Follow the chain with a cycle guard.
|
||||
FName Current = ItemId;
|
||||
int32 Guard = 0;
|
||||
while (const FName* Next = Redirects.Find(Current))
|
||||
{
|
||||
Current = *Next;
|
||||
if (++Guard > 64)
|
||||
{
|
||||
UE_LOG(LogItemDatabase, Error, TEXT("Redirect cycle detected starting at %s."), *ItemId.ToString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Current;
|
||||
}
|
||||
|
||||
const FItemDefinitionRow* UItemDatabaseSubsystem::GetItemDefinition(FName ItemId) const
|
||||
{
|
||||
return Definitions.Find(ResolveRedirect(ItemId));
|
||||
}
|
||||
|
||||
bool UItemDatabaseSubsystem::FindItemDefinition(FName ItemId, FItemDefinitionRow& OutDefinition) const
|
||||
{
|
||||
if (const FItemDefinitionRow* Def = GetItemDefinition(ItemId))
|
||||
{
|
||||
OutDefinition = *Def;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UItemDatabaseSubsystem::HasItem(FName ItemId) const
|
||||
{
|
||||
return Definitions.Contains(ResolveRedirect(ItemId));
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::GetAllItemIds(TArray<FName>& OutItemIds) const
|
||||
{
|
||||
Definitions.GenerateKeyArray(OutItemIds);
|
||||
}
|
||||
|
||||
int32 UItemDatabaseSubsystem::GetMaxStack(FName ItemId) const
|
||||
{
|
||||
const FItemDefinitionRow* Def = GetItemDefinition(ItemId);
|
||||
return Def ? FMath::Max(1, Def->MaxStack) : 1;
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::ApplyDefaultProperties(FName ItemId, FItemRuntimeProperties& Out) const
|
||||
{
|
||||
if (const TArray<FItemPropertyRow>* Rows = PropertiesByItem.Find(ResolveRedirect(ItemId)))
|
||||
{
|
||||
for (const FItemPropertyRow& Row : *Rows)
|
||||
{
|
||||
if (Row.PropertyTag.IsValid())
|
||||
{
|
||||
Out.SetFloat(Row.PropertyTag, FMath::Clamp(Row.DefaultValue, Row.Min, Row.Max));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::ApplyMissingDefaultProperties(FName ItemId, FItemRuntimeProperties& Out) const
|
||||
{
|
||||
if (const TArray<FItemPropertyRow>* Rows = PropertiesByItem.Find(ResolveRedirect(ItemId)))
|
||||
{
|
||||
for (const FItemPropertyRow& Row : *Rows)
|
||||
{
|
||||
if (Row.PropertyTag.IsValid() && !Out.HasFloat(Row.PropertyTag))
|
||||
{
|
||||
Out.SetFloat(Row.PropertyTag, FMath::Clamp(Row.DefaultValue, Row.Min, Row.Max));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::MigrateLoadedItem(FItemInstanceData& Item) const
|
||||
{
|
||||
// 1) Redirect retired ids to their replacement.
|
||||
const FName Resolved = ResolveRedirect(Item.ItemId);
|
||||
if (Resolved != Item.ItemId)
|
||||
{
|
||||
Item.ItemId = Resolved;
|
||||
}
|
||||
// 2) Fill in properties introduced after this item was saved.
|
||||
if (Item.ItemDataVersion < CurrentItemDataVersion)
|
||||
{
|
||||
ApplyMissingDefaultProperties(Item.ItemId, Item.RuntimeProperties);
|
||||
Item.ItemDataVersion = CurrentItemDataVersion;
|
||||
}
|
||||
Item.EnsureInstanceId();
|
||||
}
|
||||
|
||||
FItemInstanceData UItemDatabaseSubsystem::MakeItemInstance(FName ItemId, int32 Quantity) const
|
||||
{
|
||||
const FName Resolved = ResolveRedirect(ItemId);
|
||||
FItemInstanceData Instance(Resolved, FMath::Max(1, Quantity));
|
||||
Instance.LocationType = EItemLocationType::None;
|
||||
ApplyDefaultProperties(Resolved, Instance.RuntimeProperties);
|
||||
return Instance;
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::GetPropertyRows(FName ItemId, TArray<FItemPropertyRow>& Out) const
|
||||
{
|
||||
if (const TArray<FItemPropertyRow>* Rows = PropertiesByItem.Find(ResolveRedirect(ItemId)))
|
||||
{
|
||||
Out = *Rows;
|
||||
}
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::GetFeatureRows(FName ItemId, TArray<FItemFeatureRow>& Out) const
|
||||
{
|
||||
if (const TArray<FItemFeatureRow>* Rows = FeaturesByItem.Find(ResolveRedirect(ItemId)))
|
||||
{
|
||||
Out = *Rows;
|
||||
}
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::GetFeatureRows(FName ItemId, const FGameplayTag& Feature, TArray<FItemFeatureRow>& Out) const
|
||||
{
|
||||
if (const TArray<FItemFeatureRow>* Rows = FeaturesByItem.Find(ResolveRedirect(ItemId)))
|
||||
{
|
||||
for (const FItemFeatureRow& Row : *Rows)
|
||||
{
|
||||
if (Row.Feature == Feature)
|
||||
{
|
||||
Out.Add(Row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::GetInteractionRows(FName ItemId, TArray<FItemInteractionRow>& Out) const
|
||||
{
|
||||
if (const TArray<FItemInteractionRow>* Rows = InteractionsByItem.Find(ResolveRedirect(ItemId)))
|
||||
{
|
||||
Out = *Rows;
|
||||
}
|
||||
}
|
||||
|
||||
void UItemDatabaseSubsystem::ValidateData(TArray<FItemValidationIssue>& OutIssues) const
|
||||
{
|
||||
auto AddIssue = [&OutIssues](FName Id, const FString& Msg, bool bError)
|
||||
{
|
||||
FItemValidationIssue Issue;
|
||||
Issue.ItemId = Id;
|
||||
Issue.Message = Msg;
|
||||
Issue.bIsError = bError;
|
||||
OutIssues.Add(Issue);
|
||||
};
|
||||
|
||||
for (const TPair<FName, FItemDefinitionRow>& Pair : Definitions)
|
||||
{
|
||||
const FName Id = Pair.Key;
|
||||
const FItemDefinitionRow& Def = Pair.Value;
|
||||
|
||||
if (Def.ItemId.IsNone())
|
||||
{
|
||||
AddIssue(Id, TEXT("ItemId is empty."), true);
|
||||
}
|
||||
if (Def.MaxStack <= 0)
|
||||
{
|
||||
AddIssue(Id, TEXT("MaxStack <= 0."), true);
|
||||
}
|
||||
if (Def.GridSize.X <= 0 || Def.GridSize.Y <= 0)
|
||||
{
|
||||
AddIssue(Id, TEXT("GridSize must be >= 1 in both axes."), true);
|
||||
}
|
||||
if (Def.bCanInstantPickup && !Def.bCanStoreInInventory)
|
||||
{
|
||||
AddIssue(Id, TEXT("CanInstantPickup=true but CanStoreInInventory=false."), true);
|
||||
}
|
||||
if (Def.bCanEquip && Def.EquippedActorClass.IsNull())
|
||||
{
|
||||
AddIssue(Id, TEXT("CanEquip=true but EquippedActorClass is empty."), true);
|
||||
}
|
||||
if (Def.bCanWorldCarry && Def.WorldMesh.IsNull())
|
||||
{
|
||||
AddIssue(Id, TEXT("CanWorldCarry=true but WorldMesh is empty."), true);
|
||||
}
|
||||
if (Def.bCanInstallIntoWorldSlot && Def.CompatibleWorldSlots.IsEmpty())
|
||||
{
|
||||
AddIssue(Id, TEXT("CanInstallIntoWorldSlot=true but CompatibleWorldSlots is empty."), true);
|
||||
}
|
||||
if (Def.bCanPlace && !Def.PlacementRules.bCanPlaceOnGround
|
||||
&& !Def.PlacementRules.bCanPlaceOnWall && !Def.PlacementRules.bCanPlaceOnVehicle
|
||||
&& !Def.PlacementRules.bCanPlaceInWater)
|
||||
{
|
||||
AddIssue(Id, TEXT("CanPlace=true but PlacementRules forbid every surface."), true);
|
||||
}
|
||||
if (Def.bIsQuestCritical && Def.bCanDrop)
|
||||
{
|
||||
AddIssue(Id, TEXT("QuestCritical item is droppable (CanDrop=true)."), false);
|
||||
}
|
||||
|
||||
// Feature <-> property consistency (section 28).
|
||||
auto HasDefaultProperty = [this, Id](const FGameplayTag& Tag) -> bool
|
||||
{
|
||||
if (const TArray<FItemPropertyRow>* Rows = PropertiesByItem.Find(Id))
|
||||
{
|
||||
for (const FItemPropertyRow& R : *Rows)
|
||||
{
|
||||
if (R.PropertyTag == Tag) { return true; }
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (Def.HasFeature(ItemEcosystemTags::Feature_Battery) && !HasDefaultProperty(ItemEcosystemTags::Property_BatteryCharge))
|
||||
{
|
||||
AddIssue(Id, TEXT("Battery feature present but no Property.BatteryCharge default."), false);
|
||||
}
|
||||
if (Def.HasFeature(ItemEcosystemTags::Feature_Fuel) && !HasDefaultProperty(ItemEcosystemTags::Property_FuelAmount))
|
||||
{
|
||||
AddIssue(Id, TEXT("Fuel feature present but no Property.FuelAmount default."), false);
|
||||
}
|
||||
if (Def.HasFeature(ItemEcosystemTags::Feature_Rotting))
|
||||
{
|
||||
bool bHasRottenId = false;
|
||||
if (const TArray<FItemFeatureRow>* Rows = FeaturesByItem.Find(Id))
|
||||
{
|
||||
for (const FItemFeatureRow& R : *Rows)
|
||||
{
|
||||
if (R.Feature == ItemEcosystemTags::Feature_Rotting && R.Param == FName("RottenItemId") && !R.Value.IsEmpty())
|
||||
{
|
||||
bHasRottenId = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!bHasRottenId)
|
||||
{
|
||||
AddIssue(Id, TEXT("Rotting feature present but no RottenItemId param."), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect targets must exist.
|
||||
for (const TPair<FName, FName>& Pair : Redirects)
|
||||
{
|
||||
if (!Definitions.Contains(ResolveRedirect(Pair.Value)))
|
||||
{
|
||||
AddIssue(Pair.Key, FString::Printf(TEXT("Redirect target '%s' not found in DT_Items."), *Pair.Value.ToString()), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Source/ItemDatabase/Private/ItemEcosystemSettings.cpp
Normal file
13
Source/ItemDatabase/Private/ItemEcosystemSettings.cpp
Normal file
@ -0,0 +1,13 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#include "ItemEcosystemSettings.h"
|
||||
|
||||
UItemEcosystemSettings::UItemEcosystemSettings()
|
||||
{
|
||||
CategoryName = FName("Game");
|
||||
}
|
||||
|
||||
const UItemEcosystemSettings* UItemEcosystemSettings::Get()
|
||||
{
|
||||
return GetDefault<UItemEcosystemSettings>();
|
||||
}
|
||||
129
Source/ItemDatabase/Public/ItemAuxiliaryRows.h
Normal file
129
Source/ItemDatabase/Public/ItemAuxiliaryRows.h
Normal file
@ -0,0 +1,129 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "ItemEnums.h"
|
||||
#include "ItemAuxiliaryRows.generated.h"
|
||||
|
||||
/**
|
||||
* DT_ItemProperties row (section 6.2): default runtime property for an item.
|
||||
* Multiple rows share one ItemId, so the row name is arbitrary - the database
|
||||
* indexes these by ItemId.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct ITEMDATABASE_API FItemPropertyRow : public FTableRowBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Property")
|
||||
FName ItemId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Property")
|
||||
FGameplayTag PropertyTag;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Property")
|
||||
float DefaultValue = 0.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Property")
|
||||
float Min = 0.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Property")
|
||||
float Max = 1.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Property")
|
||||
EItemPropertyReplicationPolicy ReplicationPolicy = EItemPropertyReplicationPolicy::OwnerOnly;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Property")
|
||||
bool bSave = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* DT_ItemFeatures row (section 6.3): one tunable parameter of a feature.
|
||||
* e.g. raw_meat | Rotting | DecayRatePerHour | 0.08
|
||||
* Value is stored as text so it can carry floats, ints or item ids uniformly.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct ITEMDATABASE_API FItemFeatureRow : public FTableRowBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Feature")
|
||||
FName ItemId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Feature")
|
||||
FGameplayTag Feature;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Feature")
|
||||
FName Param;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Feature")
|
||||
FString Value;
|
||||
|
||||
float GetFloat(float Fallback = 0.f) const
|
||||
{
|
||||
return Value.IsNumeric() ? FCString::Atof(*Value) : Fallback;
|
||||
}
|
||||
int32 GetInt(int32 Fallback = 0) const
|
||||
{
|
||||
return Value.IsNumeric() ? FCString::Atoi(*Value) : Fallback;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* DT_ItemInteractions row (section 6.4): a data-driven interaction option.
|
||||
* The interaction resolver turns matching rows into FInteractionOptions.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct ITEMDATABASE_API FItemInteractionRow : public FTableRowBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||
FName ItemId;
|
||||
|
||||
/** Context filter, e.g. "World", "Inventory", "Held+Vehicle", "Held+Generator". */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||
FName Context;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||
FGameplayTag ActionTag;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||
EItemInteractionMode InteractionMode = EItemInteractionMode::None;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||
int32 Priority = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||
bool bRequiresHold = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||
float HoldDuration = 0.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||
float ActionDuration = 0.f;
|
||||
|
||||
/** Free-form requirement expression resolved by the requirement system (section 18). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||
FString Requirement;
|
||||
|
||||
/** Result verb, e.g. AddToInventory / HoldWorldItem / InstallIntoSlot / TransferFuel. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||
FName Result;
|
||||
};
|
||||
|
||||
/** DT_ItemRedirects row (section 26.1): maps a retired ItemId to its replacement. */
|
||||
USTRUCT(BlueprintType)
|
||||
struct ITEMDATABASE_API FItemRedirectRow : public FTableRowBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Redirect")
|
||||
FName OldItemId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Redirect")
|
||||
FName NewItemId;
|
||||
};
|
||||
121
Source/ItemDatabase/Public/ItemContentRows.h
Normal file
121
Source/ItemDatabase/Public/ItemContentRows.h
Normal file
@ -0,0 +1,121 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "ItemEnums.h"
|
||||
#include "ItemContentRows.generated.h"
|
||||
|
||||
/** DT_Containers row (section 6): a reusable container template. */
|
||||
USTRUCT(BlueprintType)
|
||||
struct ITEMDATABASE_API FContainerDefRow : public FTableRowBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
FGameplayTag ContainerTag;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
EInventoryContainerType Type = EInventoryContainerType::SlotList;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
int32 Width = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
int32 Height = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
int32 MaxSlots = 0;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
float MaxWeight = 0.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
FGameplayTagContainer AcceptedItemTags;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Container")
|
||||
FGameplayTagContainer BlockedItemTags;
|
||||
};
|
||||
|
||||
/** DT_WorldSlots row (section 6/14): documents a world slot's contract. */
|
||||
USTRUCT(BlueprintType)
|
||||
struct ITEMDATABASE_API FWorldSlotDefRow : public FTableRowBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "World Slot")
|
||||
FGameplayTag SlotTag;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "World Slot")
|
||||
FGameplayTagContainer AcceptedItemTags;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "World Slot")
|
||||
FName AttachSocketName;
|
||||
};
|
||||
|
||||
/** DT_LootTables row (section 6): one weighted drop in a named loot table. */
|
||||
USTRUCT(BlueprintType)
|
||||
struct ITEMDATABASE_API FLootEntryRow : public FTableRowBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Loot")
|
||||
FName TableId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Loot")
|
||||
FName ItemId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Loot")
|
||||
float Weight = 1.f;
|
||||
|
||||
/** 0..1 independent chance that this entry rolls at all. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Loot")
|
||||
float Chance = 1.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Loot")
|
||||
int32 MinQuantity = 1;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Loot")
|
||||
int32 MaxQuantity = 1;
|
||||
};
|
||||
|
||||
/** One required ingredient of a recipe. */
|
||||
USTRUCT(BlueprintType)
|
||||
struct ITEMDATABASE_API FCraftingIngredient
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
FName ItemId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
int32 Quantity = 1;
|
||||
};
|
||||
|
||||
/** DT_CraftingRecipes row (section 6). */
|
||||
USTRUCT(BlueprintType)
|
||||
struct ITEMDATABASE_API FCraftingRecipeRow : public FTableRowBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
FName RecipeId;
|
||||
|
||||
/** Optional station requirement, e.g. Station.Workbench. Empty = craft anywhere. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
FGameplayTag StationTag;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
TArray<FCraftingIngredient> Inputs;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
FName OutputItemId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
int32 OutputQuantity = 1;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting")
|
||||
float CraftTime = 0.f;
|
||||
};
|
||||
8
Source/ItemDatabase/Public/ItemDatabaseLog.h
Normal file
8
Source/ItemDatabase/Public/ItemDatabaseLog.h
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Logging/LogMacros.h"
|
||||
|
||||
ITEMDATABASE_API DECLARE_LOG_CATEGORY_EXTERN(LogItemDatabase, Log, All);
|
||||
169
Source/ItemDatabase/Public/ItemDatabaseSubsystem.h
Normal file
169
Source/ItemDatabase/Public/ItemDatabaseSubsystem.h
Normal file
@ -0,0 +1,169 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "ItemDefinitionRow.h"
|
||||
#include "ItemAuxiliaryRows.h"
|
||||
#include "ItemContentRows.h"
|
||||
#include "ItemInstanceData.h"
|
||||
#include "ItemDatabaseSubsystem.generated.h"
|
||||
|
||||
/** One problem found by ValidateData (section 28). */
|
||||
USTRUCT(BlueprintType)
|
||||
struct FItemValidationIssue
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Validation")
|
||||
FName ItemId;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Validation")
|
||||
FString Message;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Validation")
|
||||
bool bIsError = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Authoritative, read-only access point for all static item data (section 35 stage 1).
|
||||
*
|
||||
* Loads the configured data tables once, flattens them into fast lookup maps and
|
||||
* exposes definition/property/feature/interaction queries plus redirect resolution
|
||||
* and a data validator. Lives on the GameInstance so it is available on both
|
||||
* server and clients; the tables themselves are never replicated (section 32).
|
||||
*/
|
||||
UCLASS()
|
||||
class ITEMDATABASE_API UItemDatabaseSubsystem : public UGameInstanceSubsystem
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
//~ USubsystem
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
/** Convenience accessor from any world context. */
|
||||
static UItemDatabaseSubsystem* Get(const UObject* WorldContext);
|
||||
|
||||
/** (Re)load every configured table and rebuild the lookup caches. */
|
||||
void RebuildCache();
|
||||
|
||||
// --- Definition lookup ---
|
||||
|
||||
/** Returns the definition for an item (after redirects), or null if unknown. C++ fast path. */
|
||||
const FItemDefinitionRow* GetItemDefinition(FName ItemId) const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Database")
|
||||
bool FindItemDefinition(FName ItemId, FItemDefinitionRow& OutDefinition) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||
bool HasItem(FName ItemId) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||
void GetAllItemIds(TArray<FName>& OutItemIds) const;
|
||||
|
||||
/** Applies the redirect chain; returns the input unchanged if no redirect exists. */
|
||||
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||
FName ResolveRedirect(FName ItemId) const;
|
||||
|
||||
// --- Instances ---
|
||||
|
||||
/**
|
||||
* Builds a fresh instance of ItemId with a new GUID and its default runtime
|
||||
* properties applied. The single correct way to mint an item (server-side).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Database")
|
||||
FItemInstanceData MakeItemInstance(FName ItemId, int32 Quantity = 1) const;
|
||||
|
||||
/** Writes the item's default runtime properties into Out (does not clear first). */
|
||||
void ApplyDefaultProperties(FName ItemId, FItemRuntimeProperties& Out) const;
|
||||
|
||||
/** Adds only default properties that are missing (save migration, section 26.2). */
|
||||
void ApplyMissingDefaultProperties(FName ItemId, FItemRuntimeProperties& Out) const;
|
||||
|
||||
/** Current schema version stamped onto fresh/migrated instances. */
|
||||
static constexpr int32 CurrentItemDataVersion = 1;
|
||||
|
||||
/** Applies redirects + version migration to a loaded instance (section 26). */
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Database")
|
||||
void MigrateLoadedItem(UPARAM(ref) FItemInstanceData& Item) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||
int32 GetMaxStack(FName ItemId) const;
|
||||
|
||||
// --- Auxiliary data ---
|
||||
|
||||
void GetPropertyRows(FName ItemId, TArray<FItemPropertyRow>& Out) const;
|
||||
void GetFeatureRows(FName ItemId, TArray<FItemFeatureRow>& Out) const;
|
||||
|
||||
/** Feature rows for one specific feature tag (e.g. all Rotting params of raw_meat). */
|
||||
void GetFeatureRows(FName ItemId, const FGameplayTag& Feature, TArray<FItemFeatureRow>& Out) const;
|
||||
|
||||
void GetInteractionRows(FName ItemId, TArray<FItemInteractionRow>& Out) const;
|
||||
|
||||
/** Per-property replication policy default (section 22), or None if unspecified. */
|
||||
EItemPropertyReplicationPolicy GetPropertyPolicy(FName ItemId, const FGameplayTag& PropertyTag) const;
|
||||
|
||||
/** Property tags on an item that are flagged ServerOnly (never sent to clients). */
|
||||
void GetServerOnlyProperties(FName ItemId, TArray<FGameplayTag>& Out) const;
|
||||
|
||||
// --- Containers / world slots / loot / recipes (section 6) ---
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||
bool FindContainerDef(FGameplayTag ContainerTag, FContainerDefRow& OutDef) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||
bool FindWorldSlotDef(FGameplayTag SlotTag, FWorldSlotDefRow& OutDef) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||
bool FindRecipe(FName RecipeId, FCraftingRecipeRow& OutRecipe) const;
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Item|Database")
|
||||
void GetAllRecipes(TArray<FCraftingRecipeRow>& OutRecipes) const;
|
||||
|
||||
void GetLootEntries(FName TableId, TArray<FLootEntryRow>& Out) const;
|
||||
|
||||
/** Rolls a loot table into a list of fresh item instances (server). */
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Database")
|
||||
void RollLoot(FName TableId, int32 RollCount, TArray<FItemInstanceData>& OutItems) const;
|
||||
|
||||
// --- Validation (section 28) ---
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Item|Database")
|
||||
void ValidateData(TArray<FItemValidationIssue>& OutIssues) const;
|
||||
|
||||
private:
|
||||
void LoadDefinitions(UDataTable* Table);
|
||||
void LoadProperties(UDataTable* Table);
|
||||
void LoadFeatures(UDataTable* Table);
|
||||
void LoadInteractions(UDataTable* Table);
|
||||
void LoadRedirects(UDataTable* Table);
|
||||
void LoadContainers(UDataTable* Table);
|
||||
void LoadWorldSlots(UDataTable* Table);
|
||||
void LoadLoot(UDataTable* Table);
|
||||
void LoadRecipes(UDataTable* Table);
|
||||
|
||||
/** ItemId -> definition. */
|
||||
TMap<FName, FItemDefinitionRow> Definitions;
|
||||
|
||||
/** ItemId -> default property rows. */
|
||||
TMap<FName, TArray<FItemPropertyRow>> PropertiesByItem;
|
||||
|
||||
/** ItemId -> feature parameter rows. */
|
||||
TMap<FName, TArray<FItemFeatureRow>> FeaturesByItem;
|
||||
|
||||
/** ItemId -> interaction rows. */
|
||||
TMap<FName, TArray<FItemInteractionRow>> InteractionsByItem;
|
||||
|
||||
/** Old -> new id. */
|
||||
TMap<FName, FName> Redirects;
|
||||
|
||||
TMap<FGameplayTag, FContainerDefRow> ContainerDefs;
|
||||
TMap<FGameplayTag, FWorldSlotDefRow> WorldSlotDefs;
|
||||
TMap<FName, TArray<FLootEntryRow>> LootByTable;
|
||||
TMap<FName, FCraftingRecipeRow> RecipesById;
|
||||
|
||||
bool bCacheBuilt = false;
|
||||
};
|
||||
112
Source/ItemDatabase/Public/ItemDefinitionRow.h
Normal file
112
Source/ItemDatabase/Public/ItemDefinitionRow.h
Normal file
@ -0,0 +1,112 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "ItemPlacementRules.h"
|
||||
#include "ItemDefinitionRow.generated.h"
|
||||
|
||||
class UTexture2D;
|
||||
class UStaticMesh;
|
||||
|
||||
/**
|
||||
* Static, design-time data for an item type (section 4.2 / 6.1 DT_Items).
|
||||
*
|
||||
* One row per ItemId. Assets are soft references so the database never force-loads
|
||||
* meshes/icons just to answer a flag query (section 33). Never replicated; never
|
||||
* saved - only the ItemId travels in FItemInstanceData.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct ITEMDATABASE_API FItemDefinitionRow : public FTableRowBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity")
|
||||
FName ItemId;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity")
|
||||
FText DisplayName;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity", meta = (MultiLine = true))
|
||||
FText Description;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity")
|
||||
FGameplayTag ItemType;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Identity")
|
||||
FGameplayTagContainer ItemTags;
|
||||
|
||||
// --- Visuals (soft refs, section 33) ---
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visual")
|
||||
TSoftObjectPtr<UTexture2D> Icon;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visual")
|
||||
TSoftObjectPtr<UStaticMesh> WorldMesh;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visual")
|
||||
TSoftClassPtr<AActor> PickupActorClass;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Visual")
|
||||
TSoftClassPtr<AActor> EquippedActorClass;
|
||||
|
||||
// --- Stacking / weight / grid ---
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stack", meta = (ClampMin = "1"))
|
||||
int32 MaxStack = 1;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stack", meta = (ClampMin = "0"))
|
||||
float Weight = 0.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stack")
|
||||
FIntPoint GridSize = FIntPoint(1, 1);
|
||||
|
||||
// --- Capability flags (section 4.2) ---
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||
bool bCanStoreInInventory = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||
bool bCanInstantPickup = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||
bool bCanWorldCarry = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||
bool bCanEquip = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||
bool bCanPlace = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||
bool bCanInstallIntoWorldSlot = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||
bool bCanDrop = true;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Capabilities")
|
||||
bool bIsQuestCritical = false;
|
||||
|
||||
// --- Interaction defaults ---
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||
FGameplayTag DefaultInteraction;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Interaction")
|
||||
FGameplayTag AlternativeInteraction;
|
||||
|
||||
// --- Slot compatibility ---
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Slots")
|
||||
FGameplayTagContainer AllowedEquipmentSlots;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Slots")
|
||||
FGameplayTagContainer CompatibleWorldSlots;
|
||||
|
||||
// --- Features ---
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Features")
|
||||
FGameplayTagContainer ItemFeatures;
|
||||
|
||||
// --- Placement (section 13.3); only meaningful when bCanPlace ---
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement", meta = (EditCondition = "bCanPlace"))
|
||||
FItemPlacementRules PlacementRules;
|
||||
|
||||
bool HasFeature(const FGameplayTag& Feature) const { return ItemFeatures.HasTag(Feature); }
|
||||
};
|
||||
63
Source/ItemDatabase/Public/ItemEcosystemSettings.h
Normal file
63
Source/ItemDatabase/Public/ItemEcosystemSettings.h
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DeveloperSettings.h"
|
||||
#include "ItemEcosystemSettings.generated.h"
|
||||
|
||||
class UDataTable;
|
||||
|
||||
/**
|
||||
* Project Settings -> Game -> Item Ecosystem.
|
||||
* Designers assign the data tables here; the database subsystem reads them on init.
|
||||
* Keeping the wiring in settings means no code change to repoint tables (section 3).
|
||||
*/
|
||||
UCLASS(Config = Game, DefaultConfig, meta = (DisplayName = "Item Ecosystem"))
|
||||
class ITEMDATABASE_API UItemEcosystemSettings : public UDeveloperSettings
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UItemEcosystemSettings();
|
||||
|
||||
virtual FName GetCategoryName() const override { return FName("Game"); }
|
||||
|
||||
/** DT_Items - row struct FItemDefinitionRow. The one required table. */
|
||||
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||
TSoftObjectPtr<UDataTable> ItemsTable;
|
||||
|
||||
/** DT_ItemProperties - row struct FItemPropertyRow. */
|
||||
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||
TSoftObjectPtr<UDataTable> PropertiesTable;
|
||||
|
||||
/** DT_ItemFeatures - row struct FItemFeatureRow. */
|
||||
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||
TSoftObjectPtr<UDataTable> FeaturesTable;
|
||||
|
||||
/** DT_ItemInteractions - row struct FItemInteractionRow. */
|
||||
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||
TSoftObjectPtr<UDataTable> InteractionsTable;
|
||||
|
||||
/** DT_ItemRedirects - row struct FItemRedirectRow. */
|
||||
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||
TSoftObjectPtr<UDataTable> RedirectsTable;
|
||||
|
||||
/** DT_Containers - row struct FContainerDefRow. */
|
||||
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||
TSoftObjectPtr<UDataTable> ContainersTable;
|
||||
|
||||
/** DT_WorldSlots - row struct FWorldSlotDefRow. */
|
||||
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||
TSoftObjectPtr<UDataTable> WorldSlotsTable;
|
||||
|
||||
/** DT_LootTables - row struct FLootEntryRow. */
|
||||
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||
TSoftObjectPtr<UDataTable> LootTable;
|
||||
|
||||
/** DT_CraftingRecipes - row struct FCraftingRecipeRow. */
|
||||
UPROPERTY(Config, EditAnywhere, Category = "Data Tables", meta = (AllowedClasses = "/Script/Engine.DataTable"))
|
||||
TSoftObjectPtr<UDataTable> RecipesTable;
|
||||
|
||||
static const UItemEcosystemSettings* Get();
|
||||
};
|
||||
69
Source/ItemDatabase/Public/ItemPlacementRules.h
Normal file
69
Source/ItemDatabase/Public/ItemPlacementRules.h
Normal file
@ -0,0 +1,69 @@
|
||||
// Copyright ExByte Studios. Item Interaction Ecosystem.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameplayTagContainer.h"
|
||||
#include "ItemPlacementRules.generated.h"
|
||||
|
||||
/** How a placeable item snaps when previewed/placed (section 13.3). */
|
||||
UENUM(BlueprintType)
|
||||
enum class EItemPlacementMode : uint8
|
||||
{
|
||||
FreeSurface,
|
||||
SnapToGrid,
|
||||
SnapToSocket
|
||||
};
|
||||
|
||||
/**
|
||||
* Static placement constraints for a placeable item (section 13.3).
|
||||
* Evaluated locally for the ghost preview and re-validated on the server.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct ITEMDATABASE_API FItemPlacementRules
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||
EItemPlacementMode PlacementMode = EItemPlacementMode::FreeSurface;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||
float MaxDistance = 400.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||
bool bRequiresFlatSurface = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement", meta = (ClampMin = "0", ClampMax = "90"))
|
||||
float MaxSlopeAngle = 35.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||
FName CollisionProfile = FName("BlockAll");
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||
FGameplayTagContainer AllowedSurfaceTags;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||
FGameplayTagContainer BlockedSurfaceTags;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||
float SnapToGrid = 0.f;
|
||||
|
||||
/** When PlacementMode == SnapToSocket, only sockets carrying this tag accept the item. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||
FGameplayTag SnapToSocketTag;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||
float MinDistanceFromOtherSameItems = 0.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||
bool bCanPlaceInWater = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||
bool bCanPlaceOnVehicle = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||
bool bCanPlaceOnWall = false;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Placement")
|
||||
bool bCanPlaceOnGround = true;
|
||||
};
|
||||
Reference in New Issue
Block a user