> r24: Y = pass/outlet only with your own puck or your own goalie's; otherwise Y takes the keeper. r22 added the B skater switch for all pads.

# Four-pad goalie controls — 2026-10-26-r19

> r20 update: Players is now editable in season playoffs and saved per season,
> including three/four players across cold boots. See [SEASON_PLAYERS.md](SEASON_PLAYERS.md).

With 4-Way Play and teammates on the same side, your goalie stays under AI
control when he catches the puck. A pass-button B tap requests a native pass
to the requesting pad's current skater, including pads 3 and 4. A request can
wait through the catch/dive recovery. Y explicitly takes the goalie; Y with
your own puck retains its pass behavior. Another pad cannot steal an already
controlled goalie. Holding/mashing B through the outlet does not switch away
from the intended receiver. Without a request he holds, retaining the native
possession timer and eventual whistle.

Both RetroArch ports must use **MD Joypad 6 Button + 4-WayPlay (1281)**, then
Restart so the game detects the adapter on port 2. Choose Three/Four - Teammates
on Players. jdc's existing r18 remap already used 1281 on both ports and is
carried unchanged. Bodin's existing 513 remap is also carried unchanged; enable
the adapter there when actually using three/four pads. r22: the native adapter does NOT switch skaters in co-op (held-B switch off); fourgoalie.s `fast` now does the B switch for all four pads.
 The ordinary two-pad switch/Y routines remain
byte-identical; CPU and opposing-human goalie behavior retains native paths.

## Cause and implementation

The native adapter does not give the skater input handler D4=4/6 for P3/P4.
It calls the P1/P2 handler with D4=0/2, temporarily swapping C320/C328 (P3)
or C322/C32A (P4), then copying the changed control back to C324/C326.
C318 is -1 for ordinary pads and 0/1 during P3/P4 input. Persistent requests
must use D4+4 during that bank swap, while native assignment C0BC/C0DA must
retain D4=0/2. The hardware reader instead identifies physical pads through
BF06 bits 7:6. Goalie settings D05A/D05C are indexed by team side.

`experimental/fourgoalie.s` at **2EA600**, 1098 bytes, exports six four-byte
entries: early B (+0), legacy B (+4), hardware Y/B reader (+8), Y handler
(+12), cooperative-side predicate (+16), and new-game reset (+20).
B1B6/B2DA, 1FFC02 and B258 dispatch here. Without an adapter they tail-jump
the preceding switch.s/ygoalie.s routines. Cooperative means at least one
human and every active side word equal to the same valid side, 1 or 2.

Raw hardware polling latches a short B request when the AI goalie already
owns the puck, or a loose puck is within 24 pixels of him. This prevents a
one-frame tap being overwritten before the next skater update. It validates
the human skater and keeper, excludes pause/menu/replay, faceoff, penalty shot,
shootout and special states, and publishes the first valid request atomically.
Y clears a queued outlet. Consumer/catch cancellation also masks interrupts
around pending-owner updates so hardware polling cannot lose a fresh request.

`outlet.s` (2FDC00, 836 bytes) accepts physical pad offsets 0/2/4/6, looks up
all four side words and uses the existing synchronous native BC64 target and
direction override. `goalie_ai.s` (2FE400, 132 bytes) keeps an AI keeper for
three/four teammates and native assignment for opposing humans.

## Two additional bugs found during play/restart testing

**Wrong player count after changing series.** TM is session RAM. A Four-player
Mini could leave TM=4 while loading a Light slot whose visible Players was
One. mtap.s previously preferred TM, starting four humans despite the label.
League side assignment now follows Players 5/6 directly; TM is used only in
the season-playoff view. Standalone native modes do not inherit it. New adapter
games reset the transient goalie input latches. mtap.s: 2EA400, 256 bytes.

**Extra humans during shootouts.** The existing shootout saved/cleared only
P1/P2. P3/P4 could remain active with only two native actors. shootout.s now
saves their side words, clears them throughout the attempts and restores them
after the result. The existing P1/P2 alternating-shooter scheme remains.
Module: 2AC000, 920 bytes. Native playoff bracket/password/statistics checks
pass for both best-of-seven Full and single-game Light/Mini.

Historical r19 limitation: TM was RAM-only and season-playoff Players was
locked. **Fixed in r20**: counts are saved per slot and Players is selectable
in Continue Playoffs. The goalie implementation below remains unchanged.

## RAM and compatibility

| Address | Use |
|---|---|
| FFF058..FFF05F | Existing synchronous native outlet target/passer/signature |
| FFF064..FFF067 | Existing pending request: physical offset+1 = 1/3/5/7, goalie object |
| FFF0B2..FFF0B9 | Four Y requests, even byte offsets 0/2/4/6 |
| FFF0BA..FFF0C1 | Four held-Y states, even byte offsets |
| FFF0C2..FFF0C9 | Four B-consumption words |
| FFF0CA..FFF0CB | Initialisation signature 3447 |
| FFF0CC..FFF0CF | P3/P4 side words during shootout only |

FFF0B0 is still the critical-shot transient; FFF0D2 remains the native season
playoff flag. No SRAM format change. Claude's r2–r18 features, portraits,
crit/sound/trail, momentum, menus, Top 100, independent seasons and adapter
hardware-flag fix are retained. Tests use copied batteries only.

## Verification and reproduction

Private evidence/backup directory:
`~/.config/retroarch/roms/claude/work/four-pad-goalie-20261026-111041/`.
The user explicitly requested a backup for this change: `backup-r18.zip`,
68 entries, ZIP integrity and all manifest hashes verified. It includes the
r18 ROM, sources/notes/metadata, user save/state/remap; it is not a public
duplicate release. `backup-manifest.json` records original file hashes.

All tests run headlessly in the installed Genesis Plus GX libretro core:

- **88 adapter goalie cases**, both teams, valid 1/2/3/4-pad configurations,
  every requesting pad, early/single-frame taps, recovery/diving, held/mashing
  buttons, simultaneous/ordered requests, explicit Y and no stealing. Checks
  real native catch/pass, physical pad ID, receiver, outgoing vector, stable
  control ownership and cleared transient state. Private observation wrappers
  mark native temporary input-bank swaps; production contains no probes.
- **41 ordinary goalie cases**, including CPU, Auto, Manual and recovery.
- **7 two-match lifecycles = 14 games**, Full/Light/Mini starting with both
  three and four humans, changing 3↔4 for the second match. Fourteen cold
  boots check progress, real P3/P4 movement, extra-pad pause/B/resume and
  forward/reverse menu circles. Warm Four→One slot regression passes.
  Six cases shorten the test with a puck crossing the native OT goal line.
  The seventh plays two complete Mini games without changing clocks/goals:
  28 requests, 27 native keeper passes, 17 physical receptions.
- **50 OT/shootout cases**: 18 ordinary-pad cases and 16 each with three/four
  teammates, covering regulation→10-minute OT→shootout, early clinch, sudden
  death, roster wrap, unchanged normal stats, save checksums and all playoff
  formats. P3/P4 are inactive during attempts and restored at the result.
- Existing regressions: slots **20**, adapter navigation **14/266 checks**,
  completed-season flows **9/108**, club labels **18/240**, native menu font.

Public test sources are `verify_four_goalie.py`, `four_goalie_hook.py`,
`verify_four_lifecycle.py`, `four_lifecycle_hook.py`, `four_pad_reboot_hook.py`,
`verify_shootout.py` and `testcases/shootout_hook.py`, all in experimental/.
The native four-pad capture is `four-native.state`; it was captured by cold
booting r18, selecting Four using controller input and starting a real match.
Historical dive captures provide object/puck RAM only, retaining fresh native
adapter hardware and input state. Fixture paths are private; scripts take
explicit inputs and never alter installed saves.

```sh
python3 tools/experimental/build_test.py "download/NHL 2026 - 94 Edition v5.1W.bin" /private/release.bin --version 2026-10-26-r19
python3 tools/experimental/verify_four_goalie.py /private/release.bin /private/goalie-tests --state /private/four-native.state --fixtures-root /private/work
python3 tools/experimental/verify_four_lifecycle.py /private/release.bin /private/lifecycle --seed /private/preferences.srm --natural
python3 tools/experimental/verify_shootout.py /private/release.bin /private/shootout --seed /private/preferences.srm --playoffs /private/three-playoffs.srm --teammates 4
```

Do not inject fixture side mappings during a native P3/P4 bank swap: the
native stack will restore its saved word. Use C318=-1 or observe the complete
bank swap. Absolute native BTST tests a byte: C2EC/C2F2 flags must be read
from that addressed byte, not the low byte of a big-endian word.

Two independent full builds are byte-identical. Guarded r18→r19 diff: **2870
bytes**, confined to version/checksum, the four dispatch hooks and the five
listed modules. IPS reconstructed against the original base: **230211 bytes**,
verified. Released ROM SHA256:
`7262f8e4a39d6970b660e24803f1d7212b29aed26a933202296b788aed8d9ec6`;
CRC32 `D82B8F78`. See release.json for current filenames. r19 replaces r18 in
canonical/Downloads/site and Bodin's existing playlist item, carrying matching
saves, states, configs, remaps and histories by unchanged content hashes.
