| Three independent season saves. Active legacy8KB + three8KB snapshots.
| SRAM header expanded to odd $200001..$20FFFF. Native helpers remain8KB-only.
| Metadata in formerly unused logical $1EA0..$1EF5, covered by native checksum.
| +0 init/migrate; +4 select(D0=1/2/3, returns0/-1); +8 suspend; +12 restart; +16 prelist; +20 native save hook.
    .include "features.inc"
    .equ TAG,0xFFFFF068
    .equ MODE,0xFFFFF06A
    .equ VIEW,0xFFFFF06C
    .equ META,0xFFFFE800
    .equ HEAD,0xFFFFE850
    .equ MAGIC,0x53335356          | S3SV
    .equ METAOFF,0x1EA0
    .equ EXTRA,0x1EAC             |66bytes D046..D087
    .equ MAP,0xA130F1
    .text
    .globl _start
_start:
    bra.w init
    bra.w select
    bra.w suspend
    bra.w restart
    bra.w prelist
    bra.w ctxhook
init:
    cmpi.w #0x5333,TAG
    beq.w init_done
    movem.l %d0-%d7/%a0-%a6,-(%sp)
    move.w %sr,-(%sp)
    ori.w #0x700,%sr
    move.b #1,MAP
    .if WITH_TRANSFERS
    jsr 0x256004                   | Recover committed reset before directory/migration.
    .endif
    bsr.w readmeta
    cmpi.l #MAGIC,META
    bne.s migrate
    cmpi.w #1,META+4
    bne.s migrate
    move.w META+6,%d0
    subq.w #1,%d0
    cmpi.w #2,%d0
    bhi.s migrate
    cmpi.w #7,META+8
    bhi.s migrate
    move.w META+6,MODE
    move.w #1,VIEW
    tst.w META+10
    bne.w initialized
    | Outside the season view, the selected season's snapshot is authoritative.
    move.w MODE,%d0
    move.w %d0,%d6
    bsr.w bankptr
    bsr.w validbank
    tst.w %d0
    bne.s blocked_resume       | never replace current data with an invalid snapshot
    bsr.w restore              | boot resumes the season view: its own playoff context
    bra.w initialized
blocked_resume:
    clr.w VIEW
    bra.w initialized
migrate:
    moveq #1,%d5
    move.l #0x1000,%d0
    moveq #16,%d1
    lea HEAD,%a0
    bsr.w read
    cmpi.l #0x5345534E,HEAD
    bne.s migrated
    tst.w HEAD+6
    beq.s empty_legacy
    .if MINI_GAMES != 8
    cmpi.w #MINI_GAMES,HEAD+10
    beq.s legacy_mini
    .endif
    cmpi.w #8,HEAD+10
    beq.s legacy_mini
    cmpi.w #31,HEAD+10
    bne.s legacy_running
    moveq #2,%d5
    bra.s legacy_running
legacy_mini:
    moveq #3,%d5
legacy_running:
    clr.w HEAD+12              | browsing New in an old ROM must not discard its saved season
    move.l #0x1000,%d0
    moveq #16,%d1
    lea HEAD,%a0
    bsr.w write
    bra.s migrated
empty_legacy:
    move.w HEAD+12,%d0
    subq.w #1,%d0
    cmpi.w #2,%d0
    bhi.s migrated
    addq.w #1,%d0
    move.w %d0,%d5
migrated:
    move.l #MAGIC,META
    move.w #1,META+4
    move.w %d5,META+6
    clr.w META+8
    move.w #1,META+10
    move.w %d5,MODE
    move.w #1,VIEW
    bsr.w writemeta
    bsr.w checksum
initialized:
    .if WITH_TRANSFERS
    jsr 0x256010
    tst.l %d0
    beq.s reset_recovery_done
    move.w %d0,MODE
    move.w %d0,META+6
    move.w #1,VIEW
    move.w #1,META+10
    bsr.w clearseason
    bsr.w writemeta
    bsr.w checksum
    jsr 0x25600C
reset_recovery_done:
    .endif
    tst.w VIEW
    beq.s count_ready
    jsr 0x2EB000                     | optional per-slot player-count extension; mapper is enabled
count_ready:
    move.w #0x5333,TAG
    move.b #0,MAP
    move.w (%sp)+,%sr
    movem.l (%sp)+,%d0-%d7/%a0-%a6
    .if WITH_TRANSFERS
    jsr 0x236028                   | Initial boot now has committed MODE/VIEW and mapperOff.
    .endif
init_done:
    rts

select:
    bsr.w init
    movem.l %d1-%d7/%a0-%a6,-(%sp)
    move.w %sr,-(%sp)
    ori.w #0x700,%sr
    move.b #1,MAP
    move.w %d0,%d6              | destination format
    subq.w #1,%d0
    cmpi.w #2,%d0
    bhi.w failed
    bsr.w readmeta
    cmp.w MODE,%d6
    bne.s different
    tst.w VIEW
    bne.w success             | active data is newer than its backup
 different:
    moveq #0,%d7
    move.w %d6,%d0
    subq.w #1,%d0
    btst %d0,META+9
    beq.s target_ready
    move.w %d6,%d0
    bsr.w bankptr
    bsr.w validbank            | validate before touching either current season or snapshot
    tst.w %d0
    bne.w failed
    moveq #1,%d7
 target_ready:
    tst.w VIEW
    beq.s no_backup
    bsr.w backup
 no_backup:
    tst.w %d7
    beq.s fresh
    move.w %d6,%d0
    bsr.w bankptr
    bsr.w restore
    bra.s selected
 fresh:
    move.w %d6,MODE
    bsr.w clearseason
 selected:
    move.w %d6,MODE
    move.w %d6,META+6
    move.w #1,META+10
    move.w #1,VIEW
    .if WITH_PASS_CHARGE
    jsr 0x2E820C                    | identity/view is committed AFTER restore
    .endif
    .if WITH_PICKUP_DELAY
    jsr 0x220004                    | load after destination identity/view commit
    .endif
    .if WITH_ALT_GOALIES
    jsr 0x221010                    | load after destination identity/view commit
    .endif
    .if WITH_STRICT_SO
    jsr 0x222010                    | independent extra-SO preference after context identity commit
    .endif
    .if WITH_MOMENTUM
    jsr 0x226000                    | actual destination context committed
    .endif
    .if WITH_FACEOFF_DOTS
    jsr 0x224008                    | load after actual destination identity
    .endif
    .if WITH_AI_SHOT_PLAN
    jsr 0x223010                    | destination identity/view committed
    .endif
    .if WITH_REFINEMENTS
    jsr 0x258000                     | destination Shot Units/Fragile Glass context
    .endif
    bsr.w writemeta            | preserve current directory mask when loading an older snapshot
    bsr.w checksum
    .if WITH_TRANSFERS
    jsr 0x25600C
    .endif
 success:
    moveq #0,%d0
    bra.s selected_done
 failed:
    moveq #-1,%d0
 selected_done:
    move.b #0,MAP
    move.w (%sp)+,%sr
    movem.l (%sp)+,%d1-%d7/%a0-%a6
    .if WITH_TRANSFERS
    jsr 0x236028                   | Refresh identity previews after actual slot/view commit.
    .endif
    rts

suspend:
    bsr.w init
    movem.l %d0-%d7/%a0-%a6,-(%sp)
    move.w %sr,-(%sp)
    ori.w #0x700,%sr
    move.b #1,MAP
    bsr.w readmeta
    tst.w VIEW
    beq.s suspended
    bsr.w backup
    clr.w VIEW
    clr.w META+10
    bsr.w writemeta
    bsr.w ctxload              | standalone native playoffs use their own context
    bsr.w checksum
suspended:
    move.b #0,MAP
    move.w (%sp)+,%sr
    movem.l (%sp)+,%d0-%d7/%a0-%a6
    rts

restart:
    bsr.w init
    movem.l %d0-%d7/%a0-%a6,-(%sp)
    move.w %sr,-(%sp)
    ori.w #0x700,%sr
    move.b #1,MAP
    bsr.w clearseason
    bsr.w checksum
    .if WITH_TRANSFERS
    jsr 0x25600C
    .endif
    move.b #0,MAP
    move.w (%sp)+,%sr
    movem.l (%sp)+,%d0-%d7/%a0-%a6
    rts

| Add Restart Season to season-playoff pre-game menus, keeping standalone lists native.
prelist:
    lea 0x131CC,%a0
    movea.l (%a0,%d0.w),%a0
    cmpi.w #20,%d0
    bne.s list_ready
    tst.w VIEW
    beq.s list_ready
    lea 0x2AB000,%a0
list_ready:
    rts

| Native settings/password can be newer in RAM than their battery image.
backup:
    lea 0xFFFFD076,%a0
    move.l #0x1EF6,%d0
    move.w #256,%d1
    bsr.w write
    lea 0xFFFFD046,%a0
    move.l #EXTRA,%d0
    moveq #66,%d1
    bsr.w write
    move.w MODE,%d0
    subq.w #1,%d0
    bset %d0,META+9
    move.w MODE,META+6
    move.w #1,META+10
    bsr.w writemeta
    bsr.w checksum
    move.w MODE,%d0
    bsr.w bankptr
    movea.l %a2,%a1
    lea 0x200001,%a0
    bsr.w copybank
    movea.l %a2,%a0
    bsr.w sumbank              | the bank's shared-profile bytes differ from the active window
    rts

| A2=validated snapshot. META in RAM remains the current directory.
restore:
    movea.l %a2,%a0
    lea 0x200001,%a1
    bsr.w copybank
    move.w 0xFFFFD046,-(%sp)           | r18: $D046 is the 4-Way Play hardware flag, not a setting:
    lea 0xFFFFD046,%a0                 | a slot saved without the adapter cleared it, the menu then
    move.l #EXTRA,%d0                  | read the pads the wrong way and took no input at all
    moveq #66,%d1
    bsr.w read
    move.w (%sp)+,0xFFFFD046
    lea 0xFFFFD076,%a0
    move.l #0x1EF6,%d0
    move.w #256,%d1
    bsr.w read
    | Native records/settings arrays are read on boot but must also follow a live switch.
    lea 0xFFFFCF36,%a0
    move.l #0xD20,%d0
    move.w #128,%d1
    bsr.w read
    lea 0xFFFFD45A,%a0
    move.l #0xDA0,%d0
    move.w #128,%d1
    bsr.w read
    jsr 0x2EB000                     | restore the target slot's own human count, including three/four
    bra.w checksum             | active keeps its shared profiles, so its checksum changes

clearseason:
    .if WITH_TRANSFERS
    jsr 0x256008                   | Full old active odd snapshot, committed before mutations.
    jsr 0x256014                   | Reset only this format's ownership and pending preference.
    .endif
    .if WITH_REFINEMENTS
    jsr 0x258008                     | reset new even preferences AFTER the reset journal commits
    .endif
    .if WITH_MOMENTUM
    jsr 0x226008                    | reset only confirmed selected season toOn
    .endif
    .if WITH_FACEOFF_DOTS
    jsr 0x224010                    | selected confirmed restart only
    .endif
    .if WITH_AI_SHOT_PLAN
    jsr 0x223018                    | selected confirmed restart only
    .endif
    .if WITH_STRICT_SO
    jsr 0x222018                    | confirmed selected season reset only
    .endif
    .if WITH_ALT_GOALIES
    jsr 0x221018                    | reset only selected season preference and transients
    .endif
    .if WITH_PICKUP_DELAY
    jsr 0x22000C                    | reset selected format, preserve native choice
    .endif
    .if WITH_PASS_CHARGE
    jsr 0x2E821C                    | reset this format only, mapped operation
    .endif
    .if WITH_PENALTY_RATE
    .if WITH_REFINEMENTS
    move.w #1,0xFFFFF006
    .else
    clr.w 0xFFFFF006                 | fresh100% frequency, separate from native rules
    .endif
    .endif
    .if WITH_DIRECTIONAL
    clr.l 0xFFFFF0FC                | Assisted and Default cone on this season restart
    .endif
    .if WITH_PASS_SPEED
    clr.w 0xFFFFF0E8              | separate fresh/restarted season uses native Normal speed
    .endif
    .if WITH_PASS_HEIGHT
    .if WITH_REFINEMENTS
    move.w #1,0xFFFFF0DC            | fresh Low lift
    .else
    clr.w 0xFFFFF0DC              | fresh/restarted season uses native Normal lift
    .endif
    .endif
    .if WITH_PREF_DEFAULTS
    move.w #1,0xFFFFF07A              | fresh format / explicit restart defaults Hard
    .endif
    .if WITH_ICING_MENU
    move.w #10,0xFFFFD04C           | new empty slot / explicit restart defaults to Edmonton
    .if WITH_PREF_DEFAULTS
    move.w #ICING_DEFAULT,0xFFFFF0EE | fresh/restarted format defaults Icing On
    .else
    clr.w 0xFFFFF0EE               | legacy r5 default On
    .endif
    .endif
    clr.b 0x203FED                    | discard the old season settings record on an explicit restart
    lea 0x201C41,%a0            | logicalE20: all goalie/header/table/player/opponent/playoff data
    move.w #0x1080-1,%d0
 clearloop:
    clr.b (%a0)
    addq.l #2,%a0
    dbra %d0,clearloop
    lea HEAD,%a0
    moveq #7,%d0
 clearhead:
    clr.w (%a0)+
    dbra %d0,clearhead
    move.l #0x5345534E,HEAD
    move.w MODE,HEAD+12
    move.w #3,HEAD+14
    move.w #84,HEAD+10
    cmpi.w #2,MODE
    bne.s not_light
    move.w #31,HEAD+10
 not_light:
    cmpi.w #3,MODE
    bne.s not_mini
    move.w #MINI_GAMES,HEAD+10
 not_mini:
    lea HEAD,%a0
    move.l #0x1000,%d0
    moveq #16,%d1
    bsr.w write
    clr.w 0xFFFFD048
    clr.w 0xFFFFD074
    | One and Teammates are the season's supported human configurations (r16: also Three /
    | Four - Teammates with 4-Way Play, mtap.s TM = 3 / 4 with Players 5 / 6).
    cmpi.w #3,0xFFFFD04A
    beq.s players_ready
    cmpi.w #5,0xFFFFD04A
    bne.s 1f
    cmpi.w #3,0xFFFFF07C
    beq.s players_ready
1:  cmpi.w #6,0xFFFFD04A
    bne.s single_player
    cmpi.w #4,0xFFFFF07C
    beq.s players_ready
    move.w #3,0xFFFFD04A
    bra.s players_ready
 single_player:
    move.w #1,0xFFFFD04A
 players_ready:
    jsr 0x2EB010                     | restart inherits the selected slot's preferred count
    jsr 0x2EB004                     | a fresh slot also needs its own optional PC record
    move.b #1,MAP                    | store closes the mapper; the slot operation continues below
    lea 0xFFFFD088,%a0
    move.w #0xEE/2-1,%d0
 clearpassword:
    clr.w (%a0)+
    dbra %d0,clearpassword
    lea 0xFFFFD076,%a0
    move.l #0x1EF6,%d0
    move.w #256,%d1
    bsr.w write
    rts

readmeta:
    lea META,%a0
    move.l #METAOFF,%d0
    moveq #12,%d1
    bra.s read
writemeta:
    lea META,%a0
    move.l #METAOFF,%d0
    moveq #12,%d1
    bra.s write

| Full32-bit SRAM pointer: native helpers sign-extend their doubled word offsets.
read:
    lea 0x200001,%a1
    add.l %d0,%d0
    adda.l %d0,%a1
    subq.w #1,%d1
 readloop:
    move.b (%a1),(%a0)+
    addq.l #2,%a1
    dbra %d1,readloop
    rts
write:
    lea 0x200001,%a1
    add.l %d0,%d0
    adda.l %d0,%a1
    subq.w #1,%d1
 writeloop:
    move.b (%a0)+,(%a1)
    addq.l #2,%a1
    dbra %d1,writeloop
    rts
bankptr:
    andi.l #0xFFFF,%d0
    lsl.l #8,%d0
    lsl.l #6,%d0                 | physicalstride$4000 = logical$2000
    lea 0x200001,%a2
    adda.l %d0,%a2
    rts
| Logical D20..E1F (user profiles CF36/D45A) are shared by all seasons since r9:
| they stay in the active window; the banks' copies of that range are free.
copybank:
    move.w #0xD20-1,%d0
 copyloop:
    move.b (%a0),(%a1)
    addq.l #2,%a0
    addq.l #2,%a1
    dbra %d0,copyloop
    adda.l #0x200,%a0
    adda.l #0x200,%a1
    move.w #0x11E0-1,%d0
 copyloop2:
    move.b (%a0),(%a1)
    addq.l #2,%a0
    addq.l #2,%a1
    dbra %d0,copyloop2
    rts

| Standalone native playoff context (password + packed playoff stats, RAM
| D088..D175 = 238 bytes) in the Full bank's free profile range, logical 2D20:
| "NPO1", 238 bytes, sum, ~sum. MAP must be on, interrupts masked.
    .equ CTX,0x2D20
ctxload:
    movem.l %d0-%d2/%a0-%a1,-(%sp)
    lea 0x200001+CTX*2,%a1
    cmpi.b #0x4E,(%a1)
    bne.s ctx_empty
    cmpi.b #0x50,2(%a1)
    bne.s ctx_empty
    cmpi.b #0x4F,4(%a1)
    bne.s ctx_empty
    cmpi.b #0x31,6(%a1)
    bne.s ctx_empty
    addq.l #8,%a1
    moveq #0,%d2
    move.w #238-1,%d0
 ctx_sum:
    add.b (%a1),%d2
    addq.l #2,%a1
    dbra %d0,ctx_sum
    cmp.b (%a1),%d2
    bne.s ctx_empty
    not.b %d2
    cmp.b 2(%a1),%d2
    bne.s ctx_empty
    lea 0x200001+CTX*2+8,%a1
    lea 0xFFFFD088,%a0
    move.w #238-1,%d0
 ctx_copy:
    move.b (%a1),(%a0)+
    addq.l #2,%a1
    dbra %d0,ctx_copy
    bra.s ctx_store
 ctx_empty:
    lea 0xFFFFD088,%a0
    move.w #238-1,%d0
 ctx_clear:
    clr.b (%a0)+
    dbra %d0,ctx_clear
 ctx_store:
    jsr 0x2EB01C                     | separate standalone player preference, mapper already enabled
    lea 0xFFFFD076,%a0
    move.l #0x1EF6,%d0
    move.w #256,%d1
    bsr.w write
    movem.l (%sp)+,%d0-%d2/%a0-%a1
    rts
ctxsave:
    movem.l %d0-%d2/%a0-%a1,-(%sp)
    lea 0x200001+CTX*2,%a1
    move.b #0x4E,(%a1)
    move.b #0x50,2(%a1)
    move.b #0x4F,4(%a1)
    move.b #0x31,6(%a1)
    addq.l #8,%a1
    lea 0xFFFFD088,%a0
    moveq #0,%d2
    move.w #238-1,%d0
 ctxs_copy:
    move.b (%a0),(%a1)
    add.b (%a0)+,%d2
    addq.l #2,%a1
    dbra %d0,ctxs_copy
    move.b %d2,(%a1)
    not.b %d2
    move.b %d2,2(%a1)
    lea 0x204001,%a0           | Full bank: keep its own checksum valid
    bsr.w sumbank
    movem.l (%sp)+,%d0-%d2/%a0-%a1
    rts
| Native settings/playoff save $FE696 ends with jsr $1A206 (replaced by this).
| Outside the season view, its D088.. block is the standalone playoff: keep a copy.
ctxhook:
    jsr 0x1A206
    cmpi.w #0x5333,TAG
    bne.s ctxh_done
    tst.w VIEW
    bne.s ctxh_done
    move.w %sr,-(%sp)
    ori.w #0x700,%sr
    move.b #1,MAP
    bsr.w ctxsave
    move.b #0,MAP
    move.w (%sp)+,%sr
 ctxh_done:
    rts

validbank:
    movea.l %a2,%a0
    moveq #0,%d0
    move.w #0x1FFE-1,%d1
 validloop:
    add.b (%a0),%d0
    addq.l #2,%a0
    dbra %d1,validloop
    not.b %d0
    cmp.b (%a0),%d0
    bne.s invalid
    not.b %d0
    cmp.b 2(%a0),%d0
    bne.s invalid
    movea.l %a2,%a1
    adda.l #METAOFF*2,%a1
    lea HEAD,%a0
    moveq #11,%d1
 validmeta:
    move.b (%a1),(%a0)+
    addq.l #2,%a1
    dbra %d1,validmeta
    cmpi.l #MAGIC,HEAD
    bne.s invalid
    cmpi.w #1,HEAD+4
    bne.s invalid
    cmp.w HEAD+6,%d6
    bne.s invalid
    moveq #0,%d0
    rts
 invalid:
    moveq #-1,%d0
    rts
checksum:
    lea 0x200001,%a0
| sumbank: A0 = first odd byte of an 8KB image; writes its ~sum/sum at 1FFE/1FFF.
sumbank:
    move.l %d1,-(%sp)
    moveq #0,%d0
    move.w #0x1FFE-1,%d1
 sumloop:
    add.b (%a0),%d0
    addq.l #2,%a0
    dbra %d1,sumloop
    not.b %d0
    move.b %d0,(%a0)
    not.b %d0
    move.b %d0,2(%a0)
    move.l (%sp)+,%d1
    rts
