The operator's decision of 2026-09-22: pay the fly for keeping a wild Pokemon, bump the adapter properly, and restart the live run from an early checkpoint rather than from scratch. The rule. `catch` is the catalog's ninth kind, appended so the key order `counts` serializes in does not move. 0.30 for a species this run had never owned, 0.10 for a repeat, three payouts per species for the lifetime of the ledger; the `species` rule is untouched, so a first catch of a new species pays 0.80 across two kinds. The catch is read from `wCapturedMonSpecies` ($d11c), whose comment in ram/wram.asm is "0 if no mon was captured": ItemUseBall zeroes it before every throw and writes wEnemyMonSpecies into it only on the branch that keeps the Pokemon, and UseBagItem's `.returnAfterCapturingMon` zeroes it again and sets wBattleResult to 2 -- a value written on exactly two paths in the game, that one and a link battle whose opponent ran. Both are required, so a byte read out of a half-initialised battle cannot pay. Not wPartyCount: a catch with a full party raises wBoxCount instead, and wPartyCount also rises for a gift, a trade and a PC withdrawal. "Never owned this run" is the `species` payout inside the same battle, because nothing else can set a Pokedex bit during one. It is not read off the captured species byte: that is the cartridge's internal index while the owned bitset is by Pokedex number, and nothing in WRAM converts between them. The address was resolved by tools/resolve_wram.py, not written by hand. The tool needed NUM_TMS and NUM_HMS, which the decomp defines through its `const` enumeration, so it now counts them from the file's own add_tm/add_hm definitions and cross-checks NUM_TMS against the literal the same file declares. The feed's kinds are closed, so `catch` publishes on `wildwin` and nothing in packages/feed or apps/stage changed. Deliberately not `pokedex`: the `species` rule already pays for the bit the same catch sets. The stage's ticker copy is keyed on the feed kind, so a catch row reads "wild win" -- stated in docs/rewards-learning.md rather than left to be discovered. v5 -> v6. STATE_VERSION stays 4: the rule adds one counter, `catchCounts`, and changes nothing else, so a v5 state restores with it empty. That migration is opt-in and needs all three of: the adapter segment being the only difference between the two compatibility strings, the running adapter listing the checkpoint's adapter in `migrates_from()`, and the deploy naming it in FLY_ACCEPT_ADAPTERS. flysim applies the rule at restore and 05-deploy's gate applies the same rule before it flips the symlink, writing the variable into fly.env so the two cannot disagree. The restart. infra/bin/fly-reset-to-milestone <N> archives both stores to a dated directory, rewrites milestone-<N>.checkpoint with the ratchet's attempts and recoveries at zero, installs it as the newest generation of both stores, clears the milestone archives above N and the event log, and prints what it did. It refuses while flysim is running and refuses a rung the run never reached. The envelope work is in flysim::reset (`flysim --reset-to-milestone N`); the shell script is the operator's wrapper. Tests: catalog values and order; a synthetic WRAM trace of a catch (new, repeat, cap, already-owned species, trainer/Safari/old-man/missed-ball negatives, rollback replay); a v5 state restoring with the counter at zero; a v5 checkpoint fixture accepted with the opt-in and refused without it; the reset tool against copies of a state dir in temp directories; and a ROM-gated catch from a rung-9 forest checkpoint, driven by the shipping THROW BALL macro. The compatibility string differs from main's in exactly one segment, checked by splitting both on `/`: pokered-unique8-v5 -> pokered-unique8-v6.
77 lines
3.6 KiB
Bash
Executable file
77 lines
3.6 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# infra/bin/fly-reset-to-milestone — restart the run from an earlier ladder rung,
|
|
# instead of from scratch.
|
|
#
|
|
# The operator's decision of 2026-09-22: "restart the live run from an early
|
|
# checkpoint instead of from scratch". 05-deploy's FLY_RESET_STATE=1 cannot do
|
|
# that — it archives the durable state and the next start warms up a fresh fly,
|
|
# losing everything the brain has learned. This promotes one milestone archive
|
|
# (milestone-<N>.checkpoint, written at the first commit at a new best rank and
|
|
# never rotated away) to being what both stores restore.
|
|
#
|
|
# Usage: fly-reset-to-milestone <N>
|
|
# Run INSIDE the container, as root, with flysim STOPPED. It refuses
|
|
# otherwise, and it refuses a rung this run never reached.
|
|
#
|
|
# The whole sequence — stop, reset, deploy with the adapter opt-in, start,
|
|
# verify the rank — is in infra/docs/runbook.md, "Restart the run from a rung".
|
|
# Nothing here is destructive on its own: every file in both stores is copied to
|
|
# a dated directory next to the durable one before anything is rewritten.
|
|
set -euo pipefail
|
|
|
|
: "${FLY_STATE_DIR:=/srv/fly/state}"
|
|
: "${FLY_STATE_HOT_DIR:=/run/fly/state}"
|
|
: "${FLY_RELEASE_DIR:=/opt/fly/current}"
|
|
: "${FLY_SERVICE:=flysim.service}"
|
|
: "${FLY_USER:=fly}"
|
|
|
|
FLYSIM="${FLY_BIN:-${FLY_RELEASE_DIR}/flysim}"
|
|
|
|
log() { echo "fly-reset-to-milestone: $*" >&2; }
|
|
die() { log "$*"; exit 1; }
|
|
|
|
RANK="${1:-}"
|
|
if [ "$#" -ne 1 ] || ! [[ "$RANK" =~ ^[0-9]+$ ]]; then
|
|
die "usage: fly-reset-to-milestone <rung> (e.g. fly-reset-to-milestone 9)"
|
|
fi
|
|
|
|
# --- refusals ----------------------------------------------------------------
|
|
# A running flysim owns both stores: it commits a hot checkpoint every few
|
|
# seconds and a durable one every few minutes, so a reset underneath it would be
|
|
# overwritten within the minute and the tool would have lied.
|
|
if command -v systemctl >/dev/null 2>&1 && systemctl is-active --quiet "$FLY_SERVICE"; then
|
|
die "$FLY_SERVICE is running. Stop it first: systemctl stop $FLY_SERVICE"
|
|
fi
|
|
[ -x "$FLYSIM" ] || die "no flysim binary at $FLYSIM (set FLY_BIN to point at one)"
|
|
|
|
milestone="${FLY_STATE_DIR}/milestone-${RANK}.checkpoint"
|
|
# The binary refuses this too, and refuses before it copies anything; checking
|
|
# here as well is what makes the message name the rungs that do exist.
|
|
if [ ! -f "$milestone" ]; then
|
|
log "no milestone archive for rung ${RANK}: $milestone does not exist."
|
|
log "rungs this run reached:"
|
|
ls -1 "${FLY_STATE_DIR}"/milestone-*.checkpoint 2>/dev/null \
|
|
| sed 's|.*/milestone-||; s|\.checkpoint$||' | sort -n | tr '\n' ' ' >&2 || true
|
|
echo >&2
|
|
exit 1
|
|
fi
|
|
|
|
# --- the reset ---------------------------------------------------------------
|
|
log "resetting to rung ${RANK} (durable ${FLY_STATE_DIR}, hot ${FLY_STATE_HOT_DIR})"
|
|
FLY_STATE="$FLY_STATE_DIR" FLY_STATE_HOT="$FLY_STATE_HOT_DIR" \
|
|
"$FLYSIM" --reset-to-milestone "$RANK"
|
|
|
|
# flysim runs unprivileged; this tool runs as root, so everything it wrote and
|
|
# everything it archived has to go back to the service account.
|
|
if command -v chown >/dev/null 2>&1 && id "$FLY_USER" >/dev/null 2>&1; then
|
|
chown -R "${FLY_USER}:${FLY_USER}" "$FLY_STATE_DIR" "$FLY_STATE_HOT_DIR" 2>/dev/null || true
|
|
for dir in "${FLY_STATE_DIR}".reset-*; do
|
|
[ -d "$dir" ] && chown -R "${FLY_USER}:${FLY_USER}" "$dir"
|
|
done
|
|
fi
|
|
|
|
log "done. Next, per infra/docs/runbook.md:"
|
|
log " 1. deploy the build whose adapter wrote that checkpoint, or deploy the new"
|
|
log " one with FLY_ACCEPT_ADAPTERS set to the checkpoint's adapter id"
|
|
log " 2. systemctl start $FLY_SERVICE"
|
|
log " 3. curl -s localhost:7401/status | grep -o '\"rank\":[0-9]*'"
|