Android: a Game-scope save must still write the process-wide fields

Toggling PINE from the in-game menu wrote it nowhere, so the setting was gone
at the next launch while the switch still read as enabled -- saveSettings had
already updated the in-memory Settings, and only a process restart exposed that
the store never agreed.

PINE is one server for the whole process, so "this game runs with PINE on" is
not a thing that can be true. Settings.merge therefore pins pineEnabled and
pineSlot to the global value, and Settings.diff never emits either key, so a
per-game file can never acquire them. Both are deliberate and both are right.

What was missing is the other half: a Game-scope save writes ONLY the override
file. So for these two fields the write had no destination at all -- the
override file structurally refuses them, and global was never touched. Every
other field is fine, because every other field is one the override file accepts.
The in-game menu saves in Game scope whenever a game is running, which is
exactly when someone reaches for PINE, so the toggle looked simply broken.

So promote those fields to global on a Game-scope save. Copied onto the loaded
global rather than saving `updated` wholesale: `updated` is the game's RESOLVED
settings, so writing all of it to global would push every per-game value into
the global layer. The diff below is unaffected -- it reads the pre-promotion
`global`, and the keys involved are precisely the ones it never emits.

Pairs with the core fix that makes a commit act on the value; without this the
value never survived to be acted on a second time.
This commit is contained in:
Brian Degenhardt
2026-08-14 11:45:40 -07:00
parent 58b6dd983c
commit 6aa2fd79d9
@@ -289,6 +289,20 @@ object ConfigStore {
fun save(scope: SettingsScope, serial: String?, updated: Settings, previous: Settings? = null) {
if (scope == SettingsScope.Game && serial != null) {
val global = loadGlobal()
// Process-wide fields have to go to global even from a Game-scope save, because the
// per-game file structurally cannot hold them. PINE is one server for the whole
// process, so Settings.merge pins it to the global value and Settings.diff never
// emits the key -- both deliberate. The consequence was that toggling PINE from the
// in-game menu, which saves in Game scope, wrote it NOWHERE: the override file
// refuses the key and global was not being written. The switch stayed on only
// because saveSettings had already updated the in-memory Settings, so it read as
// "enabled" until the process restarted and the store answered false again.
//
// Promote just those fields, by copying them onto global rather than saving
// `updated` wholesale -- `updated` is the game's resolved settings, and writing all
// of it to global would leak every per-game value into the global layer.
if (updated.pineEnabled != global.pineEnabled || updated.pineSlot != global.pineSlot)
saveGlobal(global.copy(pineEnabled = updated.pineEnabled, pineSlot = updated.pineSlot))
val overrides = Settings.diff(global, updated)
// Every field, so a pinned key can be given its CURRENT value even when that
// value equals global's (the diff above necessarily omits it).