Aetheria/Assets/Scripts/ServerShared/ItemManager.cs

234 lines
8.6 KiB
C#
Raw Normal View History

Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
/* 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;
using System.Collections.Generic;
using System.Linq;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
using GameCult.Caching;
using CultMath;
using static CultMath.math;
using float2 = CultMath.float2;
using Random = CultMath.Random;
using JM.LinqFaster;
using UniRx;
using float4 = CultMath.float4;
public class ItemManager
{
public Random Random = new Random((uint) (DateTime.Now.Ticks%uint.MaxValue));
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
private Action<string> _logger;
public CultCache ItemData { get; }
public ProvenanceLedger Lots { get; }
public GameplaySettings GameplaySettings { get; }
public ItemManager(CultCache itemData, ProvenanceLedger lots, GameplaySettings settings, Action<string> logger)
{
ItemData = itemData;
Lots = lots;
GameplaySettings = settings;
_logger = logger;
}
public void Log(string s)
{
_logger(s);
}
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
// The one resolution path from an item instance to its design
public ItemData GetData(ItemInstance item) => ItemData.Get(item.Data);
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
public SimpleCommodityData GetData(SimpleCommodity item) => GetData((ItemInstance) item) as SimpleCommodityData;
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
public CraftedItemData GetData(CraftedItemInstance item) => GetData((ItemInstance) item) as CraftedItemData;
public EquippableItemData GetData(EquippableItem item) => GetData((ItemInstance) item) as EquippableItemData;
public float GetMass(ItemInstance item)
{
return item switch
{
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
CraftedItemInstance _ => GetData(item).Mass,
SimpleCommodity commodity => GetData(item).Mass * commodity.Quantity,
_ => 0
};
}
public float GetThermalMass(ItemInstance item)
{
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
var data = GetData(item);
return item switch
{
CraftedItemInstance _ => data.Mass * data.SpecificHeat,
SimpleCommodity commodity => data.Mass * data.SpecificHeat * commodity.Quantity,
_ => 0
};
}
// The one resolution path from a crafted instance to the lot it was minted from.
public Lot GetLot(CraftedItemInstance item) => Lots[item.Lot];
Cut 0: collapse the three disagreeing stat evaluate bodies into one ItemManager.Evaluate (unequipped), ConsumableItemEffect.Evaluate and EquippedItem.Evaluate each hand-computed the same quality/durability/heat/ modifier arithmetic with small, undocumented disagreements (docs/stats-and- power-cut.md Sec 0.2). Collapse the arithmetic into one function, PerformanceStat.Evaluate(IStatContext), fed by three IStatContext implementations: EquippedItem (the only one with heat, live durability and modifiers), ConsumableItemEffect (progress substitutes for heat, unconditionally, matching its prior behaviour exactly) and a new UnequippedStatContext in ItemManager (no heat, no modifiers, by name). No behaviour change beyond the two disagreements the cut map names as defects. Each context's original NaN handling is preserved exactly (unequipped throws with diagnostic detail; equipped/consumable fall back to Min). Behavior.Evaluate(PerformanceStat) keeps its name and signature, so the ~81 existing call sites are untouched. Verified: dotnet test tests/Aetheria.Shared.Tests unchanged at 61/61 before new tests are added; a real-catalog probe over GameData/Aetheria.cc found the new unequipped path reproduces the old formula exactly across 3123 (item, quality, durability, stat) cases; Unity batchmode compiles with 0 error CS and the Cut 1/2/3 shield probes still pass; AetherDb census, factions, dangling, station-fit, loadout 1 and hardpoint-fit are byte-identical before and after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:19:22 +02:00
// Returns stat when not equipped: a null-entity context with no heat, no power and no modifiers. It says so
// in its own name (UnequippedStatContext) rather than being a third quiet dialect of the arithmetic.
public float Evaluate(PerformanceStat stat, EquippableItem item)
{
var data = GetData(item);
Cut 0: collapse the three disagreeing stat evaluate bodies into one ItemManager.Evaluate (unequipped), ConsumableItemEffect.Evaluate and EquippedItem.Evaluate each hand-computed the same quality/durability/heat/ modifier arithmetic with small, undocumented disagreements (docs/stats-and- power-cut.md Sec 0.2). Collapse the arithmetic into one function, PerformanceStat.Evaluate(IStatContext), fed by three IStatContext implementations: EquippedItem (the only one with heat, live durability and modifiers), ConsumableItemEffect (progress substitutes for heat, unconditionally, matching its prior behaviour exactly) and a new UnequippedStatContext in ItemManager (no heat, no modifiers, by name). No behaviour change beyond the two disagreements the cut map names as defects. Each context's original NaN handling is preserved exactly (unequipped throws with diagnostic detail; equipped/consumable fall back to Min). Behavior.Evaluate(PerformanceStat) keeps its name and signature, so the ~81 existing call sites are untouched. Verified: dotnet test tests/Aetheria.Shared.Tests unchanged at 61/61 before new tests are added; a real-catalog probe over GameData/Aetheria.cc found the new unequipped path reproduces the old formula exactly across 3123 (item, quality, durability, stat) cases; Unity batchmode compiles with 0 error CS and the Cut 1/2/3 shield probes still pass; AetherDb census, factions, dangling, station-fit, loadout 1 and hardpoint-fit are byte-identical before and after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:19:22 +02:00
var result = stat.Evaluate(new UnequippedStatContext(this, item, data));
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
if (float.IsNaN(result))
throw new InvalidOperationException($"Performance Stat on {data.Name} evaluating as NaN: input data is invalid! Durability: {item.Durability} / {data.Durability}");
return result;
Cut 0: collapse the three disagreeing stat evaluate bodies into one ItemManager.Evaluate (unequipped), ConsumableItemEffect.Evaluate and EquippedItem.Evaluate each hand-computed the same quality/durability/heat/ modifier arithmetic with small, undocumented disagreements (docs/stats-and- power-cut.md Sec 0.2). Collapse the arithmetic into one function, PerformanceStat.Evaluate(IStatContext), fed by three IStatContext implementations: EquippedItem (the only one with heat, live durability and modifiers), ConsumableItemEffect (progress substitutes for heat, unconditionally, matching its prior behaviour exactly) and a new UnequippedStatContext in ItemManager (no heat, no modifiers, by name). No behaviour change beyond the two disagreements the cut map names as defects. Each context's original NaN handling is preserved exactly (unequipped throws with diagnostic detail; equipped/consumable fall back to Min). Behavior.Evaluate(PerformanceStat) keeps its name and signature, so the ~81 existing call sites are untouched. Verified: dotnet test tests/Aetheria.Shared.Tests unchanged at 61/61 before new tests are added; a real-catalog probe over GameData/Aetheria.cc found the new unequipped path reproduces the old formula exactly across 3123 (item, quality, durability, stat) cases; Unity batchmode compiles with 0 error CS and the Cut 1/2/3 shield probes still pass; AetherDb census, factions, dangling, station-fit, loadout 1 and hardpoint-fit are byte-identical before and after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:19:22 +02:00
}
// The unequipped case: quality and durability read live off the item and its lot; heat and modifiers do not
// exist without an entity, so they resolve as the identity for their term rather than a bespoke formula.
private sealed class UnequippedStatContext : IStatContext
{
private readonly EquippableItem _item;
private readonly EquippableItemData _data;
private readonly float _durabilityExponent;
public Lot Lot { get; }
Cut 0: collapse the three disagreeing stat evaluate bodies into one ItemManager.Evaluate (unequipped), ConsumableItemEffect.Evaluate and EquippedItem.Evaluate each hand-computed the same quality/durability/heat/ modifier arithmetic with small, undocumented disagreements (docs/stats-and- power-cut.md Sec 0.2). Collapse the arithmetic into one function, PerformanceStat.Evaluate(IStatContext), fed by three IStatContext implementations: EquippedItem (the only one with heat, live durability and modifiers), ConsumableItemEffect (progress substitutes for heat, unconditionally, matching its prior behaviour exactly) and a new UnequippedStatContext in ItemManager (no heat, no modifiers, by name). No behaviour change beyond the two disagreements the cut map names as defects. Each context's original NaN handling is preserved exactly (unequipped throws with diagnostic detail; equipped/consumable fall back to Min). Behavior.Evaluate(PerformanceStat) keeps its name and signature, so the ~81 existing call sites are untouched. Verified: dotnet test tests/Aetheria.Shared.Tests unchanged at 61/61 before new tests are added; a real-catalog probe over GameData/Aetheria.cc found the new unequipped path reproduces the old formula exactly across 3123 (item, quality, durability, stat) cases; Unity batchmode compiles with 0 error CS and the Cut 1/2/3 shield probes still pass; AetherDb census, factions, dangling, station-fit, loadout 1 and hardpoint-fit are byte-identical before and after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:19:22 +02:00
public UnequippedStatContext(ItemManager items, EquippableItem item, EquippableItemData data)
{
_item = item;
_data = data;
Lot = items.GetLot(item);
_durabilityExponent = lerp(
items.GameplaySettings.DurabilityQualityMin,
items.GameplaySettings.DurabilityQualityMax,
pow(Lot.Quality, items.GameplaySettings.DurabilityQualityExponent));
}
Cut 1: terms replace the four fixed exponents; R-heat PerformanceStat's four fixed fields (HeatExponentMultiplier, DurabilityExponentMultiplier, QualityExponent, FromRole) tombstone into a declared List<StatTerm> (source + exponent + role), walked by the one evaluation path Cut 0 unified. A stat with no terms resolves to Max (the same identity a stat with all-zero exponents already produced), so the migration is exact: a zero multiplier becomes no term, not a term with exponent 0. R-heat (operator, docs/stats-and-power-cut.md): EquippableItemData's HeatPerformanceCurve (a BezierCurve, plus the 100-sample scan that inferred OptimalTemperature and its cache) is retired in favor of an authored minimum, maximum, optimum and plateau width, with linear falloff outside the plateau. StatValidation refuses an optimum outside its bounds or a negative plateau width, loudly, at catalog load. The plateau's consequence rides along for free: Wear's thermal term is pow(ThermalPerformance, ...), and Performance() now returns exactly 1 across the plateau, so a well-managed item takes wear only from deltaTemp there -- the operational-lifespan lever the operator asked for. ConsumableItemEffect's progress-through-duration substitution for "heat" becomes its own StatSource (ConsumableProgress) rather than an unconditional override applied regardless of what a stat declares. Tests: LoadoutTests' four PerformanceStat-shaped fixtures rebuilt against Terms/authored heat fields; HeatResponseTests.cs (new) pins load validation, the plateau shape, and the three wear-lever claims (held inside takes no thermal wear, held outside does, a fast swing across still wears) each under its own mutation in tests/mutation_tests_stats_power_cut1.py (new, 10 rules including a no-op control; all pass). 64 pre-Cut-1 tests still pass unmodified; 71 total with the 7 new ones. Unity batchmode: 0 error CS. ShieldPanelCut1Verify and Cut2Verify pass; Cut3Verify fails with an unrelated pre-existing "Kernel 'KInit' not found" compute-shader error, reproduced against Cut 0's own landing state with no shader/compute file in this diff -- flagged for Soul, not fixed here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 17:18:06 +02:00
public float HeatFactor(float exponent) => 1f;
public float DurabilityFactor(float exponent) =>
pow(_item.Durability / _data.Durability, _durabilityExponent * exponent);
public float ConsumableProgressFactor(float exponent) => 1f;
public float PowerSupplyFactor(float exponent) => 1f;
Cut 0: collapse the three disagreeing stat evaluate bodies into one ItemManager.Evaluate (unequipped), ConsumableItemEffect.Evaluate and EquippedItem.Evaluate each hand-computed the same quality/durability/heat/ modifier arithmetic with small, undocumented disagreements (docs/stats-and- power-cut.md Sec 0.2). Collapse the arithmetic into one function, PerformanceStat.Evaluate(IStatContext), fed by three IStatContext implementations: EquippedItem (the only one with heat, live durability and modifiers), ConsumableItemEffect (progress substitutes for heat, unconditionally, matching its prior behaviour exactly) and a new UnequippedStatContext in ItemManager (no heat, no modifiers, by name). No behaviour change beyond the two disagreements the cut map names as defects. Each context's original NaN handling is preserved exactly (unequipped throws with diagnostic detail; equipped/consumable fall back to Min). Behavior.Evaluate(PerformanceStat) keeps its name and signature, so the ~81 existing call sites are untouched. Verified: dotnet test tests/Aetheria.Shared.Tests unchanged at 61/61 before new tests are added; a real-catalog probe over GameData/Aetheria.cc found the new unequipped path reproduces the old formula exactly across 3123 (item, quality, durability, stat) cases; Unity batchmode compiles with 0 error CS and the Cut 1/2/3 shield probes still pass; AetherDb census, factions, dangling, station-fit, loadout 1 and hardpoint-fit are byte-identical before and after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:19:22 +02:00
public float ScaleModifier(PerformanceStat stat) => 1f;
public float ConstantModifier(PerformanceStat stat) => 0f;
}
public int GetPrice(CraftedItemInstance item)
{
var data = GetData(item);
return (int) (GameplaySettings.QualityPriceModifier.Evaluate(GetLot(item).Quality) * data.Price);
}
// The tier roll every mint applies, whether or not the lot fills roles.
public float RollQuality()
{
var quality = Random.NextFloat();
var tier = GameplaySettings.Tiers[0];
foreach (var t in GameplaySettings.Tiers)
2026-06-14 07:41:32 +01:00
{
if (t.Rarity > quality)
tier = t;
}
return tier.Quality;
}
// Builds one of a manufacturer's branded products: the unit's own workmanship rolled as before, and each of
// the design's roles filled with a part whose quality is drawn from that manufacturer's distribution for it.
public int CreateLot(FactionProductData product)
{
var design = ItemData.Get(product.Design);
var lot = new Lot
{
Design = ItemData.RefOf<ItemData>(design),
Origin = new Attributed { Faction = product.Manufacturer },
Quality = RollQuality(),
Roles = new List<RoleFill>()
};
if (design.Roles != null)
foreach (var role in design.Roles)
{
var build = product.Roles?.FirstOrDefault(b => b.Role == role.Name) ?? new ProductRole();
lot.Roles.Add(new RoleFill
{
Role = role.Name,
Quality = clamp(Random.NextGaussian(build.Mean, build.StandardDeviation), .01f, 1)
});
}
return Lots.Add(lot);
2026-06-14 07:41:32 +01:00
}
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
// No branded product: a lot attributed to maker (which may be unset) with no role fills, e.g. console `give`.
public int CreateLot(CraftedItemData design, CultRecordRef<Faction> maker, float quality)
{
var lot = new Lot
{
Design = ItemData.RefOf<ItemData>(design),
Origin = new Attributed { Faction = maker },
Quality = quality
};
return Lots.Add(lot);
}
// The one writer of a crafted instance's Data: copied from the lot's design, never independently set.
public CraftedItemInstance CreateInstance(int lot)
{
var l = Lots[lot];
var design = ItemData.Get(l.Design) as CraftedItemData;
if (design is EquippableItemData equippableItemData)
{
return new EquippableItem
{
Data = l.Design, Lot = lot, Durability = equippableItemData.Durability
};
}
return new CompoundCommodity
{
Data = l.Design, Lot = lot
};
2026-06-14 07:41:32 +01:00
}
public CraftedItemInstance CreateInstance(FactionProductData product)
{
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
var design = ItemData.Get(product.Design);
if (design == null)
{
_logger($"Product {product.Name} names a design that does not exist!");
return null;
}
return CreateInstance(CreateLot(product));
}
// Who made an item and what it is branded as, derived from its lot's provenance. Null maker (an unset faction,
// or an Extracted lot with none) means no brand at all; a set maker with no matching product still shows the
// maker, with no product name or flavour text.
public (Faction Maker, FactionProductData Product) Brand(CraftedItemInstance item)
{
var lot = GetLot(item);
var maker = lot.Origin switch
{
Attributed attributed => attributed.Faction,
Produced produced => produced.Faction,
_ => default
};
if (!maker.IsSet()) return (null, null);
var product = ItemData.GetAll<FactionProductData>()
.Where(p => p.Manufacturer.Key.Equals(maker.Key) && p.Design.Key.Equals(item.Data.Key))
.OrderBy(p => ItemData.RefOf(p).Key.Value, StringComparer.Ordinal)
.FirstOrDefault();
return (ItemData.Get(maker), product);
}
public (RarityTier tier, int upgrades) GetTier(CraftedItemInstance item)
{
var quality = GetLot(item).Quality;
var tier = GameplaySettings.Tiers[0];
foreach (var t in GameplaySettings.Tiers)
if (quality + .001f > t.Quality)
tier = t;
int upgrades = (int) ((quality - tier.Quality) / .0499f);
return (tier, upgrades);
}
Move the ServerShared data model onto CultLib's CultCache Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also checks the Caching project exists. Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject] and integer [Key]s on the Q7 concrete types (the eight ItemData leaves, Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout). SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName]. ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose [MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout moves into ServerShared so AetheriaStores can route it. References: Guid and DatabaseLink<T> members become CultRecordRef<T>; Guid-keyed maps become ref-keyed with [CultReference(..., many: true)]; ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits, bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey. Identity is the record key: ID comparisons become reference or key comparisons. StatModifierData.RequireBehavior is a simple type name. AetheriaStores.Open composes the catalog (read-only unless writable), run and player stores, one home per type. CultRecordRefs carries IsSet, a null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert. ItemManager.GetData is the one item resolution path. MathResolver is declared by [assembly: CultCacheFormatterResolver]. Inspector attributes: text, range and asset path map onto CultLib's CultInspector attributes (asset path only on string members); every replaced Aetheria attribute keeps [Inspectable] for the runtime panel. Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project, which follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
}