Release 2026-11-04-r2 · SHA256 53753f78156376d1c1cd2e191cf40673d7bd9add95da8ec15e37d557bfa087f4 · Click a line number for a direct link.
1| Three independent season saves. Active legacy8KB + three8KB snapshots.2| SRAM header expanded to odd $200001..$20FFFF. Native helpers remain8KB-only.3| Metadata in formerly unused logical $1EA0..$1EF5, covered by native checksum.4| +0 init/migrate; +4 select(D0=1/2/3, returns0/-1); +8 suspend; +12 restart; +16 prelist; +20 native save hook.5 .include "features.inc"6 .equ TAG,0xFFFFF0687 .equ MODE,0xFFFFF06A8 .equ VIEW,0xFFFFF06C9 .equ META,0xFFFFE80010 .equ HEAD,0xFFFFE85011 .equ MAGIC,0x53335356 | S3SV12 .equ METAOFF,0x1EA013 .equ EXTRA,0x1EAC |66bytes D046..D08714 .equ MAP,0xA130F115 .text16 .globl _start17_start:18 bra.w init19 bra.w select20 bra.w suspend21 bra.w restart22 bra.w prelist23 bra.w ctxhook24init:25 cmpi.w #0x5333,TAG26 beq.w init_done27 movem.l %d0-%d7/%a0-%a6,-(%sp)28 move.w %sr,-(%sp)29 ori.w #0x700,%sr30 move.b #1,MAP31 .if WITH_TRANSFERS32 jsr 0x256004 | Recover committed reset before directory/migration.33 .endif34 bsr.w readmeta35 cmpi.l #MAGIC,META36 bne.s migrate37 cmpi.w #1,META+438 bne.s migrate39 move.w META+6,%d040 subq.w #1,%d041 cmpi.w #2,%d042 bhi.s migrate43 cmpi.w #7,META+844 bhi.s migrate45 move.w META+6,MODE46 move.w #1,VIEW47 tst.w META+1048 bne.w initialized49 | Outside the season view, the selected season's snapshot is authoritative.50 move.w MODE,%d051 move.w %d0,%d652 bsr.w bankptr53 bsr.w validbank54 tst.w %d055 bne.s blocked_resume | never replace current data with an invalid snapshot56 bsr.w restore | boot resumes the season view: its own playoff context57 bra.w initialized58blocked_resume:59 clr.w VIEW60 bra.w initialized61migrate:62 moveq #1,%d563 move.l #0x1000,%d064 moveq #16,%d165 lea HEAD,%a066 bsr.w read67 cmpi.l #0x5345534E,HEAD68 bne.s migrated69 tst.w HEAD+670 beq.s empty_legacy71 .if MINI_GAMES != 872 cmpi.w #MINI_GAMES,HEAD+1073 beq.s legacy_mini74 .endif75 cmpi.w #8,HEAD+1076 beq.s legacy_mini77 cmpi.w #31,HEAD+1078 bne.s legacy_running79 moveq #2,%d580 bra.s legacy_running81legacy_mini:82 moveq #3,%d583legacy_running:84 clr.w HEAD+12 | browsing New in an old ROM must not discard its saved season85 move.l #0x1000,%d086 moveq #16,%d187 lea HEAD,%a088 bsr.w write89 bra.s migrated90empty_legacy:91 move.w HEAD+12,%d092 subq.w #1,%d093 cmpi.w #2,%d094 bhi.s migrated95 addq.w #1,%d096 move.w %d0,%d597migrated:98 move.l #MAGIC,META99 move.w #1,META+4100 move.w %d5,META+6101 clr.w META+8102 move.w #1,META+10103 move.w %d5,MODE104 move.w #1,VIEW105 bsr.w writemeta106 bsr.w checksum107initialized:108 .if WITH_TRANSFERS109 jsr 0x256010110 tst.l %d0111 beq.s reset_recovery_done112 move.w %d0,MODE113 move.w %d0,META+6114 move.w #1,VIEW115 move.w #1,META+10116 bsr.w clearseason117 bsr.w writemeta118 bsr.w checksum119 jsr 0x25600C120reset_recovery_done:121 .endif122 tst.w VIEW123 beq.s count_ready124 jsr 0x2EB000 | optional per-slot player-count extension; mapper is enabled125count_ready:126 move.w #0x5333,TAG127 move.b #0,MAP128 move.w (%sp)+,%sr129 movem.l (%sp)+,%d0-%d7/%a0-%a6130 .if WITH_TRANSFERS131 jsr 0x236028 | Initial boot now has committed MODE/VIEW and mapperOff.132 .endif133init_done:134 rts135136select:137 bsr.w init138 movem.l %d1-%d7/%a0-%a6,-(%sp)139 move.w %sr,-(%sp)140 ori.w #0x700,%sr141 move.b #1,MAP142 move.w %d0,%d6 | destination format143 subq.w #1,%d0144 cmpi.w #2,%d0145 bhi.w failed146 bsr.w readmeta147 cmp.w MODE,%d6148 bne.s different149 tst.w VIEW150 bne.w success | active data is newer than its backup151 different:152 moveq #0,%d7153 move.w %d6,%d0154 subq.w #1,%d0155 btst %d0,META+9156 beq.s target_ready157 move.w %d6,%d0158 bsr.w bankptr159 bsr.w validbank | validate before touching either current season or snapshot160 tst.w %d0161 bne.w failed162 moveq #1,%d7163 target_ready:164 tst.w VIEW165 beq.s no_backup166 bsr.w backup167 no_backup:168 tst.w %d7169 beq.s fresh170 move.w %d6,%d0171 bsr.w bankptr172 bsr.w restore173 bra.s selected174 fresh:175 move.w %d6,MODE176 bsr.w clearseason177 selected:178 move.w %d6,MODE179 move.w %d6,META+6180 move.w #1,META+10181 move.w #1,VIEW182 .if WITH_PASS_CHARGE183 jsr 0x2E820C | identity/view is committed AFTER restore184 .endif185 .if WITH_PICKUP_DELAY186 jsr 0x220004 | load after destination identity/view commit187 .endif188 .if WITH_ALT_GOALIES189 jsr 0x221010 | load after destination identity/view commit190 .endif191 .if WITH_STRICT_SO192 jsr 0x222010 | independent extra-SO preference after context identity commit193 .endif194 .if WITH_MOMENTUM195 jsr 0x226000 | actual destination context committed196 .endif197 .if WITH_FACEOFF_DOTS198 jsr 0x224008 | load after actual destination identity199 .endif200 .if WITH_AI_SHOT_PLAN201 jsr 0x223010 | destination identity/view committed202 .endif203 .if WITH_REFINEMENTS204 jsr 0x258000 | destination Shot Units/Fragile Glass context205 .endif206 bsr.w writemeta | preserve current directory mask when loading an older snapshot207 bsr.w checksum208 .if WITH_TRANSFERS209 jsr 0x25600C210 .endif211 success:212 moveq #0,%d0213 bra.s selected_done214 failed:215 moveq #-1,%d0216 selected_done:217 move.b #0,MAP218 move.w (%sp)+,%sr219 movem.l (%sp)+,%d1-%d7/%a0-%a6220 .if WITH_TRANSFERS221 jsr 0x236028 | Refresh identity previews after actual slot/view commit.222 .endif223 rts224225suspend:226 bsr.w init227 movem.l %d0-%d7/%a0-%a6,-(%sp)228 move.w %sr,-(%sp)229 ori.w #0x700,%sr230 move.b #1,MAP231 bsr.w readmeta232 tst.w VIEW233 beq.s suspended234 bsr.w backup235 clr.w VIEW236 clr.w META+10237 bsr.w writemeta238 bsr.w ctxload | standalone native playoffs use their own context239 bsr.w checksum240suspended:241 move.b #0,MAP242 move.w (%sp)+,%sr243 movem.l (%sp)+,%d0-%d7/%a0-%a6244 rts245246restart:247 bsr.w init248 movem.l %d0-%d7/%a0-%a6,-(%sp)249 move.w %sr,-(%sp)250 ori.w #0x700,%sr251 move.b #1,MAP252 bsr.w clearseason253 bsr.w checksum254 .if WITH_TRANSFERS255 jsr 0x25600C256 .endif257 move.b #0,MAP258 move.w (%sp)+,%sr259 movem.l (%sp)+,%d0-%d7/%a0-%a6260 rts261262| Add Restart Season to season-playoff pre-game menus, keeping standalone lists native.263prelist:264 lea 0x131CC,%a0265 movea.l (%a0,%d0.w),%a0266 cmpi.w #20,%d0267 bne.s list_ready268 tst.w VIEW269 beq.s list_ready270 lea 0x2AB000,%a0271list_ready:272 rts273274| Native settings/password can be newer in RAM than their battery image.275backup:276 lea 0xFFFFD076,%a0277 move.l #0x1EF6,%d0278 move.w #256,%d1279 bsr.w write280 lea 0xFFFFD046,%a0281 move.l #EXTRA,%d0282 moveq #66,%d1283 bsr.w write284 move.w MODE,%d0285 subq.w #1,%d0286 bset %d0,META+9287 move.w MODE,META+6288 move.w #1,META+10289 bsr.w writemeta290 bsr.w checksum291 move.w MODE,%d0292 bsr.w bankptr293 movea.l %a2,%a1294 lea 0x200001,%a0295 bsr.w copybank296 movea.l %a2,%a0297 bsr.w sumbank | the bank's shared-profile bytes differ from the active window298 rts299300| A2=validated snapshot. META in RAM remains the current directory.301restore:302 movea.l %a2,%a0303 lea 0x200001,%a1304 bsr.w copybank305 move.w 0xFFFFD046,-(%sp) | r18: $D046 is the 4-Way Play hardware flag, not a setting:306 lea 0xFFFFD046,%a0 | a slot saved without the adapter cleared it, the menu then307 move.l #EXTRA,%d0 | read the pads the wrong way and took no input at all308 moveq #66,%d1309 bsr.w read310 move.w (%sp)+,0xFFFFD046311 lea 0xFFFFD076,%a0312 move.l #0x1EF6,%d0313 move.w #256,%d1314 bsr.w read315 | Native records/settings arrays are read on boot but must also follow a live switch.316 lea 0xFFFFCF36,%a0317 move.l #0xD20,%d0318 move.w #128,%d1319 bsr.w read320 lea 0xFFFFD45A,%a0321 move.l #0xDA0,%d0322 move.w #128,%d1323 bsr.w read324 jsr 0x2EB000 | restore the target slot's own human count, including three/four325 bra.w checksum | active keeps its shared profiles, so its checksum changes326327clearseason:328 .if WITH_TRANSFERS329 jsr 0x256008 | Full old active odd snapshot, committed before mutations.330 jsr 0x256014 | Reset only this format's ownership and pending preference.331 .endif332 .if WITH_REFINEMENTS333 jsr 0x258008 | reset new even preferences AFTER the reset journal commits334 .endif335 .if WITH_MOMENTUM336 jsr 0x226008 | reset only confirmed selected season toOn337 .endif338 .if WITH_FACEOFF_DOTS339 jsr 0x224010 | selected confirmed restart only340 .endif341 .if WITH_AI_SHOT_PLAN342 jsr 0x223018 | selected confirmed restart only343 .endif344 .if WITH_STRICT_SO345 jsr 0x222018 | confirmed selected season reset only346 .endif347 .if WITH_ALT_GOALIES348 jsr 0x221018 | reset only selected season preference and transients349 .endif350 .if WITH_PICKUP_DELAY351 jsr 0x22000C | reset selected format, preserve native choice352 .endif353 .if WITH_PASS_CHARGE354 jsr 0x2E821C | reset this format only, mapped operation355 .endif356 .if WITH_PENALTY_RATE357 .if WITH_REFINEMENTS358 move.w #1,0xFFFFF006359 .else360 clr.w 0xFFFFF006 | fresh100% frequency, separate from native rules361 .endif362 .endif363 .if WITH_DIRECTIONAL364 clr.l 0xFFFFF0FC | Assisted and Default cone on this season restart365 .endif366 .if WITH_PASS_SPEED367 clr.w 0xFFFFF0E8 | separate fresh/restarted season uses native Normal speed368 .endif369 .if WITH_PASS_HEIGHT370 .if WITH_REFINEMENTS371 move.w #1,0xFFFFF0DC | fresh Low lift372 .else373 clr.w 0xFFFFF0DC | fresh/restarted season uses native Normal lift374 .endif375 .endif376 .if WITH_PREF_DEFAULTS377 move.w #1,0xFFFFF07A | fresh format / explicit restart defaults Hard378 .endif379 .if WITH_ICING_MENU380 move.w #10,0xFFFFD04C | new empty slot / explicit restart defaults to Edmonton381 .if WITH_PREF_DEFAULTS382 move.w #ICING_DEFAULT,0xFFFFF0EE | fresh/restarted format defaults Icing On383 .else384 clr.w 0xFFFFF0EE | legacy r5 default On385 .endif386 .endif387 clr.b 0x203FED | discard the old season settings record on an explicit restart388 lea 0x201C41,%a0 | logicalE20: all goalie/header/table/player/opponent/playoff data389 move.w #0x1080-1,%d0390 clearloop:391 clr.b (%a0)392 addq.l #2,%a0393 dbra %d0,clearloop394 lea HEAD,%a0395 moveq #7,%d0396 clearhead:397 clr.w (%a0)+398 dbra %d0,clearhead399 move.l #0x5345534E,HEAD400 move.w MODE,HEAD+12401 move.w #3,HEAD+14402 move.w #84,HEAD+10403 cmpi.w #2,MODE404 bne.s not_light405 move.w #31,HEAD+10406 not_light:407 cmpi.w #3,MODE408 bne.s not_mini409 move.w #MINI_GAMES,HEAD+10410 not_mini:411 lea HEAD,%a0412 move.l #0x1000,%d0413 moveq #16,%d1414 bsr.w write415 clr.w 0xFFFFD048416 clr.w 0xFFFFD074417 | One and Teammates are the season's supported human configurations (r16: also Three /418 | Four - Teammates with 4-Way Play, mtap.s TM = 3 / 4 with Players 5 / 6).419 cmpi.w #3,0xFFFFD04A420 beq.s players_ready421 cmpi.w #5,0xFFFFD04A422 bne.s 1f423 cmpi.w #3,0xFFFFF07C424 beq.s players_ready4251: cmpi.w #6,0xFFFFD04A426 bne.s single_player427 cmpi.w #4,0xFFFFF07C428 beq.s players_ready429 move.w #3,0xFFFFD04A430 bra.s players_ready431 single_player:432 move.w #1,0xFFFFD04A433 players_ready:434 jsr 0x2EB010 | restart inherits the selected slot's preferred count435 jsr 0x2EB004 | a fresh slot also needs its own optional PC record436 move.b #1,MAP | store closes the mapper; the slot operation continues below437 lea 0xFFFFD088,%a0438 move.w #0xEE/2-1,%d0439 clearpassword:440 clr.w (%a0)+441 dbra %d0,clearpassword442 lea 0xFFFFD076,%a0443 move.l #0x1EF6,%d0444 move.w #256,%d1445 bsr.w write446 rts447448readmeta:449 lea META,%a0450 move.l #METAOFF,%d0451 moveq #12,%d1452 bra.s read453writemeta:454 lea META,%a0455 move.l #METAOFF,%d0456 moveq #12,%d1457 bra.s write458459| Full32-bit SRAM pointer: native helpers sign-extend their doubled word offsets.460read:461 lea 0x200001,%a1462 add.l %d0,%d0463 adda.l %d0,%a1464 subq.w #1,%d1465 readloop:466 move.b (%a1),(%a0)+467 addq.l #2,%a1468 dbra %d1,readloop469 rts470write:471 lea 0x200001,%a1472 add.l %d0,%d0473 adda.l %d0,%a1474 subq.w #1,%d1475 writeloop:476 move.b (%a0)+,(%a1)477 addq.l #2,%a1478 dbra %d1,writeloop479 rts480bankptr:481 andi.l #0xFFFF,%d0482 lsl.l #8,%d0483 lsl.l #6,%d0 | physicalstride$4000 = logical$2000484 lea 0x200001,%a2485 adda.l %d0,%a2486 rts487| Logical D20..E1F (user profiles CF36/D45A) are shared by all seasons since r9:488| they stay in the active window; the banks' copies of that range are free.489copybank:490 move.w #0xD20-1,%d0491 copyloop:492 move.b (%a0),(%a1)493 addq.l #2,%a0494 addq.l #2,%a1495 dbra %d0,copyloop496 adda.l #0x200,%a0497 adda.l #0x200,%a1498 move.w #0x11E0-1,%d0499 copyloop2:500 move.b (%a0),(%a1)501 addq.l #2,%a0502 addq.l #2,%a1503 dbra %d0,copyloop2504 rts505506| Standalone native playoff context (password + packed playoff stats, RAM507| D088..D175 = 238 bytes) in the Full bank's free profile range, logical 2D20:508| "NPO1", 238 bytes, sum, ~sum. MAP must be on, interrupts masked.509 .equ CTX,0x2D20510ctxload:511 movem.l %d0-%d2/%a0-%a1,-(%sp)512 lea 0x200001+CTX*2,%a1513 cmpi.b #0x4E,(%a1)514 bne.s ctx_empty515 cmpi.b #0x50,2(%a1)516 bne.s ctx_empty517 cmpi.b #0x4F,4(%a1)518 bne.s ctx_empty519 cmpi.b #0x31,6(%a1)520 bne.s ctx_empty521 addq.l #8,%a1522 moveq #0,%d2523 move.w #238-1,%d0524 ctx_sum:525 add.b (%a1),%d2526 addq.l #2,%a1527 dbra %d0,ctx_sum528 cmp.b (%a1),%d2529 bne.s ctx_empty530 not.b %d2531 cmp.b 2(%a1),%d2532 bne.s ctx_empty533 lea 0x200001+CTX*2+8,%a1534 lea 0xFFFFD088,%a0535 move.w #238-1,%d0536 ctx_copy:537 move.b (%a1),(%a0)+538 addq.l #2,%a1539 dbra %d0,ctx_copy540 bra.s ctx_store541 ctx_empty:542 lea 0xFFFFD088,%a0543 move.w #238-1,%d0544 ctx_clear:545 clr.b (%a0)+546 dbra %d0,ctx_clear547 ctx_store:548 jsr 0x2EB01C | separate standalone player preference, mapper already enabled549 lea 0xFFFFD076,%a0550 move.l #0x1EF6,%d0551 move.w #256,%d1552 bsr.w write553 movem.l (%sp)+,%d0-%d2/%a0-%a1554 rts555ctxsave:556 movem.l %d0-%d2/%a0-%a1,-(%sp)557 lea 0x200001+CTX*2,%a1558 move.b #0x4E,(%a1)559 move.b #0x50,2(%a1)560 move.b #0x4F,4(%a1)561 move.b #0x31,6(%a1)562 addq.l #8,%a1563 lea 0xFFFFD088,%a0564 moveq #0,%d2565 move.w #238-1,%d0566 ctxs_copy:567 move.b (%a0),(%a1)568 add.b (%a0)+,%d2569 addq.l #2,%a1570 dbra %d0,ctxs_copy571 move.b %d2,(%a1)572 not.b %d2573 move.b %d2,2(%a1)574 lea 0x204001,%a0 | Full bank: keep its own checksum valid575 bsr.w sumbank576 movem.l (%sp)+,%d0-%d2/%a0-%a1577 rts578| Native settings/playoff save $FE696 ends with jsr $1A206 (replaced by this).579| Outside the season view, its D088.. block is the standalone playoff: keep a copy.580ctxhook:581 jsr 0x1A206582 cmpi.w #0x5333,TAG583 bne.s ctxh_done584 tst.w VIEW585 bne.s ctxh_done586 move.w %sr,-(%sp)587 ori.w #0x700,%sr588 move.b #1,MAP589 bsr.w ctxsave590 move.b #0,MAP591 move.w (%sp)+,%sr592 ctxh_done:593 rts594595validbank:596 movea.l %a2,%a0597 moveq #0,%d0598 move.w #0x1FFE-1,%d1599 validloop:600 add.b (%a0),%d0601 addq.l #2,%a0602 dbra %d1,validloop603 not.b %d0604 cmp.b (%a0),%d0605 bne.s invalid606 not.b %d0607 cmp.b 2(%a0),%d0608 bne.s invalid609 movea.l %a2,%a1610 adda.l #METAOFF*2,%a1611 lea HEAD,%a0612 moveq #11,%d1613 validmeta:614 move.b (%a1),(%a0)+615 addq.l #2,%a1616 dbra %d1,validmeta617 cmpi.l #MAGIC,HEAD618 bne.s invalid619 cmpi.w #1,HEAD+4620 bne.s invalid621 cmp.w HEAD+6,%d6622 bne.s invalid623 moveq #0,%d0624 rts625 invalid:626 moveq #-1,%d0627 rts628checksum:629 lea 0x200001,%a0630| sumbank: A0 = first odd byte of an 8KB image; writes its ~sum/sum at 1FFE/1FFF.631sumbank:632 move.l %d1,-(%sp)633 moveq #0,%d0634 move.w #0x1FFE-1,%d1635 sumloop:636 add.b (%a0),%d0637 addq.l #2,%a0638 dbra %d1,sumloop639 not.b %d0640 move.b %d0,(%a0)641 not.b %d0642 move.b %d0,2(%a0)643 move.l (%sp)+,%d1644 rts645