Delete runtime item DTO hydration

This commit is contained in:
Meta Vens 2026-06-14 07:52:18 +01:00
commit c0d0fbf656
7 changed files with 49 additions and 327 deletions

Binary file not shown.

View file

@ -10,13 +10,11 @@ public interface IRuntimeItemProjectionReader
{
AetheriaRuntimeCatalogItem GetRuntimeItem(Guid guid);
IReadOnlyList<BehaviorData> GetBehaviorProjections(Guid guid);
ItemData Get(Guid guid);
}
public sealed class AetheriaRuntimeItemCatalog : IRuntimeItemProjectionReader
{
private readonly Dictionary<Guid, AetheriaRuntimeCatalogItem> _typedItems;
private readonly Dictionary<Guid, ItemData> _items;
private static readonly IReadOnlyDictionary<int, Type> BehaviorTypesByUnionKey = new Dictionary<int, Type>
{
{ 0, typeof(GuidedWeaponData) },
@ -59,11 +57,6 @@ public sealed class AetheriaRuntimeItemCatalog : IRuntimeItemProjectionReader
_typedItems = catalog.Items
.Where(item => Guid.TryParse(item.LegacyId, out _))
.ToDictionary(item => Guid.Parse(item.LegacyId), item => item);
_items = catalog.Items
.Select(ProjectItem)
.Where(item => item != null)
.ToDictionary(item => item.ID, item => item);
}
public AetheriaRuntimeCatalogItem GetRuntimeItem(Guid guid)
@ -80,95 +73,6 @@ public sealed class AetheriaRuntimeItemCatalog : IRuntimeItemProjectionReader
: ProjectBehaviors(item);
}
public ItemData Get(Guid guid)
{
ItemData item;
_items.TryGetValue(guid, out item);
return item;
}
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.SpecificHeat = (float)item.SpecificHeat;
projected.Conductivity = (float)item.Conductivity;
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 ConsumableItemData consumable)
{
consumable.Stackable = item.Stackable;
consumable.Duration = (float)item.Duration;
consumable.Effectiveness = ProjectCurve(item.EffectivenessCurveKeys);
consumable.Behaviors = ProjectBehaviors(item).ToList();
}
if (projected is EquippableItemData equippable)
{
equippable.Durability = (float)item.Durability;
equippable.ActionBarIcon = item.ActionBarIcon;
equippable.ThermalResilience = (float)item.ThermalResilience;
equippable.AudioStats = ProjectAudioStats(item).ToList();
equippable.Behaviors = ProjectBehaviors(item).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 DockingBayData dockingBay)
{
dockingBay.MaxSize = new int2(item.DockingMaxSizeX, item.DockingMaxSizeY);
}
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();
hull.GridOffset = (float)item.HullGridOffset;
hull.Armor = (float)item.HullArmor;
hull.Drag = (float)item.HullDrag;
hull.CanTow = item.HullCanTow;
}
return projected;
}
private static BehaviorData[] ProjectBehaviors(AetheriaRuntimeCatalogItem item)
{
return item.BehaviorPayloads
@ -177,29 +81,6 @@ public sealed class AetheriaRuntimeItemCatalog : IRuntimeItemProjectionReader
.ToArray();
}
private static BezierCurve ProjectCurve(IReadOnlyList<AetheriaRuntimeCurveKey> keys)
{
return new BezierCurve
{
Keys = keys == null || keys.Count == 0
? new[] { new float4(0, 1, 0, 0), new float4(1, 1, 0, 0) }
: keys.Select(key => new float4(
(float)key.Time,
(float)key.Value,
(float)key.InTangent,
(float)key.OutTangent)).ToArray()
};
}
private static IEnumerable<AudioStat> ProjectAudioStats(AetheriaRuntimeCatalogItem item)
{
return item.AudioStats.Select(audioStat => new AudioStat
{
Parameter = audioStat.Parameter,
Stat = ProjectPerformanceStat(audioStat.Stat)
});
}
private static PerformanceStat ProjectPerformanceStat(AetheriaRuntimePerformanceStat stat)
{
return new PerformanceStat
@ -212,60 +93,6 @@ public sealed class AetheriaRuntimeItemCatalog : IRuntimeItemProjectionReader
};
}
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))
@ -390,28 +217,6 @@ public sealed class AetheriaRuntimeItemCatalog : IRuntimeItemProjectionReader
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(

View file

@ -1247,7 +1247,6 @@ public class ConsumableItemEffect
public float RemainingDuration { get; private set; }
public Entity Entity { get; }
public ConsumableItem Item { get; }
public ConsumableItemData Data { get; }
public Behavior[] Behaviors { get; }
public AetheriaRuntimeCatalogItem RuntimeItem { get; }
public float Duration => Math.Max(_duration, float.Epsilon);
@ -1260,9 +1259,8 @@ public class ConsumableItemEffect
Item = item;
Entity = entity;
RuntimeItem = entity.ItemManager.GetRuntimeItem(item);
Data = entity.ItemManager.GetData(item) as ConsumableItemData;
_duration = RuntimeItem?.Duration > 0 ? (float)RuntimeItem.Duration : Data?.Duration ?? 0;
_effectiveness = CreateEffectivenessCurve(RuntimeItem, Data);
_duration = RuntimeItem?.Duration > 0 ? (float)RuntimeItem.Duration : 0;
_effectiveness = CreateEffectivenessCurve(RuntimeItem);
RemainingDuration = _duration;
Behaviors = entity.ItemManager.GetRuntimeBehaviorProjections(item)
@ -1297,7 +1295,7 @@ public class ConsumableItemEffect
return result;
}
private static BezierCurve CreateEffectivenessCurve(AetheriaRuntimeCatalogItem item, ConsumableItemData fallback)
private static BezierCurve CreateEffectivenessCurve(AetheriaRuntimeCatalogItem item)
{
if (item?.EffectivenessCurveKeys != null && item.EffectivenessCurveKeys.Count > 0)
{
@ -1313,7 +1311,7 @@ public class ConsumableItemEffect
};
}
return fallback?.Effectiveness ?? new BezierCurve
return new BezierCurve
{
Keys = new[] { new float4(0, 1, 0, 0), new float4(1, 1, 0, 0) }
};
@ -1342,7 +1340,6 @@ public class EquippedItem
public float Wear { get; private set; }
public Shape InsetShape { get; }
public Entity Entity { get; }
public EquippableItemData Data { get; }
public AetheriaRuntimeCatalogItem RuntimeItem { get; }
public ReactiveProperty<bool> ThermalOnline { get; } = new ReactiveProperty<bool>(false);
@ -1443,16 +1440,15 @@ public class EquippedItem
public EquippedItem(ItemManager itemManager, EquippableItem item, int2 position, Entity entity)
{
ItemManager = itemManager;
Data = ItemManager.GetData(item);
Entity = entity;
EquippableItem = item;
Position = position;
RuntimeItem = ItemManager.GetRuntimeItem(item);
Conductivity = RuntimeItem != null ? (float)RuntimeItem.Conductivity : Data.Conductivity;
Conductivity = RuntimeItem != null ? (float)RuntimeItem.Conductivity : 0;
MaxDurability = RuntimeItem?.Durability > 0 ? (float)RuntimeItem.Durability : Math.Max(item.Durability, 1f);
ThermalResilience = RuntimeItem?.ThermalResilience > 0 ? (float)RuntimeItem.ThermalResilience : Data.ThermalResilience;
ThermalResilience = RuntimeItem?.ThermalResilience > 0 ? (float)RuntimeItem.ThermalResilience : 1;
_thermalPerformanceCurve = CreateThermalPerformanceCurve(RuntimeItem);
_audioStats = CreateAudioStatBindings(RuntimeItem, Data);
_audioStats = CreateAudioStatBindings(RuntimeItem);
ThermalExponent = lerp(
ItemManager.GameplaySettings.ThermalQualityMin,
ItemManager.GameplaySettings.ThermalQualityMax,
@ -1541,7 +1537,7 @@ public class EquippedItem
RuntimeItem == null ||
RuntimeItem.MaximumTemperature <= RuntimeItem.MinimumTemperature)
{
return Data.Performance(temperature);
return 1;
}
var t = unlerp((float)RuntimeItem.MinimumTemperature, (float)RuntimeItem.MaximumTemperature, temperature);
@ -1594,9 +1590,7 @@ public class EquippedItem
return null;
}
private static EquippedItemAudioStatBinding[] CreateAudioStatBindings(
AetheriaRuntimeCatalogItem item,
EquippableItemData fallback)
private static EquippedItemAudioStatBinding[] CreateAudioStatBindings(AetheriaRuntimeCatalogItem item)
{
if (item?.AudioStats != null)
{
@ -1607,9 +1601,7 @@ public class EquippedItem
.ToArray();
}
return fallback.AudioStats
.Select(audioStat => new EquippedItemAudioStatBinding(audioStat.Parameter, audioStat.Stat))
.ToArray();
return Array.Empty<EquippedItemAudioStatBinding>();
}
private static PerformanceStat CreatePerformanceStat(AetheriaRuntimePerformanceStat stat)

View file

@ -94,49 +94,11 @@ public class ItemManager
return new RuntimeItemReference(itemId);
}
public ItemData GetData(ItemInstance item)
{
Hydrate(item);
return item?.Data?.Projection;
}
public void Hydrate(ItemInstance item)
{
if (item?.Data == null || item.Data.Projection != null)
{
return;
}
var data = _itemProjections.Get(item.Data.ItemId);
if (data == null)
{
_logger($"Attempted to hydrate missing item id {item.Data.ItemId}");
return;
}
item.Data.SetProjection(data);
}
public void Log(string s)
{
_logger(s);
}
public SimpleCommodityData GetData(SimpleCommodity item)
{
return GetData((ItemInstance)item) as SimpleCommodityData;
}
public CraftedItemData GetData(CraftedItemInstance item)
{
return GetData((ItemInstance)item) as CraftedItemData;
}
public EquippableItemData GetData(EquippableItem item)
{
return GetData((ItemInstance)item) as EquippableItemData;
}
public float GetMass(ItemInstance item)
{
var typedItem = GetRuntimeItem(item);

View file

@ -28,7 +28,6 @@ public abstract class RuntimeItemProjectionEntry
public class RuntimeItemReference
{
public ItemData Projection { get; private set; }
public Guid ItemId;
public RuntimeItemReference()
@ -39,17 +38,6 @@ public class RuntimeItemReference
{
ItemId = itemId;
}
public RuntimeItemReference(ItemData value)
{
SetProjection(value);
}
public void SetProjection(ItemData value)
{
Projection = value;
ItemId = value?.ID ?? Guid.Empty;
}
}
public interface ITintInspector

View file

@ -346,29 +346,6 @@ public class PropertiesPanel : MonoBehaviour
OnPropertyAdded?.Invoke(field.gameObject);
}
// public void Inspect(Entity entity)
// {
// Clear();
// Title.text = entity.Name;
// var hullData = Context.GetData(entity.Hull) as HullData;
// AddSection(
// hullData.HullType == HullType.Ship ? "Ship" :
// hullData.HullType == HullType.Station ? "Station" :
// "Platform");
// //AddList(hullData.Name).Inspect(hull, entity);
// //PropertiesPanel.AddProperty("Hull", () => $"{hullData.Name}");
// AddEntityProperties(entity);
// //cargoList.SetExpanded(false,true);
//
// RefreshValues();
// }
// private void AddEntityProperties(Entity entity)
// {
// AddField("Name", () => entity.Name, name => entity.Name = name);
// AddProperty("Mass", () => $"{entity.Mass.SignificantDigits(Context.GameplaySettings.SignificantDigits)}");
// }
private void AddItemProperties(ItemInstance item)
{
var typedItem = FindTypedPropertyItem(item);

View file

@ -80,11 +80,10 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
same typed player-settings commit after mutating the runtime projection. The
action bar also uses typed runtime catalog category rows to reject
non-consumable inventory drops and creates consumable bindings around typed
catalog rows before hydrating any legacy DTO. Missing typed rows are rejected
for consumable binding instead of falling through to legacy DTO classification.
Consumable activation and active-duration fill still resolve legacy
`ConsumableItemData` lazily because runtime effects and duration are not typed
execution surfaces yet. Gear action-bar bindings read custom icon resource
catalog rows. Missing typed rows are rejected for consumable binding instead
of falling through to legacy DTO classification. Consumable activation,
active-duration fill, runtime duration, and effectiveness curves now use typed
item rows with neutral defaults for missing optional facets. Gear action-bar bindings read custom icon resource
paths from typed item rows, then use typed weapon and hardpoint facets for
fallback icon selection, and finally fall back to a generic tool icon when
typed facets are incomplete. The current legacy catalog has zero populated
@ -134,20 +133,19 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
- `ActionGameManager` opens `AetheriaRuntimeCatalogStore` over
`aetheria-world.cc`, projects it through `AetheriaRuntimeItemCatalog`, and
gives `ItemManager` explicit item lookup authority. Item instances carry
`RuntimeItemReference`, an item-definition id plus optional hydrated
projection cache, not a process-global catalog resolver or durable item-data
owner. The old `LegacyItemCatalogBoundary`,
`RuntimeItemReference`, an item-definition id, not a process-global catalog
resolver, hydrated projection cache, or durable item-data owner. The old
`LegacyItemCatalogBoundary`,
`LegacyItemCatalogCache`, and runtime MessagePack deserializer path have been
deleted. The old `DatabaseEntry`/`RuntimeCatalogEntry` base has been demoted
to `RuntimeItemProjectionEntry` for surviving item DTOs, and the old generic
`DatabaseLink<T>`/`RuntimeCatalogLink<T>` path has collapsed into the
item-specific runtime reference.
- `ItemManager` no longer exposes the raw runtime item catalog reader as public
gameplay/UI API. Temporary item projection hydration now goes through
`ItemManager.GetData` over `RuntimeItemReference.ItemId`. `ItemManager` no
longer exposes a legacy catalog enumeration API or a catalog-entry-shaped
single-item API. This prevents catalog authority from leaking into every
caller that only needs a projection lookup. The item properties UI no longer uses
gameplay/UI API, and its old `GetData`/`Hydrate` item DTO projection path has
been deleted. `AetheriaRuntimeItemCatalog` no longer materializes whole
`ItemData` DTOs as a runtime cache; it exposes typed item rows plus the
temporary behavior payload projection bridge. The item properties UI no longer uses
`ItemManager` for manufacturer display; it resolves the manufacturer through
the package-owned `ActionGameManager.RuntimeCatalog` typed snapshot. Entity
restore and loadout manufacturer-distance weighting no longer use `ItemManager`
@ -241,13 +239,13 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted.
conduction, durability performance, thermal performance, and wear scaling.
Thermal resilience is also typed and feeds wear scaling. Audio parameter stat
bindings are typed item rows and feed equipped-item Wwise parameter updates;
the legacy equippable DTO is now only an incomplete-row projection fallback
for equipped-item simulation. Consumable stackability, duration, and
missing optional thermal/audio facets now degrade to neutral runtime defaults
instead of hydrating legacy equippable DTOs. Consumable stackability, duration, and
effectiveness curve slots are part of typed item rows, and action-bar
consumable activation/fill now command typed item IDs. The current mapped
legacy catalog has no consumable item rows, so `ConsumableItemData` remains a
compatibility fallback for any future/unmapped consumable instance until
consumable behavior execution is fully typed.
legacy catalog has no consumable item rows, so consumable runtime behavior
still needs future typed data coverage, but it no longer hydrates
`ConsumableItemData` as a fallback.
Inventory drag preview occupancy now projects typed item shape cells only into
the local `Shape` grid, and final fit/equip acceptance shares the typed
runtime catalog geometry path in `Entity.ItemFits` and `TryEquip`.
@ -772,6 +770,10 @@ First Aetheria surfaces to publish:
typed runtime item rows; `ItemManager.Instantiate` now clones simple
commodities, crafted items, consumables, and equippables from
`AetheriaRuntimeCatalogItem` identity and facets.
- Done: delete the runtime item DTO hydration bridge; `ItemManager.GetData`,
`ItemManager.Hydrate`, `IRuntimeItemProjectionReader.Get`, the
`AetheriaRuntimeItemCatalog` whole-item DTO cache, and
`RuntimeItemReference.Projection` are gone.
- Done: move final `Entity.ItemFits`, `TryFindSpace`, `TryEquip`, and
`TryUnequip` gear occupancy/mass deltas onto typed catalog rows for item
shape, hull shape/interior, hardpoint masks, cargo/docking category, and
@ -790,24 +792,23 @@ First Aetheria surfaces to publish:
typed runtime item rows; legacy `EquippableItemData.Durability` no longer
owns that stat-evaluation path.
- Done: move new equippable instance durability initialization onto typed
runtime item rows; legacy `EquippableItemData.Durability` remains only as
an incomplete-row fallback in the instantiation bridge.
runtime item rows.
- Done: move `EquippedItem` conductivity and max-durability performance/wear
inputs onto typed runtime item rows; legacy thermal resilience and audio
stats remain explicit DTO residues.
inputs onto typed runtime item rows.
- Done: move `EquippedItem` thermal performance evaluation onto typed
runtime item thermal bounds and curve keys; legacy `Data.Performance`
remains only as an incomplete-row fallback.
runtime item thermal bounds and curve keys; missing typed curve/range data
uses a neutral runtime default.
- Done: import typed thermal resilience and move equipped-item wear scaling
off `EquippableItemData.ThermalResilience`; it remains only as an
incomplete-row fallback.
off `EquippableItemData.ThermalResilience`; missing typed resilience uses
a neutral runtime default.
- Done: import typed audio parameter stat bindings and move equipped-item
Wwise parameter updates off `EquippableItemData.AudioStats`; it remains
only as an incomplete-row fallback.
Wwise parameter updates off `EquippableItemData.AudioStats`; missing
typed audio stat bindings produce no parameter updates.
- Done: add typed consumable stackability, duration, and effectiveness curve
fields and route action-bar consumable activation through typed item rows;
verifier records that the current mapped catalog has `0` consumable item
rows, so the old DTO path cannot be fully deleted yet.
fields and route action-bar consumable activation/runtime evaluation
through typed item rows; verifier records that the current mapped catalog
has `0` consumable item rows, so future consumable content still needs
typed coverage.
- Done: import action-bar icon resource paths into typed item definitions and
move `ActionBarSlot` custom gear icon lookup onto typed runtime item rows.
- Done: move `Ship` drag, combat/turret predicted shot height, and thruster
@ -946,10 +947,9 @@ First Aetheria surfaces to publish:
- Done: collapse the old generic `RuntimeCatalogLink<T>` abstraction into
`RuntimeItemReference`, an item-specific runtime projection reference owned
by `ItemManager`.
- Done: demote `RuntimeItemReference.Value` to
`RuntimeItemReference.Projection`, making the hydrated `ItemData` DTO a
cache/display/behavior bridge derived from `ItemId` instead of the
reference's apparent state value.
- Done: demote `RuntimeItemReference.Value` to an item-definition id, then
delete `RuntimeItemReference.Projection`; item references no longer carry
hydrated `ItemData` DTOs.
- Done: delete the dead `ItemManager.GetRuntimeItemProjection<T>` bridge
after loadout generation stopped hydrating selected typed rows into
`EquippableItemData` merely to instantiate equipment.
@ -1048,11 +1048,9 @@ First Aetheria surfaces to publish:
## Immediate Cut Line
Do not add behavior to `ItemData`, `RuntimeItemProjectionEntry`, or the
temporary `AetheriaRuntimeItemCatalog` DTO materializer. The typed state spine
and migration quarantine exist; the next cuts should replace surviving
projection consumers with typed item facets, typed behavior factories, and Eve
surfaces until the remaining `GetData` hydration users are gone and the old DTO
bridge can be deleted. Any predicate that still needs `ItemData` must earn that
Do not add behavior to `ItemData` or `RuntimeItemProjectionEntry`. The typed
state spine and migration quarantine exist; the next cuts should replace the
surviving behavior DTO projection bridge with typed behavior factories and Eve
surfaces. Any predicate that still needs legacy DTO objects must earn that
dependency by using behavior objects or simulation-only methods that typed
facets do not yet expose.