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
2020-12-22 03:13:43 +00:00
* 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 ;
2026-08-10 14:57:50 +01:00
using System.Collections ;
2020-03-22 19:05:26 +00:00
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 ;
2026-09-14 17:39:12 +02:00
using CultMath ;
using static CultMath . math ;
using float2 = CultMath . float2 ;
using Random = CultMath . Random ;
2020-05-10 00:16:33 +01:00
using JM.LinqFaster ;
2020-12-01 00:16:11 +00:00
using UniRx ;
2026-09-14 17:39:12 +02:00
using float4 = CultMath . float4 ;
2020-03-22 19:05:26 +00:00
2020-12-10 23:20:38 +00:00
public class ItemManager
2020-03-22 19:05:26 +00:00
{
2020-04-30 02:54:31 +01:00
public Random Random = new Random ( ( uint ) ( DateTime . Now . Ticks % uint . MaxValue ) ) ;
2026-06-14 10:54:17 +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
private Action < string > _logger ;
2026-06-15 18:51:59 +01:00
2026-08-10 14:57:50 +01:00
public CultCache ItemData { get ; }
2026-09-17 04:16:23 +02:00
public ProvenanceLedger Lots { get ; }
2026-08-10 14:57:50 +01:00
public GameplaySettings GameplaySettings { get ; }
2026-06-14 05:15:18 +01:00
2026-09-17 04:16:23 +02:00
public ItemManager ( CultCache itemData , ProvenanceLedger lots , GameplaySettings settings , Action < string > logger )
2026-06-14 22:58:02 +01:00
{
2026-08-10 14:57:50 +01:00
ItemData = itemData ;
2026-09-17 04:16:23 +02:00
Lots = lots ;
2026-08-10 14:57:50 +01:00
GameplaySettings = settings ;
_logger = logger ;
2026-06-14 07:27:23 +01:00
}
2020-04-16 03:08:18 +01:00
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 ) ;
2026-08-10 14:57:50 +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
public SimpleCommodityData GetData ( SimpleCommodity item ) = > GetData ( ( ItemInstance ) item ) as SimpleCommodityData ;
2026-08-10 14:57:50 +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
public CraftedItemData GetData ( CraftedItemInstance item ) = > GetData ( ( ItemInstance ) item ) as CraftedItemData ;
public EquippableItemData GetData ( EquippableItem item ) = > GetData ( ( ItemInstance ) item ) as EquippableItemData ;
2026-08-10 14:57:50 +01:00
2020-03-22 19:05:26 +00:00
public float GetMass ( ItemInstance item )
{
2021-04-18 05:13:20 +01:00
return item switch
2020-03-22 19:05:26 +00: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
CraftedItemInstance _ = > GetData ( item ) . Mass ,
SimpleCommodity commodity = > GetData ( item ) . Mass * commodity . Quantity ,
2021-04-18 05:13:20 +01:00
_ = > 0
} ;
2020-03-22 19:05:26 +00:00
}
2020-06-05 04:16:33 +01:00
public float GetThermalMass ( ItemInstance item )
2020-03-22 19:05:26 +00: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
var data = GetData ( item ) ;
2021-04-18 05:13:20 +01:00
return item switch
2020-03-22 19:05:26 +00:00
{
2026-08-10 14:57:50 +01:00
CraftedItemInstance _ = > data . Mass * data . SpecificHeat ,
SimpleCommodity commodity = > data . Mass * data . SpecificHeat * commodity . Quantity ,
2021-04-18 05:13:20 +01:00
_ = > 0
} ;
2020-03-22 19:05:26 +00:00
}
2026-09-17 04:16:23 +02:00
// 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.
2020-12-10 23:20:38 +00:00
public float Evaluate ( PerformanceStat stat , EquippableItem item )
2020-03-22 19:05:26 +00:00
{
2026-08-10 14:57:50 +01:00
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 ) )
2026-08-10 14:57:50 +01:00
throw new InvalidOperationException ( $"Performance Stat on {data.Name} evaluating as NaN: input data is invalid! Durability: {item.Durability} / {data.Durability}" ) ;
2020-03-22 19:05:26 +00:00
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 ; }
2021-03-22 05:22:31 +00:00
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 ;
2020-03-22 19:05:26 +00:00
}
2020-05-25 03:05:30 +01:00
2021-03-20 04:20:17 +00:00
public int GetPrice ( CraftedItemInstance item )
{
2026-08-10 14:57:50 +01:00
var data = GetData ( item ) ;
2026-09-17 04:16:23 +02:00
return ( int ) ( GameplaySettings . QualityPriceModifier . Evaluate ( GetLot ( item ) . Quality ) * data . Price ) ;
2026-06-14 05:56:41 +01:00
}
2026-09-17 04:16:23 +02:00
// The tier roll every mint applies, whether or not the lot fills roles.
public float RollQuality ( )
2021-03-27 10:03:24 +00:00
{
2026-09-17 04:16:23 +02:00
var quality = Random . NextFloat ( ) ;
var tier = GameplaySettings . Tiers [ 0 ] ;
foreach ( var t in GameplaySettings . Tiers )
2026-06-14 07:41:32 +01:00
{
2026-09-17 04:16:23 +02:00
if ( t . Rarity > quality )
tier = t ;
2021-03-27 10:03:24 +00:00
}
2026-09-17 04:16:23 +02:00
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
2021-03-27 10:03:24 +00:00
{
2026-09-17 04:16:23 +02:00
Design = ItemData . RefOf < ItemData > ( design ) ,
Origin = new Attributed { Faction = product . Manufacturer } ,
Quality = RollQuality ( ) ,
Roles = new List < RoleFill > ( )
2021-03-27 10:03:24 +00:00
} ;
2026-09-17 04:16:23 +02:00
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
2026-09-17 04:16:23 +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 )
2026-06-14 07:27:23 +01:00
{
2026-09-17 04:16:23 +02:00
var lot = new Lot
2026-06-14 07:27:23 +01:00
{
2026-09-17 04:16:23 +02:00
Design = ItemData . RefOf < ItemData > ( design ) ,
Origin = new Attributed { Faction = maker } ,
Quality = quality
} ;
return Lots . Add ( lot ) ;
}
2026-06-14 07:27:23 +01:00
2026-09-17 04:16:23 +02:00
// 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 )
2021-03-08 02:57:38 +00:00
{
2026-09-17 04:16:23 +02:00
return new EquippableItem
{
Data = l . Design , Lot = lot , Durability = equippableItemData . Durability
} ;
2021-03-08 02:57:38 +00:00
}
2020-03-22 19:05:26 +00:00
2026-09-17 04:16:23 +02:00
return new CompoundCommodity
{
Data = l . Design , Lot = lot
} ;
2026-06-14 07:41:32 +01:00
}
2026-09-12 01:12:12 +02: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 ) ;
2026-09-12 01:12:12 +02:00
if ( design = = null )
{
_logger ( $"Product {product.Name} names a design that does not exist!" ) ;
return null ;
}
2026-09-17 04:16:23 +02:00
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
2026-09-12 01:12:12 +02:00
{
2026-09-17 04:16:23 +02:00
Attributed attributed = > attributed . Faction ,
Produced produced = > produced . Faction ,
_ = > default
} ;
if ( ! maker . IsSet ( ) ) return ( null , null ) ;
2026-09-12 01:12:12 +02:00
2026-09-17 04:16:23 +02:00
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 ) ;
2026-09-12 01:12:12 +02:00
}
2021-03-08 02:57:38 +00:00
public ( RarityTier tier , int upgrades ) GetTier ( CraftedItemInstance item )
{
2026-09-17 04:16:23 +02:00
var quality = GetLot ( item ) . Quality ;
2021-03-08 02:57:38 +00:00
var tier = GameplaySettings . Tiers [ 0 ] ;
foreach ( var t in GameplaySettings . Tiers )
2026-09-17 04:16:23 +02:00
if ( quality + . 001f > t . Quality )
2021-03-08 02:57:38 +00:00
tier = t ;
2026-09-17 04:16:23 +02:00
int upgrades = ( int ) ( ( quality - tier . Quality ) / . 0499f ) ;
2021-03-08 02:57:38 +00:00
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
}