Use typed runtime item catalog

This commit is contained in:
Meta Vens 2026-06-13 14:36:54 +01:00
commit 949907de65
11 changed files with 415 additions and 245 deletions

View file

@ -0,0 +1,338 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using GameCult.Aetheria.State.Unity;
using MessagePack;
using Unity.Mathematics;
public interface IRuntimeItemCatalogReader
{
ItemData Get(Guid guid);
T Get<T>(Guid guid) where T : ItemData;
}
public sealed class AetheriaRuntimeItemCatalog : IRuntimeItemCatalogReader
{
private readonly Dictionary<Guid, ItemData> _items;
private static readonly IReadOnlyDictionary<int, Type> BehaviorTypesByUnionKey = typeof(BehaviorData)
.GetCustomAttributes<UnionAttribute>()
.ToDictionary(attribute => attribute.Key, attribute => attribute.SubType);
public AetheriaRuntimeItemCatalog(AetheriaRuntimeCatalogSnapshot catalog)
{
if (catalog == null) throw new ArgumentNullException(nameof(catalog));
_items = catalog.Items
.Select(ProjectItem)
.Where(item => item != null)
.ToDictionary(item => item.ID, item => item);
}
public ItemData Get(Guid guid)
{
ItemData item;
_items.TryGetValue(guid, out item);
return item;
}
public T Get<T>(Guid guid) where T : ItemData
{
return Get(guid) as T;
}
private static ItemData ProjectItem(AetheriaRuntimeCatalogItem item)
{
if (!Guid.TryParse(item.LegacyId, out var id))
{
return null;
}
var projected = CreateItemData(item);
if (projected == null)
{
return null;
}
projected.ID = id;
projected.Name = item.Name;
projected.Description = item.Description;
projected.Manufacturer = ParseGuid(item.ManufacturerLegacyId);
projected.Mass = (float)item.Mass;
projected.Shape = ProjectShape(item.ShapeWidth, item.ShapeHeight, item.ShapeCells);
projected.Price = item.Price;
if (projected is SimpleCommodityData simple)
{
simple.MaxStack = item.MaxStack;
}
if (projected is EquippableItemData equippable)
{
equippable.Durability = (float)item.Durability;
equippable.Behaviors = item.BehaviorPayloads
.Select(ProjectBehavior)
.Where(behavior => behavior != null)
.ToList();
}
if (projected is GearData gear)
{
gear.Hardpoint = ParseEnum(item.HardpointType, HardpointType.Tool);
}
if (projected is CargoBayData cargoBay)
{
cargoBay.InteriorShape = ProjectShape(item.InteriorShapeWidth, item.InteriorShapeHeight, item.InteriorShapeCells);
}
if (projected is WeaponItemData weapon)
{
weapon.WeaponRange = ParseEnum(item.WeaponRange, WeaponRange.Short);
weapon.WeaponCaliber = ParseEnum(item.WeaponCaliber, WeaponCaliber.Small);
weapon.WeaponType = ParseEnum(item.WeaponType, WeaponType.Laser);
weapon.WeaponFireTypes = ParseFlags(item.WeaponFireTypes, WeaponFireType.None);
weapon.WeaponModifiers = ParseFlags(item.WeaponModifiers, WeaponModifiers.None);
}
if (projected is HullData hull)
{
hull.HullType = ParseEnum(item.HullType, HullType.Ship);
hull.Hardpoints = item.Hardpoints.Select(ProjectHardpoint).ToList();
}
return projected;
}
private static ItemData CreateItemData(AetheriaRuntimeCatalogItem item)
{
switch (item.Category)
{
case "SimpleCommodityData":
return new SimpleCommodityData();
case "CompoundCommodityData":
return new CompoundCommodityData();
case "ConsumableItemData":
return new ConsumableItemData();
case "GearData":
return new GearData();
case "CargoBayData":
return new CargoBayData();
case "DockingBayData":
return new DockingBayData();
case "WeaponItemData":
return new WeaponItemData();
case "HullData":
return new HullData();
default:
return !string.IsNullOrWhiteSpace(item.HardpointType) ? new GearData() : null;
}
}
private static Shape ProjectShape(int width, int height, IReadOnlyList<AetheriaRuntimeShapeCell> cells)
{
var shape = new Shape(Math.Max(width, 1), Math.Max(height, 1));
foreach (var coordinate in shape.AllCoordinates)
{
shape[coordinate] = false;
}
foreach (var cell in cells)
{
shape[new int2(cell.X, cell.Y)] = true;
}
return shape;
}
private static HardpointData ProjectHardpoint(AetheriaRuntimeHardpoint hardpoint)
{
return new HardpointData
{
Type = ParseEnum(hardpoint.Type, HardpointType.Hull),
Position = new int2(hardpoint.PositionX, hardpoint.PositionY),
Shape = ProjectShape(hardpoint.ShapeWidth, hardpoint.ShapeHeight, hardpoint.ShapeCells),
Transform = hardpoint.Transform,
Rotation = ParseEnum(hardpoint.Rotation, ItemRotation.None),
Armor = (float)hardpoint.Armor
};
}
private static BehaviorData ProjectBehavior(AetheriaRuntimeBehaviorPayload payload)
{
if (!BehaviorTypesByUnionKey.TryGetValue(payload.UnionKey, out var behaviorType))
{
return null;
}
var behavior = Activator.CreateInstance(behaviorType) as BehaviorData;
if (behavior == null)
{
return null;
}
behavior.Group = payload.Group;
foreach (var field in GetKeyedFields(behaviorType))
{
var payloadField = payload.Fields.FirstOrDefault(candidate => candidate.Key == field.key);
if (payloadField == null)
{
continue;
}
field.field.SetValue(behavior, ConvertValue(payloadField.Value, field.field.FieldType));
}
return behavior;
}
private static IEnumerable<(int key, FieldInfo field)> GetKeyedFields(Type type)
{
return type
.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Select(field => (attribute: field.GetCustomAttribute<KeyAttribute>(), field))
.Where(entry => entry.attribute?.IntKey != null)
.Select(entry => (entry.attribute.IntKey.GetValueOrDefault(), entry.field));
}
private static object ConvertValue(AetheriaRuntimeBehaviorValue value, Type targetType)
{
if (targetType == typeof(string)) return value.StringValue ?? "";
if (targetType == typeof(bool)) return value.BoolValue;
if (targetType == typeof(float)) return (float)value.NumberValue;
if (targetType == typeof(double)) return value.NumberValue;
if (targetType == typeof(int)) return checked((int)value.NumberValue);
if (targetType == typeof(uint)) return checked((uint)value.NumberValue);
if (targetType == typeof(Guid)) return ParseGuid(value.LegacyIdValue);
if (targetType == typeof(float2)) return ConvertFloat2(value);
if (targetType == typeof(float3)) return ConvertFloat3(value);
if (targetType == typeof(float4)) return ConvertFloat4(value);
if (targetType == typeof(int2)) return ConvertInt2(value);
if (targetType.IsEnum) return ParseEnumValue(value, targetType);
if (targetType.IsArray)
{
return ConvertArray(value, targetType.GetElementType());
}
if (typeof(IList).IsAssignableFrom(targetType) && targetType.IsGenericType)
{
return ConvertList(value, targetType);
}
if (targetType.GetCustomAttribute<MessagePackObjectAttribute>() != null)
{
return ConvertKeyedObject(value, targetType);
}
return targetType.IsValueType ? Activator.CreateInstance(targetType) : null;
}
private static object ConvertArray(AetheriaRuntimeBehaviorValue value, Type elementType)
{
var array = Array.CreateInstance(elementType, value.Children.Count);
for (var index = 0; index < value.Children.Count; index++)
{
array.SetValue(ConvertValue(value.Children[index], elementType), index);
}
return array;
}
private static object ConvertList(AetheriaRuntimeBehaviorValue value, Type targetType)
{
var elementType = targetType.GetGenericArguments()[0];
var list = Activator.CreateInstance(targetType) as IList;
foreach (var child in value.Children)
{
list.Add(ConvertValue(child, elementType));
}
return list;
}
private static object ConvertKeyedObject(AetheriaRuntimeBehaviorValue value, Type targetType)
{
var instance = Activator.CreateInstance(targetType);
var children = value.Children;
foreach (var field in GetKeyedFields(targetType))
{
if (field.key >= 0 && field.key < children.Count)
{
field.field.SetValue(instance, ConvertValue(children[field.key], field.field.FieldType));
}
}
return instance;
}
private static object ParseEnumValue(AetheriaRuntimeBehaviorValue value, Type targetType)
{
if (!string.IsNullOrWhiteSpace(value.StringValue))
{
return Enum.Parse(targetType, value.StringValue, true);
}
return Enum.ToObject(targetType, checked((int)value.NumberValue));
}
private static Guid ParseGuid(string value)
{
return Guid.TryParse(value, out var result) ? result : Guid.Empty;
}
private static T ParseEnum<T>(string value, T fallback) where T : struct
{
return Enum.TryParse(value, true, out T parsed) ? parsed : fallback;
}
private static T ParseFlags<T>(string value, T fallback) where T : struct
{
if (string.IsNullOrWhiteSpace(value))
{
return fallback;
}
return value
.Split('|')
.Select(part => part.Trim())
.Where(part => part.Length > 0)
.Aggregate(fallback, (current, part) =>
Enum.TryParse(part, true, out T parsed)
? (T)Enum.ToObject(typeof(T), Convert.ToInt32(current) | Convert.ToInt32(parsed))
: current);
}
private static float2 ConvertFloat2(AetheriaRuntimeBehaviorValue value)
{
return new float2(
value.Children.Count > 0 ? (float)value.Children[0].NumberValue : 0,
value.Children.Count > 1 ? (float)value.Children[1].NumberValue : 0);
}
private static float3 ConvertFloat3(AetheriaRuntimeBehaviorValue value)
{
return new float3(
value.Children.Count > 0 ? (float)value.Children[0].NumberValue : 0,
value.Children.Count > 1 ? (float)value.Children[1].NumberValue : 0,
value.Children.Count > 2 ? (float)value.Children[2].NumberValue : 0);
}
private static float4 ConvertFloat4(AetheriaRuntimeBehaviorValue value)
{
return new float4(
value.Children.Count > 0 ? (float)value.Children[0].NumberValue : 0,
value.Children.Count > 1 ? (float)value.Children[1].NumberValue : 0,
value.Children.Count > 2 ? (float)value.Children[2].NumberValue : 0,
value.Children.Count > 3 ? (float)value.Children[3].NumberValue : 0);
}
private static int2 ConvertInt2(AetheriaRuntimeBehaviorValue value)
{
return new int2(
value.Children.Count > 0 ? checked((int)value.Children[0].NumberValue) : 0,
value.Children.Count > 1 ? checked((int)value.Children[1].NumberValue) : 0);
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 897dd3ee37f64ef987d554516646c5cf
timeCreated: 1781359840

View file

@ -68,7 +68,7 @@ public abstract class DatabaseEntry
public class DatabaseLink<T> : DatabaseLinkBase where T : ItemData
{
[IgnoreMember]
public T Value => ResolveLegacyItemCatalog<T>(LinkID);
public T Value => ResolveRuntimeItemCatalog<T>(LinkID);
}
[MessagePackObject]
@ -78,17 +78,17 @@ public class DatabaseLinkBase
public Guid LinkID;
[IgnoreMember]
private static ILegacyItemCatalogReader Catalog { get; set; }
private static IRuntimeItemCatalogReader Catalog { get; set; }
protected static T ResolveLegacyItemCatalog<T>(Guid linkId) where T : ItemData
protected static T ResolveRuntimeItemCatalog<T>(Guid linkId) where T : ItemData
{
if (Catalog == null)
throw new InvalidOperationException("Legacy item link resolution is not bound. Open LegacyItemCatalogBoundary before reading legacy item object graphs.");
throw new InvalidOperationException("Runtime item catalog resolution is not bound. Open typed Aetheria runtime state before reading item object graphs.");
return Catalog.Get<T>(linkId);
}
public static void BindLegacyItemCatalog(ILegacyItemCatalogReader catalog)
public static void BindRuntimeItemCatalog(IRuntimeItemCatalogReader catalog)
{
if (catalog == null) throw new ArgumentNullException(nameof(catalog));

View file

@ -1,114 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System;
using System.Collections.Generic;
using System.IO;
using MessagePack;
using UniRx;
public interface ILegacyItemCatalogReader
{
ItemData Get(Guid guid);
T Get<T>(Guid guid) where T : ItemData;
}
public class LegacyItemCatalogCache : ILegacyItemCatalogReader
{
private readonly object addLock = new object();
private List<CacheBackingStore> _backingStores = new List<CacheBackingStore>();
private readonly Dictionary<Guid, ItemData> _entries = new Dictionary<Guid, ItemData>();
public void AddBackingStore(CacheBackingStore store)
{
_backingStores.Add(store);
store.EntryAdded.Subscribe(entry =>
{
AddInternal(entry, store);
});
}
public void PullAllBackingStores()
{
foreach(var store in _backingStores) store.PullAll();
}
private void AddInternal(DatabaseEntry entry, CacheBackingStore source = null)
{
lock(addLock)
{
if (entry is ItemData item)
{
_entries[item.ID] = item;
}
}
}
public ItemData Get(Guid guid)
{
ItemData entry;
_entries.TryGetValue(guid, out entry);
return entry;
}
public T Get<T>(Guid guid) where T : ItemData
{
return Get(guid) as T;
}
}
public abstract class CacheBackingStore
{
protected CacheBackingStore()
{
EntryAdded = new Subject<DatabaseEntry>();
}
public abstract void PullAll();
public Subject<DatabaseEntry> EntryAdded { get; }
protected Dictionary<Guid, DatabaseEntry> Entries = new Dictionary<Guid, DatabaseEntry>();
}
public abstract class SingleFileBackingStore : CacheBackingStore
{
public FileInfo FileInfo { get; }
public abstract DatabaseEntry[] Deserialize(byte[] data);
public SingleFileBackingStore(string filePath)
{
FileInfo = new FileInfo(filePath);
}
public override void PullAll()
{
if (!FileInfo.Exists) return;
foreach (var entry in Deserialize(File.ReadAllBytes(FileInfo.FullName)))
{
Entries[entry.ID] = entry;
EntryAdded.OnNext(entry);
}
}
}
public class SingleFileMessagePackBackingStore : SingleFileBackingStore
{
public SingleFileMessagePackBackingStore(string filePath) : base(filePath)
{
RegisterResolver.Register();
}
public override DatabaseEntry[] Deserialize(byte[] data)
{
return MessagePackSerializer.Deserialize<DatabaseEntry[]>(data);
}
}

View file

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: a43b39c6991618b43aed4a24ffcdc0ff
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -30,7 +30,7 @@ public class ItemManager
// private Guid _forceLoadZone;
private readonly ILegacyItemCatalogReader _itemData;
private readonly IRuntimeItemCatalogReader _itemData;
public GameplaySettings GameplaySettings { get; }
// public double Time
@ -46,7 +46,7 @@ public class ItemManager
// private readonly Dictionary<CraftedItemData, int> Tier = new Dictionary<CraftedItemData, int>();
public ItemManager(ILegacyItemCatalogReader itemData, GameplaySettings settings, Action<string> logger)
public ItemManager(IRuntimeItemCatalogReader itemData, GameplaySettings settings, Action<string> logger)
{
_itemData = itemData;
GameplaySettings = settings;