docs: macros.md 12.23, macros-wram.md section 13, the row 60 audit
This commit is contained in:
parent
07f88c443a
commit
4591e7907a
3 changed files with 161 additions and 2 deletions
|
|
@ -953,3 +953,45 @@ where that layout puts them, so the byte is `wBattleType - 1` = `$d059`, asserte
|
||||||
neighbours in `scene/tests.rs`. Measured on the cartridge in the Pewter Gym: `$00` in the
|
neighbours in `scene/tests.rs`. Measured on the cartridge in the Pewter Gym: `$00` in the
|
||||||
overworld, `$cd` (`OPP_JR_TRAINER_M`) from the last box of the trainer's challenge through the
|
overworld, `$cd` (`OPP_JR_TRAINER_M`) from the last box of the trainer's challenge through the
|
||||||
219-frame transition and the battle. `controllable` reads it as zero.
|
219-frame transition and the battle. `controllable` reads it as zero.
|
||||||
|
|
||||||
|
## 13. What a move will do: the move table and the bytes its effect reads (2026-09-23, `docs/design/macros.md` 12.23)
|
||||||
|
|
||||||
|
Row 60. Seven addresses, resolved by `tools/resolve_wram.py` and emitted into `symbols.rs`; the
|
||||||
|
tool now follows the decomp's `const` and `_RS` counters and a struct macro's field labels, which
|
||||||
|
is what reaches the `battle_struct` fields, and it re-derives 77 of 81 pinned addresses with no
|
||||||
|
disagreement.
|
||||||
|
|
||||||
|
| symbol | address | what reads it |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `wPlayerMonStatMods` | `$cd1a` | six stages, ATTACK DEFENSE SPEED SPECIAL ACCURACY EVASION; 1 is -6, 7 normal, 13 is +6 |
|
||||||
|
| `wEnemyMonStatMods` | `$cd2e` | the same for the enemy |
|
||||||
|
| `wEnemyMonStatus` | `$cfe9` | the enemy's status byte (`battle_struct` +4) |
|
||||||
|
| `wEnemyMonType1` | `$cfea` | and `Type2` after it |
|
||||||
|
| `wEnemyMonAttack` | `$cff6` | the enemy's modified ATTACK DEFENSE SPEED SPECIAL, big-endian words |
|
||||||
|
| `wBattleMonAttack` | `$d025` | the same for the fly's Pokémon |
|
||||||
|
| `wEnemyBattleStatus2` | `$d068` | bit 1 Mist, 4 substitute, 5 must recharge |
|
||||||
|
|
||||||
|
**The move table is ROM.** `Moves` opens `SECTION "Battle Engine 7"`, which `layout.link` places
|
||||||
|
first in bank `$0E`, so it is `$0E:$4000`, six bytes a row in move-id order from `POUND`:
|
||||||
|
animation (the move id itself), effect, power, type, accuracy, PP. `state::move_data` reads a row
|
||||||
|
through `MemoryReader::read_rom`, the cartridge image, and refuses a row whose first byte is not the
|
||||||
|
id asked for. Measured from the row-60 checkpoint: TACKLE (`$21`) effect `$00` power 35, TAIL
|
||||||
|
WHIP (`$27`) effect `$13` (`DEFENSE_DOWN1_EFFECT`) power 0.
|
||||||
|
|
||||||
|
**`state::move_without_effect`** answers only refusals decided before the roll, for a move with no
|
||||||
|
power, from `engine/battle/effects.asm` and `MoveHitTest`:
|
||||||
|
|
||||||
|
- `*_UP1` / `*_UP2`: the user's stage is 13, or (ATTACK..SPECIAL) the stat is 999;
|
||||||
|
- `*_DOWN1` / `*_DOWN2`: the target has Mist or a substitute, its stage is 1, or (ATTACK..SPECIAL)
|
||||||
|
the stat is 1 -- `StatModifierDownEffect` restores the stage and prints "Nothing happened!"
|
||||||
|
then, so a low-level target reaches it before -6;
|
||||||
|
- `SLEEP_EFFECT`: any status, unless the target must recharge;
|
||||||
|
- `POISON_EFFECT`: a substitute, any status, or a Poison type;
|
||||||
|
- `PARALYZE_EFFECT`: any status, or an Electric move against a Ground type.
|
||||||
|
|
||||||
|
`None` outside a battle, without a cartridge, or with a stage byte outside 1..13.
|
||||||
|
|
||||||
|
**Not covered** (the reading would be the same kind, and nothing early in the game reaches it):
|
||||||
|
Confuse Ray and Supersonic on a confused target, Leech Seed on a seeded or Grass target, Focus
|
||||||
|
Energy, Mist, Reflect and Light Screen already up, Disable on a disabled target, and a damaging
|
||||||
|
move the type chart makes "doesn't affect" (the chart is another ROM table).
|
||||||
|
|
|
||||||
|
|
@ -1505,6 +1505,40 @@ has a person to walk to where it had none, `GO OUT` is withheld by 12.5's own ru
|
||||||
frames that were never the fly's deal nothing. The decoder, the reward catalog, the adapter
|
frames that were never the fly's deal nothing. The decoder, the reward catalog, the adapter
|
||||||
version, the roles and the compatibility string are untouched.
|
version, the roles and the compatibility string are untouched.
|
||||||
|
|
||||||
|
### 12.23 A move the cartridge answers with nothing is not dealt beside one it does not (2026-09-23, row 60)
|
||||||
|
|
||||||
|
Live on v0.5.5, early game after the reset to milestone 1: Route 1, Squirtle L5 (TACKLE, TAIL
|
||||||
|
WHIP) against a wild Pidgey, "Nothing happened!" on the screen. Since the reset `MOVE 2` 183
|
||||||
|
times and `MOVE 1` once; the last reward 25 brain minutes before the checkpoint, one wild win in
|
||||||
|
the whole run. Check 10 flagged `unrewarded` (1,600 decisions, no reward event, no new ground on
|
||||||
|
two probes), which is right, and it is unchanged.
|
||||||
|
|
||||||
|
- **The pad was at fault, not only the choice.** `MOVE n`'s precondition was "the slot holds a
|
||||||
|
move with PP", so TAIL WHIP stayed on the pad after it had walked the Pidgey's DEFENSE to the
|
||||||
|
point the cartridge refuses it. `StatModifierDownEffect` answers "Nothing happened!" when the
|
||||||
|
stage is already -6 **or the stat itself is already 1**, restoring the stage. The checkpoint is
|
||||||
|
the frame Squirtle fainted to a Pidgey L3 at DEFENSE -6 (stat 2); in the next battle the stat
|
||||||
|
reached 1 at -5. From there the pad dealt `MOVE 1, MOVE 2, RUN` and the fly pressed `MOVE 2`
|
||||||
|
until Squirtle fainted, woke at home and walked back: every battle lost, one wild win in the
|
||||||
|
run. The readout's favourite being `MOVE 2` is the fly's; a button that can do nothing at all
|
||||||
|
being on the pad is a macro that knows nothing about its own effect, section 12.2's trap.
|
||||||
|
- **What a move does is the cartridge's, read the same way for every move.** `state::move_data`
|
||||||
|
reads the move's row of `Moves` from the cartridge image (`$0E:$4000`, each row checked against
|
||||||
|
its own id) and `state::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 or 999, Mist or a substitute in
|
||||||
|
front of a stat-lowering move, a sleep, poison or paralysis move against a target that already has
|
||||||
|
a status, is Poison type, or is Ground type to an Electric move. No move is named; a miss is a
|
||||||
|
roll and is not answered. `macros-wram.md` section 13 has the bytes.
|
||||||
|
- **It is PP's rule.** A move the cartridge answers with nothing is not dealt beside one it does
|
||||||
|
not, exactly as a spent move is not (12.6, 12.8), and `MOVE 1` over the menu stops being FIGHT's
|
||||||
|
backstop only in that case. When no move would do anything the moves stay as PP deals them:
|
||||||
|
taking the last ones away would leave an open list whose only button is `BACK`, 12.11's pair,
|
||||||
|
and a turn that ends on "Nothing happened!" still ends. RUN, ITEM and SWITCH are untouched.
|
||||||
|
|
||||||
|
Nothing is ranked, weighted or pressed for the fly: a button leaves the pad while it cannot change
|
||||||
|
anything and comes back when it can (a new battle resets the stages). The decoder, the reward
|
||||||
|
catalog, the adapter version, the roles and the compatibility string are untouched.
|
||||||
|
|
||||||
## 13. Shops and Pokémon Centers (the operator, 2026-09-17: "refactor the shop macros. make it a
|
## 13. Shops and Pokémon Centers (the operator, 2026-09-17: "refactor the shop macros. make it a
|
||||||
## priority to visit the shop at least once per area; make shop macros item purchases. same
|
## priority to visit the shop at least once per area; make shop macros item purchases. same
|
||||||
## for the Pokécenter. heal should be a macro.")
|
## for the Pokécenter. heal should be a macro.")
|
||||||
|
|
|
||||||
|
|
@ -2757,5 +2757,88 @@ route survey above is the reproduction; the hunt is reported, not smoothed.
|
||||||
- `cargo clippy --all-targets`: **0 warnings**.
|
- `cargo clippy --all-targets`: **0 warnings**.
|
||||||
- `npm test` 663 passed; `npm run typecheck` clean.
|
- `npm test` 663 passed; `npm run typecheck` clean.
|
||||||
- `infra/tests/lint.sh`: ALL CHECKS PASSED, check 10's two new cases and the de-PII guard included.
|
- `infra/tests/lint.sh`: ALL CHECKS PASSED, check 10's two new cases and the de-PII guard included.
|
||||||
- `flysim --print-compatibility`: **648 bytes, sha256 `4929f340...9ebd9`**, byte-identical to the
|
- `flysim --print-compatibility`: byte-identical to the base on both bases this branch has had:
|
||||||
base. Decoder, reward catalog, adapter version and roles untouched.
|
648 bytes, sha256 `4929f340...9ebd9` on v0.5.5 (`7784a9d`), and 648 bytes, `8ce67b97...a8f68`
|
||||||
|
on `main` after the engagement rewards (adapter v7). Decoder, reward catalog, adapter version
|
||||||
|
and roles untouched.
|
||||||
|
|
||||||
|
## 2026-09-23, row 60: TAIL WHIP after it stopped working
|
||||||
|
|
||||||
|
### What was live
|
||||||
|
|
||||||
|
Map 12 (Route 1), rank 9, v0.5.5, after the operator reset the run to milestone 1: Squirtle L5
|
||||||
|
(TACKLE, TAIL WHIP) in wild battles with Pidgey, "Nothing happened!" on the screen. Since the reset
|
||||||
|
`MOVE 2` 183 starts and `MOVE 1` one; the last reward 25 brain minutes before the checkpoint,
|
||||||
|
1,600 decisions since, one wild win in the whole run. Check 10 flagged `unrewarded` -- correctly:
|
||||||
|
100+ decisions, no reward event, no new ground on two probes. It is unchanged.
|
||||||
|
|
||||||
|
### The survey: the pad on the fly's own turn
|
||||||
|
|
||||||
|
The route probe from the checkpoint (`FLY_PROBE_CATCH=route`, `FLY_PROBE_PREFER="MOVE 2"`, the
|
||||||
|
live readout's favourite; 72,000 frames) now prints the battle bytes on every pad change. The
|
||||||
|
checkpoint is the frame Squirtle fainted: Pidgey L3, DEFENSE stage 1 (-6), DEFENSE 2, TAIL WHIP's
|
||||||
|
row `$27`: effect `$13` power 0. In the next battle DEFENSE reached **1 at stage -5**, which
|
||||||
|
`StatModifierDownEffect` refuses as well. From then on the own-turn pad was `MOVE 1, MOVE 2, RUN`
|
||||||
|
over the menu and `BACK, MOVE 1, MOVE 2` over the list: **every own-turn pad with TAIL WHIP
|
||||||
|
refused dealt `MOVE 2`** (72 of 72; a 73rd frame was a list with no placeable cursor, `NEXT`
|
||||||
|
alone), TACKLE beside it every time. `MOVE 1` was never missing; `MOVE 2` was a
|
||||||
|
button that could change nothing.
|
||||||
|
|
||||||
|
| # | trap | trigger | test | fix, or why it is left |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 60 | `MOVE n` is bound by PP alone, so a move the cartridge refuses ("Nothing happened!", "didn't affect") stays on the pad beside one that works | any stat move at its stage or stat limit, any status move at a target it cannot affect; Route 1, TAIL WHIP at a Pidgey's DEFENSE 1, every battle lost | `a_move_the_cartridge_answers_with_nothing_is_off_the_pad_beside_one_it_does_not`, `a_spent_move_and_a_move_without_effect_leave_the_one_that_works`, `with_no_move_that_does_anything_the_moves_stay_as_pp_deals_them`, six `state` tests, `tail_whip_at_its_limit_is_not_dealt_and_a_route_one_battle_is_won` (ROM) | **fixed**: `state::move_data` reads the move's row of `Moves` (`$0E:$4000`) from the cartridge image; `state::move_without_effect` answers the refusals decided before the roll (stage 1/13, stat 1/999, Mist or substitute, a statused, Poison or Ground target); the palette treats such a move as it treats a spent one. `docs/design/macros.md` 12.23, `macros-wram.md` section 13 |
|
||||||
|
|
||||||
|
### Before and after
|
||||||
|
|
||||||
|
The ROM-gated run, 72,000 frames (20.1 brain minutes) from the checkpoint, the real palette
|
||||||
|
driven with `MOVE 2` preferred and a uniform choice otherwise; base is this branch with the
|
||||||
|
palette commit reverted:
|
||||||
|
|
||||||
|
| measure | base | branch |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| own-turn frames with a refused move beside a useful one | 2,657 | 568 |
|
||||||
|
| ... of them dealing the refused move | **2,657** | **0** |
|
||||||
|
| battles ended / won | 12 / **0** | 14 / **3** |
|
||||||
|
| battles ended with the fly's Pokémon fainted | 12 | 8 |
|
||||||
|
| battle length, frames, median / max | 4,970 / 8,994 | 3,594 / 4,793 |
|
||||||
|
| `MOVE 2` / `MOVE 1` / `RUN` starts | 94 / 0 / 0 | 56 / 5 / 4 |
|
||||||
|
|
||||||
|
The route survey, same driver and checkpoint: base never leaves Pallet Town, Red's house and
|
||||||
|
Route 1 (637 tiles at the end); the branch reaches Viridian City, its Pokémon Center and mart and
|
||||||
|
Route 2 (748).
|
||||||
|
|
||||||
|
The survey itself, same driver: own-turn pads dealing a refused TAIL WHIP 72 -> **0** (of 11 at
|
||||||
|
the limit); `MOVE 2` / `MOVE 1` / `RUN` done 94 / 0 / 0 -> 56 / 4 / 4; maps with a macro done 4 -> 10.
|
||||||
|
|
||||||
|
**The real-brain trap hunt was not run to the end.** Both 20-minute arms (`trap_hunt` now reports
|
||||||
|
payouts by kind, battle lengths and wins, and `MOVE n` starts on a move without effect) were
|
||||||
|
started from the checkpoint and stopped after 1 h 48 min wall at about 19 CPU-minutes each: the box
|
||||||
|
sat at load 25-40 and a stub arm ticks the same brain. The ROM-gated run and the survey above are
|
||||||
|
the before/after; they drive the real palette with the live readout's measured preference instead
|
||||||
|
of the brain, which is the deviation.
|
||||||
|
|
||||||
|
### Residuals, named rather than worked around
|
||||||
|
|
||||||
|
- **The fly still spends TAIL WHIP while it works.** Six presses at stage 7 to 1 are the fly's
|
||||||
|
choice, and a Squirtle at 8/20 can faint doing it; eight of fourteen battles on the branch
|
||||||
|
still ended that way. What changed is that the seventh is not on the pad.
|
||||||
|
- **Not covered, same kind, nothing early reaches it:** Confuse Ray and Supersonic on a confused
|
||||||
|
target, Leech Seed on a seeded or Grass target, Focus Energy, Mist, Reflect and Light Screen
|
||||||
|
already up, Disable on a disabled target, and a damaging move the type chart makes "doesn't
|
||||||
|
affect" (the chart is another ROM table). `macros-wram.md` section 13.
|
||||||
|
- **With no move that would do anything, the moves stay as PP deals them.** Taking them away would
|
||||||
|
leave an open list with `BACK` alone (12.11); a turn that ends on "Nothing happened!" still ends.
|
||||||
|
|
||||||
|
### Gates
|
||||||
|
|
||||||
|
- `cargo test --release -p flybrain-gb` with `FLY_ROM`: 426 + 27 passed, 0 failed.
|
||||||
|
- `cargo test --release -p flysim --no-fail-fast` with `FLY_ROM` and the row-60 checkpoint:
|
||||||
|
all passed but `integration::the_service_streams_takes_sugar_checkpoints_and_resumes_after_being_killed`,
|
||||||
|
the known load failure (`total` 1 against 38, or the feed at 14-18 Hz, on a box at load 30),
|
||||||
|
failing the same two ways on the base.
|
||||||
|
- `cargo clippy --workspace --all-targets -- -D warnings`: clean.
|
||||||
|
- `npm test` 663 passed; `npm run typecheck` clean; `infra/tests/lint.sh` ALL CHECKS PASSED.
|
||||||
|
- `flysim --print-compatibility`: byte-identical to the base on both bases this branch has had:
|
||||||
|
648 bytes, sha256 `4929f340...9ebd9` on v0.5.5 (`7784a9d`), and 648 bytes, `8ce67b97...a8f68`
|
||||||
|
on `main` after the engagement rewards (adapter v7). Decoder, reward catalog, adapter version
|
||||||
|
and roles untouched.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue