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-08-10 14:57:50 +01:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
using MessagePack;
|
|
|
|
|
|
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
|
|
|
[CultDocument("aetheria.playersettings", "1"), CultGlobal, MessagePackObject]
|
2026-08-10 14:57:50 +01:00
|
|
|
public class PlayerSettings
|
|
|
|
|
{
|
|
|
|
|
[Key(0)] public string Name = "Anonymous";
|
|
|
|
|
[Key(2)] public bool TutorialPassed;
|
|
|
|
|
[Key(3)] public Dictionary<string, string> HashedStoryFiles = new Dictionary<string, string>();
|
|
|
|
|
[Key(4)] public PlayerGameplaySettings GameplaySettings = new PlayerGameplaySettings();
|
|
|
|
|
[Key(5)] public PlayerInputSettings InputSettings = new PlayerInputSettings();
|
|
|
|
|
[Key(6)] public PlayerGraphicsSettings GraphicsSettings = new PlayerGraphicsSettings();
|
|
|
|
|
|
|
|
|
|
public string FormatTemperature(float t)
|
|
|
|
|
{
|
|
|
|
|
return GameplaySettings.TemperatureUnit switch
|
|
|
|
|
{
|
|
|
|
|
TemperatureUnit.Kelvin => $"{Format(t)}°K",
|
|
|
|
|
TemperatureUnit.Celsius => $"{Format(t - 273.15f)}°C",
|
|
|
|
|
TemperatureUnit.Fahrenheit => $"{Format(t * (9f / 5) - 459.67f)}°F",
|
|
|
|
|
_ => throw new ArgumentOutOfRangeException()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public float ParseTemperature(string s)
|
|
|
|
|
{
|
|
|
|
|
var t = float.Parse(s);
|
|
|
|
|
return GameplaySettings.TemperatureUnit switch
|
|
|
|
|
{
|
|
|
|
|
TemperatureUnit.Kelvin => t,
|
|
|
|
|
TemperatureUnit.Celsius => t + 273.15f,
|
|
|
|
|
TemperatureUnit.Fahrenheit => (t - 32) * (5f / 9) + 273.15f,
|
|
|
|
|
_ => throw new ArgumentOutOfRangeException()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public string Format(float d)
|
|
|
|
|
{
|
|
|
|
|
var magnitude = d == 0.0f ? 0 : (int)Math.Floor(Math.Log10(Math.Abs(d))) + 1;
|
|
|
|
|
var digits = GameplaySettings.SignificantDigits;
|
|
|
|
|
digits -= magnitude;
|
|
|
|
|
if (digits < 0)
|
|
|
|
|
digits = 0;
|
|
|
|
|
var strdec = d.ToString($"N{digits}");
|
|
|
|
|
var dec = Convert.ToChar(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator);
|
|
|
|
|
return strdec.Contains(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator) ? strdec.TrimEnd('0').TrimEnd(dec) : strdec;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[MessagePackObject]
|
|
|
|
|
public class PlayerGameplaySettings
|
|
|
|
|
{
|
|
|
|
|
[Key(0)] public TemperatureUnit TemperatureUnit = TemperatureUnit.Celsius;
|
|
|
|
|
[Key(1)] public int SignificantDigits = 3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[MessagePackObject]
|
|
|
|
|
public class PlayerGraphicsSettings
|
|
|
|
|
{
|
|
|
|
|
[Key(0)] public Quality NebulaQuality = Quality.Normal;
|
|
|
|
|
[Key(1)] public bool ShowAsteroidsInMinimap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[MessagePackObject]
|
|
|
|
|
public class PlayerInputSettings
|
|
|
|
|
{
|
Pin AetheriaStores routing with tests; make PlayerSettings readable
Cut 8b. tests/Aetheria.Shared.Tests (xunit, net10.0) builds a temp-directory
catalog through AetheriaStores.Open(catalogWritable: true) per test:
- OpenThenFlushLeavesEveryFileByteIdentical: the catalog, plus run and
player stores seeded with their globals, a SavedZone and an InputLayout.
- WeaponWrittenThroughGearHandleReloadsAsWeapon
- CatalogRefusesWrites: bytes unchanged too.
- SavedZoneNeverLandsInCatalog: no home without a run store; the snapshot
schemas show where it landed.
- FactionIsASingletonInstance: through a ref, a ref-keyed map key, and the
name index.
- MissingCatalogGlobalIsLoud: uses a test-only [CultGlobal] routed to the
catalog. The game has no catalog globals yet.
The first test found a real defect. PlayerInputSettings.InputActionMap was
keyed by (string, int), and CultLib's untrusted-data MessagePack security
has no hash-resistant comparer for a tuple, so a player store could never
be read back. It is now action name to binding index to path; its two
Unity readers change in the Unity commit.
Each test was checked to fail with the rule it pins removed; see the
report.
Builds: Aetheria.Shared, AetherDb, Aetheria.Shared.Tests (6/6 pass).
Unity does not build yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:12:54 +02:00
|
|
|
// Binding override paths by action name, then binding index. A tuple key has no hash-resistant comparer, so
|
|
|
|
|
// MessagePack's untrusted-data security refuses to read it back.
|
|
|
|
|
[Key(0)] public Dictionary<string, Dictionary<int, string>> InputActionMap = new Dictionary<string, Dictionary<int, string>>();
|
2026-08-10 14:57:50 +01:00
|
|
|
[Key(1)] public List<string> ActionBarInputs = new List<string>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public enum Quality
|
|
|
|
|
{
|
|
|
|
|
Low,
|
|
|
|
|
Normal,
|
|
|
|
|
High,
|
|
|
|
|
Ultra
|
|
|
|
|
}
|