diff --git a/docs/re/structures/savegame-format.md b/docs/re/structures/savegame-format.md index 8cb5ca7..8a0382d 100644 --- a/docs/re/structures/savegame-format.md +++ b/docs/re/structures/savegame-format.md @@ -82,11 +82,11 @@ photographed against this exact save (see "Naming the fields", further down). | +24 | **4101** (`0x1005`) | ✅ **Points — the spendable balance.** Named from the Details panel, and **separated from +28** by the develop differential: spending 4000 P moved *only* this field (4101 → 101) and the panel then read `Points 101 P` | | +28 | **4101** (`0x1005`) | 🟡 **not** the displayed Points — it did **not** move when 4000 P was spent. A lifetime/earned total is the obvious read; unproven until a save is taken after *earning* points | | +32 | 79 | ❔ | -| +36 | 2 | 🟡 **difficulty or stage** — the panel shows `Difficulty EASY` *and* `STAGE 02`, and +36/+52/+56 all hold 2, so which is which is **not** decidable from one save | +| +36 | 2 | ❌ **not** difficulty and **not** stage — refuted by probe saves (see below) | | +40 (u64) | 2014400 | ❔ | | +48 | 0 | ❔ (`Times Cleared: 0` is on screen, so it is one of the zero fields) | -| +52 | 2 | 🟡 see +36 | -| +56 | 2 | 🟡 see +36 | +| +52 | 2 | ❌ see +36 — refuted | +| +56 | 2 | ❌ see +36 — refuted | | +60 | 0 | ❔ | | +64 (raw 4) | `09 15 00 00` | ❔ the trailer's u32 is the **same** value | | +68 (raw 54) | see below | ✅ **per-item Arsenal development state**, one byte per item, `4` = owned, `2` = developable now, `0` = locked ([evidence](#the-develop-differential-one-weapon-three-fields)). Only the `4`s are stored state — `2` is re-derived at load. The index space is **solved**: it is `strings.tbl`'s item order, cut items included ([mapping](../arsenal-develop-economy.md)) | @@ -298,4 +298,35 @@ What the title does with an edited save is itself a finding: on load it can now develop …"). So the blob's `4`s are authoritative state and its `2`s are not — see the economy note. +### What the probes then refuted: +36 / +52 / +56 are **not** stage or difficulty + +The three fields that all hold `2` were read as "difficulty or stage, undecidable +from one save" from the day the format was parsed. Writing saves makes it +decidable, and the answer is **neither**. + +Method — [`boot_menu.sh`](../../../tools/re-capture/boot_menu.sh) boots to the +title menu **without loading anything**, and LOAD GAME's slot list renders each +slot's Details panel (`STAGE`, `Game Status`, `Points`, `Times Cleared`) plus a +row (date, `Difficulty`, flight time, clear ratio) straight out of that slot's +payload. Extra slots can be fabricated — copy the save directory and its +`Headers/…/gameNN.header`, patching the UTF-16BE display string and the ASCII +`gameNN` inside it — so **four probes fit in one boot**, read-only, nothing +loaded. + +Eleven candidate fields were written and read back: `+36` at 1, 3 and 9; `+52` +and `+56` at 1 and 9; `+0`, `+16`, `+32`, `+48`, `+28`, and `SHAB[0].a` — every +one of them left the panel at `STAGE 02 / Declaration of War`, `Difficulty EASY`, +`At Standby`, `Times Cleared 0`. + +The negative is meaningful because the panel demonstrably *does* read each +payload: slot 02 shows `Clear Ratio 5 %` against the others' `6 %`, and `Points` +tracked `+24` exactly. Two further controls: patching a slot's **header** string +to `STAGE09 HARD` changed nothing, so the display is payload-driven, not header +text; and the row's date follows the **container FILETIME**, which is why every +fabricated slot showed 18:04. + +So stage and difficulty live in one of the remaining unprobed fields — `+12`, +`+20`, `+40` (u64), `+60`, `+64` (raw 4) — or in the phase string. That is the +next probe round, and it is now cheap. + Editing beyond a throwaway slot is still the user's call. diff --git a/tools/re-capture/boot_menu.sh b/tools/re-capture/boot_menu.sh new file mode 100755 index 0000000..940177a --- /dev/null +++ b/tools/re-capture/boot_menu.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Boot to the TITLE MENU and stop there — no save is loaded. +# +# nav_probe.sh's boot half deliberately loads a save, which is wrong for any +# experiment that wants to *inspect* save slots rather than enter one: with +# several probe saves on disc the load picks one of them, and a probe value the +# game cannot make sense of turns into a long "NOW PROCESSING" (or a mission +# load, if a d-pad step is dropped and A lands on TAKE OFF). +# +# From the main menu, LOAD GAME is one d-pad step away and its slot list renders +# every slot's Details panel — STAGE, Game Status, Points, Times Cleared — which +# is read straight out of each save's payload. That makes this the cheap harness +# for save-field probes: write N slots, boot once, arrow through them. +# +# Run as ONE BLOCKING FOREGROUND call. Usage: boot_menu.sh [tag] +set -u +export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 +SD="$(cd "$(dirname "$0")" && pwd)" +SHOTS=/sylph-home/re/shots +TAG="${1:-menu}" +mkdir -p "$SHOTS" + +alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $1}'; } +ensure_display(){ + if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then + rm -f "/tmp/.X${DISPLAY#:}-lock" 2>/dev/null || true + nohup bash -c 'Xvfb "$0" -screen 0 1280x720x24 -ac -nolisten tcp \ + +extension GLX +extension RANDR >/tmp/xvfb98.log 2>&1' "$DISPLAY" /dev/null 2>&1 & + for _ in $(seq 1 50); do xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 && break; sleep 0.2; done + nohup env DISPLAY="$DISPLAY" HOME=/sylph-home openbox /tmp/openbox98.log 2>&1 & + sleep 1 + fi + xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 || { echo "DISPLAY UNAVAILABLE"; exit 1; } +} + +[ -n "$(alive)" ] && { kill -9 $(alive) 2>/dev/null; sleep 2; } +rm -f /dev/shm/xenia_memory_* /dev/shm/xenia_code_cache_* 2>/dev/null +ensure_display +cd /sylph-home/re +nohup run-canary --audio --apu=sdl --log_mask=13 \ + --logged_profile_slot_0_xuid=E0300000EFBEA3D4 /dev/null 2>&1 & +sleep 5 +"$SD/skip_intro.sh" 600 || { echo "BOOT FAILED (skip_intro exit $?)"; exit 1; } +sleep 14 +screenshot "$SHOTS/$TAG-menu.png" >/dev/null 2>&1 +echo "AT MAIN MENU (cursor on NEW GAME); LOAD GAME is one d-pad step down" diff --git a/tools/re-capture/savegame_edit.py b/tools/re-capture/savegame_edit.py index b25fa73..0e77236 100755 --- a/tools/re-capture/savegame_edit.py +++ b/tools/re-capture/savegame_edit.py @@ -78,6 +78,13 @@ def main(): elif a == "--set": k, v = args[i + 1].split("=") parsed["ghad"][k] = int(v, 0) + elif a == "--slot": + # --slot ,=, e.g. --slot 0,0=3 + where, v = args[i + 1].split("=") + rec, fld = (int(x) for x in where.split(",")) + row = list(parsed["slots"][rec]) + row[fld] = int(v, 0) + parsed["slots"][rec] = tuple(row) parsed["ghad"]["blob_68"] = bytes(blob) out = wrap(header, sg.serialize(parsed))