Commit graph

248 commits

Author SHA1 Message Date
acamilo
9301e395e8 docs: the trap hunt and the bench run the service's frame since FND-01, and hunts across the change do not compare 2026-09-23 20:15:20 +00:00
acamilo
9e2f90749f flysim: FLY_TRACE's boundary half read back with fly-session-types, the journal held to the service test, rustfmt on the new files 2026-09-23 20:15:20 +00:00
acamilo
60f09b79a3 flysim: every harness runs the legacy frame: the trap hunt, the benches, the ROM tests' and the probe's stub drivers
trap_hunt, palette_bench and room_escape ran the frame through NeuralAgent::tick, which installs
a frame and its rewards after the next ticks: one frame behind the stream, with the ratchet
observed without the objective signal and a rollback that never re-observed the scene. They now
restore the way the stream restores and run LegacyFrame::transition and ::boundary, looking in
through FrameObserver; FLY_TRACE works in each of them. The stub-readout drivers of
rom_macros_mode, rom_catch and scene_probe run LegacyFrame::execute and ::stub_advance, which are
the same calls they made, in the same order.
2026-09-23 20:15:20 +00:00
acamilo
ad1c0e3693 flysim: a frame-stamped sugar journal in the hot directory
Every admitted sugar and operator reward pulse is appended to sugar-journal.jsonl with the frame
counter it was applied at, which is the step of the next transition in FLY_TRACE, so a shadow run
can replay the audience's input. Append-only, not checkpointed, never read back into the fly.
2026-09-23 20:14:01 +00:00
acamilo
94cc91369a flysim: the legacy frame is one type, flysim::frame::LegacyFrame, and the sim loop runs it
prepare, execute, advance, evaluate, commit, then the boundary's ratchet capture and rollback,
in the order the stream runs them. The frame owns the remainder, the frame counter, the frame on
screen, the mask and the blocked-direction window; the sim loop keeps the feed, the event log,
the milestone archive between commit and boundary, and the checkpoints. Two moves between
disjoint state change no byte: the visual frame is installed at commit, and the stimulations
follow the scene's observation. The FLY_TRACE of a run from the rung-10 checkpoint is
byte-identical to the one the inline hooks wrote.
2026-09-23 20:14:00 +00:00
acamilo
ff74c07622 flysim: FLY_TRACE, a per-frame trace of the legacy loop in the step trace's field names
One JSON line per transition: ticks and the exact remainder, digests of the rates, of the
transition's spikes, of the frame and of work RAM, the decision and the mask, macro and reward
events in order, the rank, and the boundary's slot saves, rollback and captures in the shape of
TraceBehaviour.boundaryActions and TraceOperational.captures. Off unless FLY_TRACE names a file.
Recorded inline in step_frame first, so the frame's extraction can be held to it.
2026-09-23 20:14:00 +00:00
acamilo
bc117ad470 flybrain-gb: say what a move with power does not promise 2026-09-23 19:23:48 +00:00
acamilo
fa021918b4 Merge fix/loop-row60: a move the cartridge would refuse is not dealt beside one that works 2026-09-23 19:23:43 +00:00
acamilo
4591e7907a docs: macros.md 12.23, macros-wram.md section 13, the row 60 audit 2026-09-23 17:59:29 +00:00
acamilo
07f88c443a tests: the ROM proof from the Route 1 checkpoint 2026-09-23 17:23:05 +00:00
acamilo
b33af9e772 scene_probe: the route survey prints the battle bytes a MOVE n button's effect rests on 2026-09-23 17:23:05 +00:00
acamilo
a02d5f7995 trap_hunt: payouts by kind, battle lengths and wins, and MOVE n on a move without effect 2026-09-23 17:23:05 +00:00
acamilo
f3e3e9cf98 palette: a move the cartridge answers with nothing is not dealt beside one it does not
Row 60: TAIL WHIP against a Pidgey at DEFENSE -6 was MOVE 2 183 times. The
rule is PP's: a refused move leaves the pad while another move is usable, and
with none usable the moves stay as PP deals them, so an open list never
comes down to BACK alone.
2026-09-23 17:23:05 +00:00
acamilo
438b2d8540 state: the move table, and whether the battle engine will answer a move with nothing
move_data reads a row of Moves from the cartridge image ($0E:$4000, each row
checked against its own id); move_without_effect answers the refusals the
effect routines make on bytes already in WRAM: a stat stage at its limit or a
stat at 1/999, Mist or a substitute against a stat-lowering move, a status
move against a statused, Poison-type or (Electric) Ground-type target.
MacroState::move_without_effect defaults to false.
2026-09-23 17:23:05 +00:00
acamilo
c8ecf06db5 symbols: the stat stages and the enemy's status, type and stats, resolved from wram.asm
resolve_wram.py now follows the decomp's const and _RS counters and a struct
macro's field labels, so battle_struct fields resolve and 77 of 81 pinned
addresses are re-derived with no disagreement.
2026-09-23 17:23:05 +00:00
acamilo
37adeb9417 docs: a map script's text can pay talk when it names the thing in front, and where it can 2026-09-23 17:22:11 +00:00
acamilo
5d527f0910 Merge feat/engage-reward: the fly is paid for talking to people indoors and picking up items, not for leaving buildings (adapter v7) 2026-09-23 17:20:02 +00:00
acamilo
56b02ff758 rewards: a frame on which no item bit moved reads nothing more 2026-09-23 14:34:10 +00:00
acamilo
8d1d5c44f5 docs: the engagement rewards, and the v6 migration in deploy examples
rewards-learning.md carries the v7 table, the definition of indoors, the four
conditions a conversation pays under, the item and hidden-item reads, the seed,
boundary's indoor rule with the warp-transition frame it was measured on, the
feed-kind choice and the ticker word, and the honesty note on paying for a
conversation. flysim.md records the v6 -> v7 migration; macros-wram.md the six
new reads; the architecture tour and integration.md point at the live catalog.
The deploy comments, example.env and the runbook name FLY_ACCEPT_ADAPTERS=
pokered-unique8-v6 for the v0.6.0 deploy.
2026-09-23 14:34:10 +00:00
acamilo
3cf706c96a rewards: read the conversation's argument once the cartridge has written it, and a ROM test
The first cartridge run found what the synthetic trace could not. In the
Viridian Forest north gate the font bit rose on one frame and DisplayTextID's
argument reached wSpriteIndex twenty frames later: DisplayTextIDInit loads the
font's tiles into VRAM first. Until then the byte still names the previous
text's subject, which may be the person in front of the fly from a conversation
that did not pay. So the watch now remembers the byte from the last ready
frame and reads it once, when it changes or after 45 samples, and only while
the bottom dialogue box is drawn (the start menu draws its own elsewhere); a
box that closes first is read on the closing sample if the dialogue box was up.

tests/rom_engage.rs, gated on FLY_ROM and FLY_ENGAGE_CHECKPOINT: from the rung-10
Pewter checkpoint, with the checkpoint's own v6 ledger restored (the migration,
on real game state), a scripted walker goes down Route 2 into the forest's north
gate, talks to the old man three times (one payout, 0.10) and picks up the
Antidote ball at (25, 11) (one payout, 0.15, "FOUND ITEM #11"); pressing A at
the empty tile pays nothing, and after rolling the emulator back to before the
pickup, taking it again pays nothing. No boundary payout lands while an indoor
header is loaded.

It also measured the warp transition: for about thirty frames of
PlayMapChangeSound wCurMap already names the destination while the tileset and
warp table are still the map being left, so the exit the fly stands on is
classified by the map it belongs to -- a town door still pays its on-exit half,
keyed under the building's id as before, and a building's door does not. A unit
test pins both directions.
2026-09-23 14:34:10 +00:00
acamilo
2d288b0146 stage: the explore row reads "new find"
Since pokered-unique8-v7 the explore feed kind carries four adapter rules --
new ground, an exit, a conversation indoors, an item picked up -- and "new
place" was true of the first two only. The per-game copy now names what all
four are; the feed protocol and the kind set are unchanged.
2026-09-23 14:34:10 +00:00
acamilo
52811bf512 session types: the legacy composition names pokered-unique8-v7
The example composition's adapter and FLYSIM01 string move to v7 with the live
adapter, and the fixtures are regenerated by update_fixtures; valid.json and
invalid.json's hand-written cases follow, with the one deliberately mismatched
string moving from v5 to v6 so it is still the previous adapter.
2026-09-23 14:34:10 +00:00
acamilo
2e7eed0a78 rewards: talk and item, boundary indoors pays nothing, adapter v7 with a v6 migration
The operator's decision of 2026-09-23: pay the fly for engaging with what is
inside a building, and stop paying it for walking back out of one. Chosen over
a pad rule and over weighting the choice, and like v0.5.0's catch reward it is
a catalog change, not a loop-review fix.

Indoors. engage::indoor is two of the cartridge's own tables: not outside by
CheckIfInOutsideMap (tileset OVERWORLD or PLATEAU; WarpFound2 labels the other
branch .indoorMaps) and not a place BikeRidingTilesets lets the bike be ridden
(OVERWORLD, FOREST, UNDERGROUND, SHIP_PORT, CAVERN). That is every house, mart,
center, gym, gate, lab, museum, ship, tower, mansion and hideout, and not
Viridian Forest, a cave, the Underground Path or the dock.

`talk`, +0.10, 100 ms, the catalog's tenth kind. Paid when the text box closes
on a conversation that (1) opened on the sample after one where the fly had the
joypad (state::controllable) and was standing still (wWalkCounter zero, the only
state the overworld reads A in) on the same tile; (2) is with the thing in
front: DisplayTextID copies its argument into wSpriteIndex, a sprite slot up to
wNumSprites whose sprite stands on the tile the player faces (or one further
across a counter, on a tileset with counter tiles), or a text id matching the
sign on that tile; an item ball is not a person; (3) opened indoors; (4) closed
on the same map. Keyed talk:<map>:sprite:<slot> / talk:<map>:sign:<id> in the
lifetime `seen` ledger, which is checkpointed and survives a rollback -- not the
macros' session `talked` ledger.

`item`, +0.15, 120 ms, the eleventh. An item ball is a toggleable sprite of this
map (wToggleableObjectList) whose wMapSpriteExtraData is (item id, 0), the shape
LoadMapHeader writes for an ITEM object_event and nothing else; PickUpItem sets
its global bit in wToggleableObjectFlags after GiveItem succeeds. A hidden item
is a bit of wObtainedHiddenItemsFlags, set by FoundHiddenItemText after GiveItem
and by nothing else. Either pays when its bit rises between two playable
samples, once per item (item:<global> / hidden:<index>) for the ledger's life.

`boundary` still writes every key indoors, so exit_visited answers what it did
and the macros see no change, but emits nothing on an indoor map. Outdoor,
forest and cave exits pay as before.

v6 -> v7. STATE_VERSION stays 4 and no field is added: the new ledgers are keys
in `seen`. A v6 state restores with no talk: keys, and the first playable sample
that finds `items:seeded` absent writes one key per item the game already shows
as taken, pays for none, and marks the seed -- so a rollback that un-takes a v6
pickup cannot pay for it. The two item balls a script reveals (the Rocket
Hideout's Silph Scope and Lift Key, toggles $87 and $88, the only ITEM entries
toggleable_objects.asm starts OFF) are left out of the seed. MIGRATES_FROM is
["pokered-unique8-v6"]; v5 is no longer migrated.

Feed kinds: both publish on `explore`, the family of new ground and a door found,
at the same quiet scale; not `area` (maps, notable), `story` or `wildwin`. No
feed-protocol change.

The compatibility string differs from main's in exactly one segment:
pokered-unique8-v6 -> pokered-unique8-v7.

Tests: catalog values and order; indoor over all 24 tilesets; a talk pays once
per (map, object), not while the box is open, not re-talked, not outdoors/in the
forest/in a cave, not for text opened with the joypad taken, simulated, scripted,
mid-step, about someone not in front, or the start menu; not across a warp or a
rollback; counter reach only with counter tiles; item balls, people, trainers,
hidden items, the seed and the script-shown balls; boundary indoors records and
pays nothing; a v6 state and a v6 FLYSIM01 envelope migrate with the new ledgers
empty and the items seeded. rom.rs's bedroom walk now proves the stairs are
recorded and unpaid on the cartridge.
2026-09-23 14:34:10 +00:00
acamilo
ed0080ab2c symbols: the six WRAM names the engagement rewards read
wSpriteIndex, wWalkCounter, wMapSpriteExtraData, wToggleableObjectFlags,
wToggleableObjectList and wObtainedHiddenItemsFlags, resolved by
tools/resolve_wram.py from ram/wram.asm at the pinned commit and bracketed by
addresses symbols.rs already carries. Two of the brackets needed the tool to
count NUM_STATS and NUM_CITY_MAPS, which the decomp defines as const_value over
an enumeration; both are now counted up to the line that defines them.
2026-09-23 14:34:10 +00:00
acamilo
7784a9d172 docs: v0.5.5 status
Some checks are pending
ci / node 22 (test + typecheck) (push) Waiting to run
ci / rust stable (cargo test --workspace --release) (push) Waiting to run
ci / infra/tests/lint.sh (push) Waiting to run
ci / playwright apps/stage (allowed to fail) (push) Waiting to run
2026-09-23 13:43:21 +00:00
acamilo
6fd6d840dc Merge fix/loop-row58: the gym's people are there when the screen does not draw them, and the game's own moves write no ledger 2026-09-23 13:43:18 +00:00
acamilo
0f510c93aa infra(05-deploy): refuse a bad FLY_FEED_VIA before the release is flipped, not half way through 2026-09-23 13:13:59 +00:00
acamilo
0ca5f000d6 Merge feat/fly-edge-feed: EDGE-01, the feed over flybus behind FLY_FEED_VIA, served by fly-edge 2026-09-23 13:13:18 +00:00
acamilo
21701579a3 docs: the row 58 audit 2026-09-23 13:08:40 +00:00
acamilo
fc7fdffa6c docs: flybus.md, the reachable worst case and the known limits of the feed on the bus
Review round 1. The sizing amendment's worst case is one socket client with
all four subscriptions (15 snapshots, about 1.8 MB, 472,061 bytes measured
because fan-out shares artifacts), not seven stuck subscribers. The lifecycle
amendment gains deploy-time validation, the flyedge cpuset and the absolute
bus dir; a known-limits list records what review round 1 left as notes: feed
counters off the container, store quota per router, rollback while in bus
mode, and the old fixtures.
2026-09-23 11:59:40 +00:00
acamilo
4b1559b45a fly-edge, flysim: a taken feed port is named as such; FLY_BUS_DIR must be absolute; worst case restated
Review round 1. A bind failure after subscribing was logged once as
"waiting for the feed bus". session() now ends as Unreachable, BindFailed or
BusLost, each logged as what it is (once per streak), and
fly_edge_bind_failures_total counts the second; a new parity test holds the
port, sees the edge fail to bind without claiming to serve, frees it and gets
served.

feed.bus_dir (FLY_BUS_DIR) must be absolute and non-empty, checked in either
mode, since flysim and the edge each resolve it and a relative path would
let them disagree.

The sizing worst case was 7 stuck subscribers; that seat does not exist,
since edge.sock admits one client. It is that client with all 4
subscriptions it may open: 4*3+1+2 = 15 snapshots, about 1.8 MB. feedbus's
comment and unit test say so, and stall.rs's hoarding scenario now takes all
four subscriptions, checks a fifth and a second connection are refused, and
bounds the store at 15 snapshots.
2026-09-23 11:59:26 +00:00
acamilo
2da7f688a8 infra: FLY_FEED_VIA is validated at deploy and read case-blind by the watchdog; flyedge gets the page's CPUs
Review round 1. flysim reads FLY_FEED_VIA case-insensitively, so check 2 must
too: with Bus in fly.env it read flysim's zeroed counters and would have
escalated to restarting flystage and flycast every pass. It now lowercases.
05-deploy.sh runs the value through feed_via_normalize (lib/common.sh) and
dies on anything but direct|bus, writing the lowercased word, so a typo is a
deploy refusal instead of a flysim boot loop.

lint's fly.target check read only the first physical Wants=/Requires= line;
target_pulls joins backslash continuations and drops comments, with a
fixture that names a unit only on a continuation line. The cpuset loop writes
a flyedge drop-in on the page CPUs, and lint holds it.
2026-09-23 11:50:11 +00:00
acamilo
ae3b15ab70 tests: the ROM proof from the gym-door checkpoint
From the row-58 checkpoint, 30 brain minutes on the stub rotation: at
most three gym arrivals end in the fly walking straight back out inside
ten seconds, and the fly goes up the room to row 6 or above, where the
Jr. Trainer stands. Base: one arrival, back out in 309 frames, highest
row 11 -- fails. Branch: 4 arrivals, 1 back out, 30,879 frames in the
gym, highest row 2, beside the leader. Rung 11 is printed, not asserted.
2026-09-23 11:48:57 +00:00
acamilo
04657d3324 docs: macros.md 12.22, macros-wram.md section 12 2026-09-23 11:48:57 +00:00
acamilo
2b1a6c0dea watchdog: check 10 reads the reward events, and flags a window busy going nowhere
Row 58's pad was GO OBJECTIVE into the Pewter Gym and GO OUT straight
back out for 25 minutes, diluted by eight other names, every macro done:
ten distinct names, so the four-name sequence rule could not fire, no
macro near 95%, nothing refused or blocked, and the exploration count
flat. What the window did not have was a reward event.

The stream carries the reward events beside the macros, and one more
rule reads them behind the same no-new-ground gate: WD_LOOP_BUSY_MIN
(100) decisions and no reward in the window, on two probes running, is
'unrewarded'. fly_loop_rewards is exported and loop.json carries
window.rewards. Run against the live row-58 log it flags (211 decisions,
0 rewards, 10 names) where the rules before it did not. Still never
acts: the fixture's two new cases restart nothing.
2026-09-23 11:48:57 +00:00
acamilo
28bd980e65 survey: the route probe prints the room whole, its people, and the rung at the end
The catch dump prints a small map whole with its people drawn and off
the screen, each person's ledger entries and whether a route reaches
them; outcomes are keyed by the map they finished on, the trace line
carries the seam's bytes (wCurOpponent included), and the drive ends
with the rank. How row 58's mechanism was read.
2026-09-23 11:48:57 +00:00
acamilo
ddf0743395 a trainer's challenge is not the cartridge refusing a step
Section 12.4 and row 37 read a macro the cartridge ended by taking the
joypad as a refusal and wrote the target blocked and the tile pushed on
the spot. A trainer who sees the fly takes the joypad the same way. In
the Pewter Gym the walk toward the leader crossed the Jr. Trainer's
sight line, and BROCK went into the blocked ledger for ten brain minutes
while the fly lost, blacked out and walked back to a room whose way out
was the pad again.

The entries now wait for the cartridge to give the joypad back: back in
the overworld is a refusal, written as before; a battle is a battle and
teaches the ledgers nothing. Tests for the rung's people off the screen,
facing one of them, and the challenge; the two push-back tests now hand
the joypad back before they read the ledgers.
2026-09-23 11:48:57 +00:00
acamilo
1ba5c80053 tests: stall.rs gates only the pacing claim; the rates move to an ignored perf test
Review round 1, B1. The p99 sleep-overshoot bound measured the OS scheduler
and the published*2 bound failed whenever a starved debug publisher
coalesced, which it is designed to do, so the workspace gate went red on a
loaded box. The three gated tests keep what the slice claims: pacer lag 0,
no watch send held by a consumer (50 ms bound), no refused publication,
something reaches the bus, a bounded store, and a healthy client that reaches
the newest snapshot. The overshoot and throughput bounds are in
the_three_scenarios_keep_their_rates, #[ignore]d.
2026-09-23 11:46:45 +00:00
acamilo
3c614c87f4 docs: flybus.md amendments for the feed on the bus, its sizing and its store lifecycle
The two pending decisions, taken for the feed with EDGE-01 and dated:
sizing from the measured 122,367-byte snapshot (the frame is 92,160 bytes,
not the 1.2 MB the list assumed) with the worst case of seven stuck latest
subscribers at about 3 MB inside a 32 MiB tmpfs store; and the lifecycle,
flysim owning the router under /run/fly/bus and starting first, the edge
After= and Requires= it and reconnecting by itself across a crash. Plus the
design as built, the tour's pointer, the example config and the flybus
README's no-longer-true line.
2026-09-23 11:41:28 +00:00
acamilo
d3f98ae4f1 flysim: stop the bus runtime under the publisher at shutdown, not the router first
Shutting the router down first raced the last publish and logged a refusal
on every clean stop. The publisher ends on its own when the watch sender goes;
the edge sees the socket close either way.
2026-09-23 11:41:28 +00:00
acamilo
3d9a08d0be infra: flyedge.service, off by default, and the feed bus through build, deploy and watchdog
flyedge.service runs /opt/fly/current/fly-edge After= and Requires=
flysim.service, with its metrics on loopback :9102 and a
ConditionPathExists so a release without the binary leaves it inactive. It is
in no target and 07-enable.sh does not enable it; the header has the switch
and the way back.

build-flysim.sh also builds fly-edge beside the flysim binary and
package-release.sh ships it when present. 05-deploy.sh writes
FLY_FEED_VIA (default direct) into fly.env, flysim.service names
FLY_BUS_DIR=/run/fly/bus and tmpfiles creates it. Watchdog check 2 reads the
feed counters from whoever serves the feed: flyedge when fly.env says bus.
lint.sh holds all of that, and drives check 2's choice against a fixture.
2026-09-23 11:41:28 +00:00
acamilo
34c7a56b25 tests: the edge writes the direct feed's bytes, and a stalled consumer never lags the loop
parity.rs replays the four committed stage fixtures whose headers the Rust
producer can read (macros, shop, center, bigpad; 400 snapshots each, all of
them with FLY_EDGE_PARITY_ALL=1) into one watch slot served both ways at once,
recorded by a stage, a bridge and a frame-only client per path: headers equal
without wall times, attachments byte-equal and equal to the fixture's, and
the whole messages byte-equal. FLY_EDGE_PARITY_OUT writes the recordings as
.flyfeed files. Also: a header past the envelope limit, and the edge dropping
its clients, unbinding and coming back across a router restart.

stall.rs runs flysim's Pacer at 60 Hz publishing full-size snapshots at 30 Hz
against three stages that stopped reading, a bus subscriber that hoards every
delivery, and no subscriber at all: pacer lag 0, no slow watch send, no
refused publication, a bounded store, and a healthy client that stays current.
2026-09-23 11:41:28 +00:00
acamilo
d324ec825a fly-edge: serve the feed WebSocket from the feed bus
A new workspace binary. It reads flysim's own configuration (same env file,
same FLY_FEED_BIND, FLY_BUS_DIR and idle cadence), subscribes to
fly.feed.snapshots as fly-edge with one latest slot and one delivery in
flight, turns each publication back into a Snapshot with feedbus::receive and
serves it through flysim's feed::router, so hello, wants, drop-oldest and the
2 Hz idle header are flysim's code and the bytes are flysim's bytes.

The port is bound only once the first snapshot has arrived, and when the bus
goes away every client is dropped and the port unbound, which is what a
stopped flysim looks like to the stage; then it reconnects every 500 ms.
FLY_EDGE_METRICS_ADDR serves /metrics (fly_frames_sent_total and
fly_feed_clients under their flysim names, plus fly_edge_*) and /healthz.
2026-09-23 11:41:28 +00:00
acamilo
d3fa7908ec flysim: FLY_FEED_VIA=bus publishes every snapshot on an embedded flybus router
feed.via (FLY_FEED_VIA, default direct) and feed.bus_dir (FLY_BUS_DIR,
default /run/fly/bus). In bus mode flysim does not bind the feed port: it
starts a router on its own two-thread runtime with a closed policy (flysim
may publish fly.feed.snapshots, fly-edge may only subscribe to it), listens
for the edge on <bus_dir>/edge.sock, and a publisher task copies each snapshot
out of the watch slot onto the latest-retained topic: frame, audio and spikes
as sealed artifacts, the header as the envelope payload, or as a header
artifact past 48 KiB. The sim thread still only writes its watch slot, so a
slow bus costs snapshots on the bus and never a frame of the loop.

feedbus holds both halves of the encoding, publish and receive, and the
limits sized for the real 122,367-byte snapshot. Two counters are new:
fly_bus_published_total and fly_bus_publish_failures_total.
2026-09-23 11:41:28 +00:00
acamilo
acf7c2ebb8 flysim: the feed server takes a FeedState, so another process can run it
feed::router needed the whole AppState for three things: the watch slot, the
feed counters and the idle cadence. It now takes exactly those, and Shared's
Metrics sits behind an Arc so the counters can be handed over. Nothing about
what the feed writes changes.
2026-09-23 11:41:28 +00:00
acamilo
3eb82d7144 a warp's tear deals no pad
wCurMap names the new map thirty-two frames before the header, the
coordinates and the warp table follow it, while the screen fades, and
nothing sets the joypad bits until the fade ends. The seam read
"map 54 at (16, 17)" -- Pewter City's doormat under the gym's id -- as
an overworld, dealt a pad, and a walk started there planned over the
wrong map; what it aimed at and the tile it left went into the ledgers
under the new map's id. Live, GO OUT started and finished in 0.05 s.

The driver reads a tear as the map byte having changed while the fly
still stands on a warp of the loaded table that leads to the map the
byte names (a doormat's LAST_MAP under a town's id included), deals it
as Unknown with an empty pad, and records no ground from it. Teleport
pads do not change the map byte, so they are never a tear; a tear is
bounded at TEAR_FRAMES all the same.
2026-09-23 11:26:49 +00:00
acamilo
5fd16536db a battle decided is the cartridge's
Between a trainer's challenge closing and the battle screen the
transition runs 219 frames with every joypad and script bit clear, so
the scene read overworld and a pad was dealt: a walk toward the leader
pressed into the animation, gave up after three refused steps and put
him in the blocked ledger, and the trainer's conversation read as over.

wCurOpponent is set when a battle is decided and cleared by EndOfBattle
with wIsInBattle. It is not in the generated table; it is the byte
between wIsInBattle's flag byte and wBattleType, both neighbours
checked against the table, and controllable() reads it.
2026-09-23 11:26:49 +00:00
acamilo
484cc075cc the rung's people are in the room when the screen does not show them
CheckSpriteAvailability writes $ff into the image index of a sprite
outside its window, and state::npcs reports what is drawn. From the
Pewter Gym's doormat that is the guide alone, already talked to, so the
rung's list was empty: GO OBJECTIVE had nothing to aim at and GO OUT,
withheld only while the rung's person is in the room, was the pad.
Outside, GO OBJECTIVE walked back in. BROCK was twelve rows up.

state::offscreen_npcs reports the sprites the cartridge hides only for
being outside the window, read from bytes the seam already has, and
objective_targets reads them for a person. Nothing else does: a sprite
out of the window may be a toggleable object switched off, and GO NPC,
TALK and objects keep what is drawn.

Facing any of the rung's people is the arrival: with three in a gym,
leaving out only the one ahead walked GO OBJECTIVE between the leader
and the trainer.
2026-09-23 11:26:49 +00:00
acamilo
552428a7be survey: the route probe reads the room on the fly's Nth arrival on a map
FLY_PROBE_CATCH_MAP and FLY_PROBE_CATCH_ENTRIES stop the route survey
forty frames after the fly's Nth arrival on a map (the first frames on a
new map byte still carry the old map's warps), and the dump lists every
person the macros can see with its talked, blocked and reached entries.
Row 58's pad was one door in and one door out, and what it needed read
was the room on the far side of the door.
2026-09-23 11:26:49 +00:00
acamilo
174dc7eabd docs: v0.5.4 status, and the unstick rule
Some checks are pending
ci / node 22 (test + typecheck) (push) Waiting to run
ci / rust stable (cargo test --workspace --release) (push) Waiting to run
ci / infra/tests/lint.sh (push) Waiting to run
ci / playwright apps/stage (allowed to fail) (push) Waiting to run
2026-09-23 11:26:32 +00:00