From 75d0392d43543c2cbc3b9b845ce14ba5686840ae Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 13 Jun 2026 22:26:43 +0100 Subject: [PATCH] Rehydrate player settings from typed state --- .voidbot/state/aetheria.cc | Bin 49844 -> 50394 bytes Aetheria.State.Unity.Smoke/Program.cs | 48 +++-- Aetheria.State/README.md | 9 +- Assets/Scripts/Gameplay/ActionGameManager.cs | 66 ++++++- .../Runtime/AetheriaRuntimeCatalogSnapshot.cs | 61 +++++++ .../Runtime/AetheriaRuntimeCatalogStore.cs | 166 +++++++++++++++++- docs/aetheria-perfect-machine-map.md | 2 + 7 files changed, 327 insertions(+), 25 deletions(-) diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index be4a5113baed335c552e41bd513e7009aa597aab..32a5e1888f467753b8ed910330bed60d36fc8af9 100644 GIT binary patch delta 1573 zcmZux&5ImG6lZr<;}QudvPp=BhaLpk>2;jl&9DT5M38kALq7b-Mc!1`%+zjoRa;fP zlU8I7!NXn_qz-}-f+QLUE2tCViiqUmC8wMuOa6g?2k*J$RdvtIMm#lCRloOsU+>nJ zwY{y{PuHgpHdfwPT4}bT=HimObNfYiclz?|_{{xVJK|LB(&W|BBE6b(&&<3uee3G* z)tT+D=h6#O*aD>LgXX;=)@Zo_ckhR}nT}SeTVFr^TAO#2=Fq_;5wZ(yWE)@=#Jq!f zVomftOHibHQ)}nlgKv)xV#V^5OAEq;x(K$f>U}K0SgH;t6OyVa4Wai67;Y^UHVZ;# zxrGa>@M+F-8Y5yX%D9YuSEkZbSThf{#{sh}5nMxG3qNi*QEg0Ckkg%R3XHM@3$8yI z6^=F=^MEpjH<=YmzKtn=4<&Y~LR%?Y;vIOrp?XFD>9Vf+lE-kGFrG$6T{Xv;>j>Ve zfCE;n1V)rh3h4K^B+y`sjD8*Ur$3p}G4M;m5MMVemxkupn>w(s_@I0f3wmso7KBSk zmgF%pM%`0f$h5M`SFY+A=nLBmBO8KLJp&N~nvy1Fb4*|)NK_II6+Y;QWCY4UI>?Rn zN0wMA2?ayMC)_GIZrCn>p;S^Zs#~Gj;Ke*)wh$O7X9Y!vnk`$1V_?|szCnz^y z0wlvCQ5c)(XB-4=R#Q@zp_l3YKOcHFF%J5t-h!oBeWgIT$#n-ACsQ!g+P4Ta(?HZ# ztGwHTGh7=YoyTX(JMRL+6QUTryn2G{WC8w1Qji*XlZ|NeJ}wDa7!wk~8s`~wIh7z! z$L`ef;g+WzX<*MfDMQ$z8k{axIFFv^d%IKE{iOS~7?yKz=UJm2d%M$fb@aiM0M2YIU zJD(q0K2ci8FfB6cmrCYC;J;*anlmJd0$kl_;94u~qQ+78*Ul@?kC$tsdGWf%uDRkK z?Q9?ah0&5=eo2fw-pu`Ct^55l-1_n0>|_f!n(n#X!~6F1z1<`3@7sS&UYuACds@|= hcEA6cPR?7ehx66l!M?Mw(}xdt|D1hxJYB1G@iDD1O4k4Y delta 1316 zcmZvc&ubJ(6vr`>^#`sBf&>r316gv(v=ea><4ybl?urRU51W(K-7nLXbXT>ns%LC3 za}beS1nI*rte~K>i--|t54$dlAb1ro9`xqHn}UCUg0H&M$)H~P@T&T~Z+*Vssy8c* zzppkP-5vR{b)-FWWOB-D9cnGy*tDm3KC&`Yn<{I%acQI0o-AuyNB509&IjY8*MA%T z<&;V@3xtVCQ>i)-lIR==NvEK64@@Ko4cMZ2b8kP;Ng^$z!baY`AQm!tv+3GVv=NdN z<7TMI`BNbIuLwaZi^_t@Xi)?>0cSI1WrB0D$dTsJTApb(Ha8PYH06+zUc$gittOzJ zP~=Oy`clEMEVjo);F?!6!i~vh3e#*j6i(E6aBr_HQ5y$00OHv`knk!I~ zBFVsH7r|)A3{r*2z{LzRtbP*-f)@l{iq&*C0I6weG_I99kuXkipNTYmLA5sTPsVGw z`1->+2GT($AS{OK&?DqGLj~UofL0+IPE3_?(BDFyl~*vcoP_hALe)t9@l%EEz0iHUdo`n36DB zfz=?w5ES+*Z7I?gBiT(A6qC>P^k1FKY(_3)F2X%c%bZu8#UU&srbBV;k+kfij}co8 zTCufjQC>I5A?8>p&O{vNY-5kzssikquiLQ}WL}~ropmBO(ZvHUksSG6BsqmCUl^pW zOVYO~)QX!kBG;L!tKjHZ(lPiVSmqFT_k{`~O=oe@+`Mv!aPjr&;==4t#rwIj;^XZd zgLk(+ZSJeN>V|y(KCu8PVtLse`m0p4EiO%^g}-+GP_!n;iWd*}@2(!Bwl{Nldf~1W9rH)gSiL@2UERKI`_NkP xvN5*tAWc;d(&vBvAF8$29 SampleNames { get; } public IReadOnlyList Names { get; } } + + public sealed class AetheriaRuntimePlayerSettingsSnapshot + { + public AetheriaRuntimePlayerSettingsSnapshot( + string playerName, + bool tutorialPassed, + IReadOnlyList storyFileHashes, + string temperatureUnit, + int significantDigits, + string nebulaQuality, + bool showAsteroidsInMinimap, + IReadOnlyList bindingOverrides, + IReadOnlyList actionBarInputs) + { + PlayerName = playerName; + TutorialPassed = tutorialPassed; + StoryFileHashes = storyFileHashes; + TemperatureUnit = temperatureUnit; + SignificantDigits = significantDigits; + NebulaQuality = nebulaQuality; + ShowAsteroidsInMinimap = showAsteroidsInMinimap; + BindingOverrides = bindingOverrides; + ActionBarInputs = actionBarInputs; + } + + public string PlayerName { get; } + public bool TutorialPassed { get; } + public IReadOnlyList StoryFileHashes { get; } + public string TemperatureUnit { get; } + public int SignificantDigits { get; } + public string NebulaQuality { get; } + public bool ShowAsteroidsInMinimap { get; } + public IReadOnlyList BindingOverrides { get; } + public IReadOnlyList ActionBarInputs { get; } + } + + public sealed class AetheriaRuntimeStoryFileHash + { + public AetheriaRuntimeStoryFileHash(string storyPath, string hash) + { + StoryPath = storyPath; + Hash = hash; + } + + public string StoryPath { get; } + public string Hash { get; } + } + + public sealed class AetheriaRuntimeInputBindingOverride + { + public AetheriaRuntimeInputBindingOverride(string actionName, int bindingIndex, string bindingPath) + { + ActionName = actionName; + BindingIndex = bindingIndex; + BindingPath = bindingPath; + } + + public string ActionName { get; } + public int BindingIndex { get; } + public string BindingPath { get; } + } } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs index dad1e6bea..eff3fe35f 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs @@ -16,6 +16,8 @@ namespace GameCult.Aetheria.State.Unity private const string CorporationSchema = "aetheria.corporation"; private const string NameFileSchema = "aetheria.name_file"; private const string EveSurfaceSchema = "gamecult.eve.surface"; + private const string PlayerSettingsSchema = "aetheria.player_settings"; + private const string PlayerSettingsKey = "global:aetheria.player_settings.v1"; public static AetheriaRuntimeCatalogSnapshot OpenReadOnly(string stateFilePath) { @@ -64,6 +66,26 @@ namespace GameCult.Aetheria.State.Unity return surfaces; } + public static AetheriaRuntimePlayerSettingsSnapshot? ReadPlayerSettings(string stateFilePath) + { + if (string.IsNullOrWhiteSpace(stateFilePath)) + throw new ArgumentException("State file path must be non-empty.", nameof(stateFilePath)); + + var catalog = ReadSchemaCatalog(stateFilePath); + AetheriaRuntimePlayerSettingsSnapshot? settings = null; + foreach (var record in ReadRecords(stateFilePath)) + { + if (record.Key != PlayerSettingsKey || + !catalog.TryGetValue(record.SchemaId, out var schemaName) || + schemaName != PlayerSettingsSchema) + continue; + + settings = ReadPlayerSettingsPayload(record.Payload); + } + + return settings; + } + private static Dictionary ReadSchemaCatalog(string stateFilePath) { if (!File.Exists(stateFilePath)) @@ -122,7 +144,7 @@ namespace GameCult.Aetheria.State.Unity private static PersistedRecord ReadPersistedRecord(ref MessagePackReader reader) { var fieldCount = reader.ReadArrayHeader(); - if (fieldCount > 0) reader.Skip(); + var key = fieldCount > 0 ? ReadString(ref reader) : ""; var schemaId = fieldCount > 1 ? ReadString(ref reader) : ""; if (fieldCount > 2) reader.Skip(); var payload = Array.Empty(); @@ -134,7 +156,7 @@ namespace GameCult.Aetheria.State.Unity } for (var field = 4; field < fieldCount; field++) reader.Skip(); - return new PersistedRecord(schemaId, payload); + return new PersistedRecord(key, schemaId, payload); } private static AetheriaRuntimeCatalogItem ReadItem(byte[] payload) @@ -263,6 +285,30 @@ namespace GameCult.Aetheria.State.Unity return new EveSurfaceDocument(type, schema, providerId, providerKind, title, version, updatedAtUtc, surface, commands); } + private static AetheriaRuntimePlayerSettingsSnapshot ReadPlayerSettingsPayload(byte[] payload) + { + var reader = new MessagePackReader(payload); + var fields = reader.ReadArrayHeader(); + SkipFields(ref reader, fields, 0, 3); + var playerName = ReadFieldString(ref reader, fields, 3); + var tutorialPassed = ReadFieldBool(ref reader, fields, 4); + var storyFileHashes = ReadFieldStoryFileHashes(ref reader, fields, 5); + var gameplay = ReadFieldPlayerGameplaySettings(ref reader, fields, 6); + var graphics = ReadFieldPlayerGraphicsSettings(ref reader, fields, 7); + var input = ReadFieldPlayerInputSettings(ref reader, fields, 8); + SkipRemaining(ref reader, fields, 9); + return new AetheriaRuntimePlayerSettingsSnapshot( + playerName, + tutorialPassed, + storyFileHashes, + gameplay.TemperatureUnit, + gameplay.SignificantDigits, + graphics.NebulaQuality, + graphics.ShowAsteroidsInMinimap, + input.BindingOverrides, + input.ActionBarInputs); + } + private static string ReadFieldString(ref MessagePackReader reader, int fields, int index) { return index >= fields ? "" : ReadString(ref reader); @@ -273,6 +319,11 @@ namespace GameCult.Aetheria.State.Unity return index >= fields ? 0 : reader.ReadInt32(); } + private static bool ReadFieldBool(ref MessagePackReader reader, int fields, int index) + { + return index < fields && reader.ReadBoolean(); + } + private static long ReadFieldInt64(ref MessagePackReader reader, int fields, int index) { return index >= fields ? 0 : reader.ReadInt64(); @@ -293,6 +344,71 @@ namespace GameCult.Aetheria.State.Unity return values; } + private static IReadOnlyList ReadFieldStoryFileHashes(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var hashes = new AetheriaRuntimeStoryFileHash[count]; + for (var hash = 0; hash < count; hash++) + { + var hashFields = reader.ReadArrayHeader(); + var storyPath = ReadFieldString(ref reader, hashFields, 0); + var value = ReadFieldString(ref reader, hashFields, 1); + SkipRemaining(ref reader, hashFields, 2); + hashes[hash] = new AetheriaRuntimeStoryFileHash(storyPath, value); + } + + return hashes; + } + + private static PlayerGameplaySettings ReadFieldPlayerGameplaySettings(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return new PlayerGameplaySettings("Celsius", 3); + var gameplayFields = reader.ReadArrayHeader(); + var temperatureUnit = ReadFieldString(ref reader, gameplayFields, 0); + var significantDigits = ReadFieldInt32(ref reader, gameplayFields, 1); + SkipRemaining(ref reader, gameplayFields, 2); + return new PlayerGameplaySettings(temperatureUnit, significantDigits); + } + + private static PlayerGraphicsSettings ReadFieldPlayerGraphicsSettings(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return new PlayerGraphicsSettings("Normal", false); + var graphicsFields = reader.ReadArrayHeader(); + var nebulaQuality = ReadFieldString(ref reader, graphicsFields, 0); + var showAsteroidsInMinimap = ReadFieldBool(ref reader, graphicsFields, 1); + SkipRemaining(ref reader, graphicsFields, 2); + return new PlayerGraphicsSettings(nebulaQuality, showAsteroidsInMinimap); + } + + private static PlayerInputSettings ReadFieldPlayerInputSettings(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return new PlayerInputSettings(Array.Empty(), Array.Empty()); + var inputFields = reader.ReadArrayHeader(); + var bindings = ReadFieldInputBindings(ref reader, inputFields, 0); + var actionBarInputs = ReadFieldStringArray(ref reader, inputFields, 1); + SkipRemaining(ref reader, inputFields, 2); + return new PlayerInputSettings(bindings, actionBarInputs); + } + + private static IReadOnlyList ReadFieldInputBindings(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var bindings = new AetheriaRuntimeInputBindingOverride[count]; + for (var binding = 0; binding < count; binding++) + { + var bindingFields = reader.ReadArrayHeader(); + var actionName = ReadFieldString(ref reader, bindingFields, 0); + var bindingIndex = ReadFieldInt32(ref reader, bindingFields, 1); + var bindingPath = ReadFieldString(ref reader, bindingFields, 2); + SkipRemaining(ref reader, bindingFields, 3); + bindings[binding] = new AetheriaRuntimeInputBindingOverride(actionName, bindingIndex, bindingPath); + } + + return bindings; + } + private static IReadOnlyList ReadFieldShapeCells(ref MessagePackReader reader, int fields, int index) { return index >= fields ? Array.Empty() : ReadShapeCells(ref reader); @@ -582,15 +698,59 @@ namespace GameCult.Aetheria.State.Unity private readonly struct PersistedRecord { - public PersistedRecord(string schemaId, byte[] payload) + public PersistedRecord(string key, string schemaId, byte[] payload) { + Key = key; SchemaId = schemaId; Payload = payload; } + public string Key { get; } + public string SchemaId { get; } public byte[] Payload { get; } } + + private readonly struct PlayerGameplaySettings + { + public PlayerGameplaySettings(string temperatureUnit, int significantDigits) + { + TemperatureUnit = temperatureUnit; + SignificantDigits = significantDigits; + } + + public string TemperatureUnit { get; } + + public int SignificantDigits { get; } + } + + private readonly struct PlayerGraphicsSettings + { + public PlayerGraphicsSettings(string nebulaQuality, bool showAsteroidsInMinimap) + { + NebulaQuality = nebulaQuality; + ShowAsteroidsInMinimap = showAsteroidsInMinimap; + } + + public string NebulaQuality { get; } + + public bool ShowAsteroidsInMinimap { get; } + } + + private readonly struct PlayerInputSettings + { + public PlayerInputSettings( + IReadOnlyList bindingOverrides, + IReadOnlyList actionBarInputs) + { + BindingOverrides = bindingOverrides; + ActionBarInputs = actionBarInputs; + } + + public IReadOnlyList BindingOverrides { get; } + + public IReadOnlyList ActionBarInputs { get; } + } } } diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 043006068..e66535951 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -78,6 +78,8 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. `AetheriaInput` class is the edge consumer of typed binding overrides, not the durable owner. Binding drag/drop and action-bar remapping both queue the same typed player-settings commit after mutating the runtime projection. + Unity boot reads typed player settings back through the package-owned + CultCache reader before falling back to defaults. - `Aetheria.State` now exposes typed node put/get ports for run state, zone state, and entity snapshots. The state smoke writes a run referencing a zone, a zone referencing an entity snapshot, and an entity snapshot carrying