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>
63 lines
3.7 KiB
C#
63 lines
3.7 KiB
C#
using System;
|
|
using System.Linq;
|
|
using GameCult.Caching;
|
|
using GameCult.Caching.MessagePack;
|
|
|
|
// Aetheria's stores: one home store per type, composed here and nowhere else.
|
|
public static class AetheriaStores
|
|
{
|
|
public static readonly Type[] CatalogTypes = { typeof(ItemData), typeof(Faction), typeof(FactionProductData), typeof(PersonalityAttribute), typeof(NameFile), typeof(InputLayout), typeof(Loadout) };
|
|
public static readonly Type[] RunTypes = { typeof(OrbitData), typeof(BodyData), typeof(SavedZone), typeof(SavedGame), typeof(ProvenanceLedger) };
|
|
public static readonly Type[] PlayerTypes = { typeof(PlayerSettings) };
|
|
|
|
// Attaches (hydrates) the catalog read-only unless catalogWritable, then the run and player stores when given.
|
|
// Throws when a [CultGlobal] type routed to the catalog has no record. The one exemption is a writable catalog with
|
|
// no records at all, a seed store being authored from nothing; a populated catalog is held to it however it opens.
|
|
public static CultCache Open(string catalogPath, string runPath = null, string playerPath = null, bool catalogWritable = false)
|
|
{
|
|
var cache = new CultCache();
|
|
try
|
|
{
|
|
cache.AddBackingStore(new SingleFileMessagePackBackingStore(catalogPath, !catalogWritable), CatalogTypes);
|
|
if (runPath != null) cache.AddBackingStore(new SingleFileMessagePackBackingStore(runPath), RunTypes);
|
|
if (playerPath != null) cache.AddBackingStore(new SingleFileMessagePackBackingStore(playerPath), PlayerTypes);
|
|
var seeding = catalogWritable && !cache.AllStoredDocuments.Any(stored =>
|
|
CatalogTypes.Any(home => home.IsAssignableFrom(stored.Descriptor.DocumentType)));
|
|
var missing = seeding ? null : cache.Registry.AllDescriptors.FirstOrDefault(descriptor =>
|
|
descriptor.IsGlobal &&
|
|
CatalogTypes.Any(home => home.IsAssignableFrom(descriptor.DocumentType)) &&
|
|
cache.AllStoredDocuments.All(stored => stored.Descriptor != descriptor));
|
|
if (missing != null)
|
|
throw new InvalidOperationException($"Catalog {catalogPath} has no {missing.SchemaName} record; catalog globals are authored, never invented.");
|
|
// R-heat (docs/stats-and-power-cut.md): every equippable design's heat response must describe a
|
|
// coherent range before anything reads it. Fails loudly, naming the item.
|
|
foreach (var data in cache.GetAll<EquippableItemData>())
|
|
StatValidation.ValidateHeatResponse(data);
|
|
return cache;
|
|
}
|
|
catch
|
|
{
|
|
cache.Dispose();
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
public static class CultRecordRefs
|
|
{
|
|
public static bool IsSet(this CultRecordKey key) => !string.IsNullOrEmpty(key.Value);
|
|
public static bool IsSet<T>(this CultRecordRef<T> reference) => reference.Key.IsSet();
|
|
|
|
// The document a reference names, or null when it is unset or names nothing this cache holds.
|
|
public static T Get<T>(this CultCache cache, CultRecordRef<T> reference) where T : class =>
|
|
reference.IsSet() ? cache.Get<T>(reference.Key) : null;
|
|
|
|
// A reference to a document this cache holds. The key comes from the cache, never from the document.
|
|
public static CultRecordRef<T> RefOf<T>(this CultCache cache, T document) where T : class =>
|
|
new CultRecordRef<T>(cache.TryGetHandle(document)?.Key ??
|
|
throw new InvalidOperationException($"This cache holds no such {document.GetType().Name}."));
|
|
|
|
// Writes a document to its home store; the cache mints its key the first time.
|
|
public static CultRecordRef<T> Upsert<T>(this CultCache cache, T document) where T : class =>
|
|
new CultRecordRef<T>(cache.UpsertAsync(document).Result.Key);
|
|
}
|