re: +36/+52/+56 are neither stage nor difficulty -- refuted with eleven probe saves
Those three fields all hold 2, and "difficulty or stage, undecidable from one save" has been the reading since the format was parsed. Writing saves makes it decidable and the answer is neither. boot_menu.sh boots to the title menu WITHOUT loading anything, and LOAD GAME's slot list renders each slot's Details panel from that slot's payload. Extra slots can be fabricated (copy the directory plus a gameNN.header with its UTF-16BE display string and ASCII name patched), so four probes fit in one boot, read-only. Probed: +36 at 1/3/9, +52 and +56 at 1/9, and +0, +16, +32, +48, +28, SHAB[0].a. Every one left the panel at STAGE 02 / EASY / At Standby / Times Cleared 0. The negative is meaningful because the panel does read each payload -- slot 02 shows 5% clear ratio against the others' 6%, and Points tracked +24 exactly. Two controls: patching a slot header to "STAGE09 HARD" changed nothing (the display is payload-driven, not header text), and the row date follows the container FILETIME. Remaining candidates: +12, +20, +40 (u64), +60, +64, or the phase string. Also here: savegame_edit.py --slot for SHAB records, and boot_menu.sh itself -- nav_probe.sh's boot loads a save, which with probe slots on disc loads a probe, and a dropped d-pad step there put A on TAKE OFF and spent a boot loading a mission. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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` |
|
| +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 |
|
| +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 | ❔ |
|
| +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 | ❔ |
|
| +40 (u64) | 2014400 | ❔ |
|
||||||
| +48 | 0 | ❔ (`Times Cleared: 0` is on screen, so it is one of the zero fields) |
|
| +48 | 0 | ❔ (`Times Cleared: 0` is on screen, so it is one of the zero fields) |
|
||||||
| +52 | 2 | 🟡 see +36 |
|
| +52 | 2 | ❌ see +36 — refuted |
|
||||||
| +56 | 2 | 🟡 see +36 |
|
| +56 | 2 | ❌ see +36 — refuted |
|
||||||
| +60 | 0 | ❔ |
|
| +60 | 0 | ❔ |
|
||||||
| +64 (raw 4) | `09 15 00 00` | ❔ the trailer's u32 is the **same** value |
|
| +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)) |
|
| +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
|
can now develop …"). So the blob's `4`s are authoritative state and its `2`s are
|
||||||
not — see the economy note.
|
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.
|
Editing beyond a throwaway slot is still the user's call.
|
||||||
|
|||||||
46
tools/re-capture/boot_menu.sh
Executable file
46
tools/re-capture/boot_menu.sh
Executable file
@@ -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 >/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 </dev/null >/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 >/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"
|
||||||
@@ -78,6 +78,13 @@ def main():
|
|||||||
elif a == "--set":
|
elif a == "--set":
|
||||||
k, v = args[i + 1].split("=")
|
k, v = args[i + 1].split("=")
|
||||||
parsed["ghad"][k] = int(v, 0)
|
parsed["ghad"][k] = int(v, 0)
|
||||||
|
elif a == "--slot":
|
||||||
|
# --slot <record>,<field>=<value>, 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)
|
parsed["ghad"]["blob_68"] = bytes(blob)
|
||||||
|
|
||||||
out = wrap(header, sg.serialize(parsed))
|
out = wrap(header, sg.serialize(parsed))
|
||||||
|
|||||||
Reference in New Issue
Block a user