#!/usr/bin/env python3
"""Refresh forum attribution, source links and exact current-ROM byte exports."""
from pathlib import Path
import argparse,csv,hashlib,html,json,re
from urllib.parse import quote

root=Path(__file__).resolve().parent.parent;tools=root/'tools'
ap=argparse.ArgumentParser(description=__doc__);ap.add_argument('--assets-only',action='store_true');a=ap.parse_args()
meta=json.loads((tools/'experimental/release.json').read_text())
rom_path=root/'download'/(meta['rom_basename']+'.bin');base_path=root/'.workspace/NHL 2026 - 94 Edition v5.1W.bin.orig.bak';plain_path=root/'.workspace/NHL 2026 - 94 Edition v5.1W.bin'
sha=lambda b:hashlib.sha256(b).hexdigest()

def source(file,needle=None,label=None):
    line=1
    if needle:
        rows=(tools/file).read_text().splitlines();line=next(i+1 for i,text in enumerate(rows) if needle in text)
    assert (tools/file).is_file(),file
    return '<a href="tools/source.php?file='+quote(file,safe='/')+'#L'+str(line)+'">'+html.escape(label or file)+'</a>'

def hook(title,address,size,file,needle=None):
    return dict(title=title,address=address,size=size,source=file,line_needle=needle)

hooks=[
    hook('Easy numeric roster formatter',0x8E46,6,'experimental/easy_stats.s','roster_format:'),
    hook('Easy whole-team assistance entries',0x2EE400,24,'experimental/easy_stats.s','_start:'),
    hook('P2 red star palette descriptors',0x205744,10,'experimental/pad_colors.py','for at in (0x205744,0x20574C):'),
    hook('P3 dedicated green star source',0x2085DA,10,'experimental/pad_colors.py',"patch(at,bytes.fromhex('0f1a'),STAR_KEY"),
    hook('P3 separate green star art',0x4004,128,'experimental/pad_colors.py','STAR_ART='),
    hook('Original P1 blue and goal red palette restored',0x59956,10,'experimental/pad_colors.py',"assert data[0x59956:0x59960]"),
    hook('Goalie receiver scan',0xBD02,2,'experimental/forumfix.py','patch(0xBD02'),
    hook('Cached goalie receiver scan',0x2FF1AC,2,'experimental/forumfix.py','patch(at + 4'),
    hook('Human skating friction',0xAC12,12,'experimental/forumfix.s','friction:'),
    hook('Penalty-shot award',0xED96,8,'experimental/forumfix.s','psaward:'),
    hook('Penalty-shot setup',0xEF92,6,'experimental/forumfix.s','pssetup:'),
    hook('Wing Offensive Awareness',0xD0DE,6,'experimental/forumfix.py','patch(0xD0DE'),
    hook('Center Offensive Awareness',0xD2D6,6,'experimental/forumfix.py','patch(0xD2D6'),
    hook('Short-pass flight time',0xBF54,6,'experimental/forumfix.s','passflight:'),
    hook('Diving catch possession timer',0x1531C,6,'experimental/build_test.py','patch(0x1531C'),
    hook('Startup credit card',0x771E,6,'experimental/credits.s','_start:'),
    hook('Weak-side defensive winger',0xD086,8,'experimental/ai_defence.py','def install'),
    hook('Defensive pair positioning',0xCF8A,10,'experimental/ai_gap.py','def install'),
    hook('Attack support, first caller',0xD1DE,8,'experimental/ai_attack.py','jump(0xD1DE'),
    hook('Attack support, second caller',0xD3BE,8,'experimental/ai_attack.py','jump(0xD3BE'),
    hook('CPU pass receiver choice',0xE45E,6,'experimental/ai_attack.s','passpick:'),
    hook('CPU shot/pass decision',0xE292,6,'experimental/ai_freecross.s','shot:'),
    hook('First forward unit personnel',0x1584C,8,'experimental/coach_lines.s','personnel:'),
    hook('Matching native line energy',0x12EB8,6,'experimental/coach_lines.s','energy:'),
    hook('Real keeper selection',0x15AA4,6,'experimental/coach_goalie.s','_start:'),
    hook('Deliberate native cross-pass decision',0xE444,8,'experimental/ai_freecross.s','pass:'),
    hook('Native target opening decision',0xC30C,8,'experimental/ai_smart_aim.s','shot_hook:'),
    hook('Accepted-pass receiver plan',0xBFB6,6,'experimental/ai_pass_plan.s','_start:'),
    hook('Hard target refresh interval',0x103D0,6,'experimental/ai_steering.s','_start:'),
    hook('Native lateral keeper tracking',0xDA58,6,'experimental/ai_keeper_pass.s','_start:'),
    hook('Keeper attacking goal accounting',0x14964,6,'experimental/goalie_offense.s','_start:'),
    hook('Keeper attacking shot accounting',0x1301C,6,'experimental/goalie_offense.s','_start:'),

]

b_current=rom_path.read_bytes()
width_pattern=bytes.fromhex('08380001bf78')
width_hits=[p for p in range(0x25d000,0x260000)if b_current[p:p+len(width_pattern)]==width_pattern]
assert len(width_hits)==1,('Expected actual H32/H40 byte flag read',width_hits)
hooks += [hook('Native H32/H40 footer centre export',0x25d004,4,'experimental/grey_panels.s','center_position:'),hook('Actual native BF78 byte bit1 width selection',width_hits[0],6,'experimental/grey_panels.s','btst #1,0xFFFFBF78')]

maintenance_start=len(hooks)
hooks += [hook('Crit Shots season-lock writer',0x2EB02C,4,'experimental/seasonplayers.s','writer:'),hook('Immediate B input and goalie handoff',0xB1B6,10,'experimental/fourgoalie.s','early:'),hook('Native Stars HUD cleanup',0x18514,6,'experimental/stars_hud.s','_start:')]

hooks += [hook('Actual carried-puck keeper target',0xD8F8,2,'experimental/build_test.py','patch(0xD8F8'),hook('Taken keeper recovery branch',0xD650,2,'experimental/build_test.py','patch(0xD650'),hook('Keeper recovery veneer in dead glass padding',0x14BE6,6,'experimental/goalie_anticipation.s','_start:')]

hooks += [hook('Ordinary accepted keeper request',0x2FDE80,6,'experimental/outlet.s','jsr 0x2DB400'),hook('Adapter accepted keeper request',0x2EAACE,6,'experimental/fourgoalie.s','jsr 0x2DB400'),hook('One-time keeper deadline reservation',0x2DB400,98,'experimental/outlet_deadline.s','_start:')]
hooks += [hook('Optional native touch-icing award',0x101EE,8,'experimental/build_test.py','patch(0x101EE'),hook('Icing On/Off award helper',0x2DBA00,24,'experimental/icing.s','_start:'),hook('Edmonton startup default',0x17C92,2,'experimental/build_test.py','patch(0x17C92'),hook('Current native menu cursor limit',0xF777A,2,'experimental/replaymenu.py',"'002a' if with_alt_goalies"),hook('Icing menu label',0x2E12C0,16,'experimental/replaymenu.py','Icing        ')]
hooks += [hook('Cooperative keeper catch handoff call',0x2FE46E,6,'experimental/goalie_ai.s','0x2DBC00'),hook('Native keeper-to-skater handoff helper',0x2DBC00,288,'experimental/goalie_catch_control.s','_start:'),hook('Current-skater keeper outlet request',0x2FDC0C,4,'experimental/outlet.s','request:'),hook('Requested Hard startup defaults',0x2E0008,4,'experimental/crowd.s','defaults:'),hook('Requested Replay/Crit fallback defaults',0x2DE004,4,'experimental/replaymenu.s','loadrecord:')]
hooks += [hook('Fresh-format Icing On default',0x2AA360,6,'season/slots.s','move.w #ICING_DEFAULT'),hook('Boot Icing On default',0x2DE078,6,'experimental/replaymenu.s','move.w #ICING_DEFAULT'),hook('Empty-season Icing On fallback',0x2EB454,6,'experimental/seasonplayers.s','move.w #ICING_DEFAULT')]
hooks += [hook('Overtime follows selected period length',0x2AC024,8,'experimental/shootout.s','timer:'),hook('Consume post-whistle momentum events',0x2E074A,52,'experimental/crowd.s','changed_events:')]
hooks += [hook('Mini divisional opponent lookup',0x2DBE00,4,'season/minischedule.s','bra.w pick'),hook('Mini divisional simulated fixtures',0x2DBE04,4,'season/minischedule.s','bra.w fixtures')]
hooks += [hook('Human buffered contact/follow-through frame',0x2E1004,4,'experimental/onetimer.s','bra.w frame'),hook('Owned C keeps a buffered pass shot',0xB810,6,'experimental/onetimer.py','patch(0xB810'),hook('Native stick-contact gate',3019132,16,'experimental/onetimer.s','stick_contact:')]

hooks += [hook('Capture fouled physical penalty-shot pad',0x121C2,10,'experimental/build_test.py','patch(0x121C2'),hook('Penalty-shot participant assignment',0xF20C,8,'experimental/build_test.py','patch(0xF20C'),hook('Fouled pad ownership helper',0x2DA800,8,'experimental/penalty_owner.s','_start:')]

hooks += [hook('Mini records seven regular games',0x2F814E,6,'season/season.s','move.w  #MINI_GAMES,10'),hook('Mini menu denominator seven',0x22A47C,2,'season/seasonmenu.s','moveq   #MINI_GAMES'),hook('Mini seven keeps single-game playoffs',0x2FC94C,10,'season/playoffs.s','#MINI_GAMES,PBUF+10')]

hooks += [hook('Owned manual keeper uses native pass',0x2DB0B6,4,'experimental/switch.s','beq.w reset_original            | owned manual keeper'),hook('Adapter owned keeper uses native pass',0x2EA784,4,'experimental/fourgoalie.s','beq.w native_early              | owning manual keeper')]

hooks += [hook('Goal net release positive X threshold',0x14B6C,2,'experimental/build_test.py','for at,old,new in [(0x14B6C'),hook('Goal net release negative X threshold',0x14B74,2,'experimental/build_test.py','for at,old,new in [(0x14B6C'),hook('Goal net release positive Y threshold',0x14B7C,2,'experimental/build_test.py','for at,old,new in [(0x14B6C'),hook('Goal net release negative Y threshold',0x14B84,2,'experimental/build_test.py','for at,old,new in [(0x14B6C')]

hooks += [hook('Automatic attempt initialization',0xF280,6,'experimental/build_test.py','patch(0xF280'),hook('Automatic CPU attempt countdown',0xF298,2,'experimental/build_test.py','patch(0xF298'),hook('Shooter-centre blue-line movement watcher',0xF2F4,8,'experimental/shootout_rules.s','enforce:')]

hooks += [hook('Downed native B-pass recipient guard',0xBD06,6,'experimental/downed_receiver.s','scan:'),hook('Downed keeper fallback recipient guard',0x2FF1B0,6,'experimental/downed_receiver.s','scan:'),hook('Downed actual stick-contact guard',0x150F6,6,'experimental/downed_receiver.s','pickup:')]

height_hook_start=len(hooks)
hooks += [hook('Optional targeted B-pass lift',0xBF5E,18,'experimental/pass_height.s','lift:'),hook('Combined height/speed preference exports',0x2DE400,20,'experimental/pass_height.s','_start:'),hook('Pass Height label',0x2E12D0,16,'experimental/replaymenu.py','Pass Height  ')]

hooks += [hook('Independent targeted B-pass speed',0xBF54,6,'experimental/pass_speed.s','flight:'),hook('Pass speed timing/menu exports',0x2DE600,8,'experimental/pass_speed.s','_start:'),hook('Pass Speed label',0x2E12E0,16,'experimental/replaymenu.py','Pass Speed   ')]

pickup_hook_start=len(hooks)
hooks += [hook('Fair accepted co-op pickup control',0x1538A,6,'experimental/fair_pickup.s','_start:')]

directional_hook_start=len(hooks)
hooks += [hook('Ordinary human B mode dispatcher',0xBC40,8,'experimental/pass_mode.s','input_dispatch:'),hook('Human vector targeting cone',0x2FF034,16,'experimental/pass_mode.s','cone:'),hook('Human keeper cone fallback',0x2FF1D8,12,'experimental/pass_mode.s','keeper_fallback:'),hook('Mode/cone module exports',0x2E6800,32,'experimental/pass_mode.s','_start:'),hook('Pass Mode menu label',0x2E12F0,16,'experimental/replaymenu.py','Pass Mode    '),hook('Target Cone menu label',0x2E1300,16,'experimental/replaymenu.py','Target Cone  ')]

penalty_rate_hook_start=len(hooks)
hooks += [hook('Separate ordinary penalty admission',0x11F78,6,'experimental/penalty_rate.s','event:'),hook('Whole-fight percentage commit',0x2004F0,10,'experimental/penalty_rate.s','fight:'),hook('Penalty rate helper exports',0x2EC800,8,'experimental/penalty_rate.s','_start:'),hook('Penaltys percentage menu label',0x2E1310,16,'experimental/replaymenu.py','Penaltys     '),hook('Native combined rules label',0x2E1260,16,'experimental/replaymenu.py','Rules        '),hook('Protected human contact-to-release interval',0x2E1088,6,'experimental/onetimer.s','keep contact sprite/flip coherent')]

charge_hook_start=len(hooks)
hooks += [hook('Human held-pass begin',0xB760,6,'experimental/pass_charge.s','begin:'),hook('Held-pass module exports',0x2E8200,32,'experimental/pass_charge.s','_start:'),hook('Human old gate / charged emitter exports',0x2E6820,8,'experimental/pass_mode.s','private charging fall-through'),hook('Pass Charge menu label',0x2E1320,16,'experimental/replaymenu.py','Pass Charge  ')]

pickup_call=rom_path.read_bytes().find(bytes.fromhex('4eb900220000'),0x2E6800,0x2E7000)
assert pickup_call>=0 and rom_path.read_bytes().find(bytes.fromhex('4eb900220000'),pickup_call+6,0x2E7000)<0
pickup_delay_hook_start=len(hooks)
hooks += [hook('Actual human B pickup cooldown dispatch',pickup_call,6,'experimental/pass_mode.s','jsr 0x220000'),hook('Pickup-delay human release and preference exports',0x220000,16,'experimental/pickup_delay.s','_start:'),hook('Pickup Delay menu label',0x2E1330,16,'experimental/replaymenu.py','Pickup Delay ')]


alt_sog_call=rom_path.read_bytes().find(bytes.fromhex('4ef900221020'),0x2AC000,0x2AD000)
assert alt_sog_call>=0 and rom_path.read_bytes().find(bytes.fromhex('4ef900221020'),alt_sog_call+6,0x2AD000)<0
alt_goalies_hook_start=len(hooks)
hooks += [hook('Actual shot provenance after native release',0xC408,8,'experimental/alt_goalies.s','shot:'),hook('Optional native keeper reach envelope',0x2FF354,12,'experimental/alt_goalies.s','reach:'),hook('Native first-save catch/rebound choice',0x155FC,8,'experimental/alt_goalies.s','decision:'),hook('Bounded optional rebound velocity',0x15580,8,'experimental/alt_goalies.s','velocity:'),hook('Actual accepted possession invalidates shot provenance',0x15288,8,'experimental/alt_goalies.s','accepted_pickup:'),hook('Normal goal same-shot SOG filter dispatch',alt_sog_call,6,'experimental/alt_goalies.s','filter_sog:'),hook('Alt Goalies ten fixed exports',0x221000,40,'experimental/alt_goalies.s','_start:'),hook('Alt Goalies menu label',0x2E1340,16,'experimental/replaymenu.py','Alt Goalies  ')]

strict_so_hook_start=len(hooks)
hooks += [hook('Optional OT shootout eight fixed exports',0x222000,32,'experimental/strict_so.s','_start:'),hook('Legacy OT SO label; current rule permanent On/hidden',0x2E1350,16,'experimental/replaymenu.py','OT SO Rules ')]
for title,hexcode,lo,hi,file,needle in [
 ('Capture actual eligible rosters before lineup reset','4eb900222000',0x2AC000,0x2AD000,'experimental/shootout.s','jsr 0x222000'),
 ('Native setup with optional actual bench roster','4ef900222004',0x2AC000,0x2AD000,'experimental/shootout.s','jmp 0x222004'),
 ('Post-blue strict full-body turn observer','4eb900222008',0x2DAA00,0x2DB000,'experimental/shootout_rules.s','jsr 0x222008'),
 ('Reset per-attempt full-angle history','4eb90022200c',0x2DAA00,0x2DB000,'experimental/shootout_rules.s','jsr 0x22200C')]:
 b=rom_path.read_bytes();pattern=bytes.fromhex(hexcode);at=b.find(pattern,lo,hi);assert at>=0 and b.find(pattern,at+6,hi)<0,(title,hex(at));hooks.append(hook(title,at,6,file,needle))

shot_plan_hook_start=len(hooks)
hooks += [hook('Native ordinary shot commitment / optional Hard deferral',0xE412,6,'experimental/shot_plan.s','commit:'),hook('Native carrier target / optional lateral approach',0xE10C,10,'experimental/shot_plan.s','target:'),hook('AI Shot Plan eight fixed exports',0x223000,32,'experimental/shot_plan.s','_start:'),hook('AI Shot Plan menu label',0x2E1360,16,'experimental/replaymenu.py','AI Shot Plan '),hook('AI Shot Plan Off/On value strings',0x2E1576,44,'experimental/replaymenu.py','if with_ai_shot_plan:')]
for title,hexcode,lo,hi,file,needle in [
 ('Load independent shot plan after mapped context','4eb900223010',0x2E6800,0x2E7000,'experimental/pass_mode.s','jsr 0x223010'),
 ('Save independent shot plan record','4eb900223014',0x2E6800,0x2E7000,'experimental/pass_mode.s','jsr 0x223014'),
 ('Load shot plan after destination slot identity','4eb900223010',0x2AA000,0x2AA800,'season/slots.s','jsr 0x223010'),
 ('Reset only confirmed selected shot plan season','4eb900223018',0x2AA000,0x2AA800,'season/slots.s','jsr 0x223018')]:
 b=rom_path.read_bytes();pattern=bytes.fromhex(hexcode);at=b.find(pattern,lo,hi);assert at>=0 and b.find(pattern,at+6,hi)<0,(title,hex(at));hooks.append(hook(title,at,6,file,needle))

faceoff_dots_hook_start=len(hooks)
hooks += [hook('Optional painted-dot final stoppage point',0x1255A,8,'experimental/faceoff_dots.s','commit:'),hook('Faceoff Dots five fixed exports',0x224000,20,'experimental/faceoff_dots.s','_start:'),hook('Legacy dot label; current rule permanent On/hidden',0x2E1370,16,'experimental/replaymenu.py','Faceoff Dots '),hook('Faceoff Dots Off/On values',0x2E15A2,44,'experimental/replaymenu.py','if with_faceoff_dots:')]
for title,hexcode,lo,hi,file,needle in [
 ('Load dot policy after mapped context','4eb900224008',0x2E6800,0x2E7000,'experimental/pass_mode.s','jsr 0x224008'),
 ('Save independent dot policy record','4eb90022400c',0x2E6800,0x2E7000,'experimental/pass_mode.s','jsr 0x22400C'),
 ('Load dot policy after destination slot identity','4eb900224008',0x2AA000,0x2AA800,'season/slots.s','jsr 0x224008'),
 ('Reset only confirmed selected dot-policy season','4eb900224010',0x2AA000,0x2AA800,'season/slots.s','jsr 0x224010')]:
 b=rom_path.read_bytes();pattern=bytes.fromhex(hexcode);at=b.find(pattern,lo,hi);assert at>=0 and b.find(pattern,at+6,hi)<0,(title,hex(at));hooks.append(hook(title,at,6,file,needle))

# New r5 sites; source/byte guards identify this exact combined image.
r5_hook_start=len(hooks)
hooks += [
 hook('Native receiver far/near/geometry exports',0x225000,12,'experimental/finisher_position.s','_start:'),
 hook('Independent Momentum record and percentage exports',0x226000,28,'experimental/momentum.s','_start:'),
 hook('Lower-left panel ownership/layout exports',0x227000,20,'experimental/shot_panel_left.s','_start:'),
 hook('Outward coast target and arrival-brake callback',0x228000,78,'experimental/defensive_brake.s','_start:'),
 hook('Native shot presentation exports',0x2ED000,24,'experimental/shotmeter.s','_start:'),
 hook('Compact dual-unit font first glyph; full bank in byte diff',0x260000,96,'experimental/shot_panel_left.py','def '),
 hook('Momentum menu label',0x2E1380,16,'experimental/replaymenu.py',"Momentum     "),
 hook('Momentum On/Off menu values',0x2E15CE,44,'experimental/replaymenu.py',"for v in (b'On',b'Off')"),
 hook('Extended final value row',0xF7B44,2,'experimental/replaymenu.py','patch(0xF7B44'),
]
for title,hexcode,lo,hi,file,needle,count in [
 ('Far native pass receiver positioning','4eb900225000',0x2DD000,0x2DD400,'experimental/ai_playbook.s','jsr 0x225000',1),
 ('Near native receiver target refresh','4eb900225004',0x2DD000,0x2DD400,'experimental/ai_playbook.s','jsr 0x225004',1),
 ('Containment remaining outward coast','4eb900228000',0x2DC000,0x2DC280,'experimental/ai_gap.s','jsr 0x228000',1),
 ('Containing-back arrival callback','41f900228040',0x2DC000,0x2DC280,'experimental/ai_gap.s','lea 0x228040',1),
 ('Disabled momentum consumes actual counters','4eb90022600c',0x2E0000,0x2E0800,'experimental/crowd.s','jsr 0x22600C',1),
 ('Effective critical-roll percentage gate','4eb900226010',0x2E0000,0x2E0800,'experimental/crowd.s','jsr 0x226010',1),
 ('Effective roster/critical-show percentage gates','4eb900226014',0x2E0000,0x2E0800,'experimental/crowd.s','jsr 0x226014',2),
 ('Effective loaded-attribute percentage gate','4eb900226018',0x2E0000,0x2E0800,'experimental/crowd.s','jsr 0x226018',1),
 ('Load Momentum after mapped preferences','4eb900226000',0x2E6800,0x2E7000,'experimental/pass_mode.s','jsr 0x226000',1),
 ('Save separate Momentum preference','4eb900226004',0x2E6800,0x2E7000,'experimental/pass_mode.s','jsr 0x226004',1),
 ('Load Momentum after destination identity','4eb900226000',0x2AA000,0x2AA800,'season/slots.s','jsr 0x226000',1),
 ('Reset selected Momentum season only','4eb900226008',0x2AA000,0x2AA800,'season/slots.s','jsr 0x226008',1),
]:
 b=rom_path.read_bytes();pattern=bytes.fromhex(hexcode);found=[];at=lo
 while (at:=b.find(pattern,at,hi))>=0:found.append(at);at+=len(pattern)
 assert len(found)==count,(title,found,count)
 for i,at in enumerate(found):hooks.append(hook(title+(' '+str(i+1) if count>1 else ''),at,len(pattern),file,needle))

# R6 fixed exports and actual native dispatches; full byte diff also includes all lists/code.
assert rom_path.read_bytes()[0x22a47c:0x22a484]==bytes.fromhex('74070c79000300ff')
hooks += [
 hook('Quickgame ten fixed exports',0x229000,40,'experimental/quickgame.s','_start:'),
 hook('Human-only native side commit',0x2EA412,6,'experimental/mtap.s','jsr 0x22900C'),
 hook('Quickgame match-end season isolation',0x7C2E,6,'experimental/quickgame.s','gameend:'),
 hook('Physical P3/P4 human demo-exit guard',0x7C68,6,'experimental/quickgame.s','demo_gate:'),
 hook('Native Quickgame prelist dispatch',0x13134,6,'experimental/quickgame.s','prelist_gate:'),
 hook('Quickgame season-table suppression',0x17336,6,'experimental/quickgame.s','table_gate:'),
 hook('Quickgame physical shootout-pool preparation',0xF280,6,'experimental/quickgame.s','attempt_gate:'),
 hook('Relocated season menu legacy entry bridges',0x2F9800,16,'season/seasonmenu.s','_start:'),
 hook('Relocated menu entry and writer',0x22A000,16,'season/seasonmenu.s','_start:'),
 hook('Filtered Quickgame native list pointers',0x22B000,40,'experimental/build_test.py','quickgame'),
]

hooks += [hook('Allstar fixed exports',0x22C000,80,'experimental/allstar.s','_start:')]
hooks += [hook('Allstar native dispatch F8A3E',1018430,6,'experimental/allstar.s','team_title:' if 1018430==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch F99F8',1022456,6,'experimental/allstar.s','team_title:' if 1022456==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch F9A2C',1022508,6,'experimental/allstar.s','team_title:' if 1022508==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch FA370',1024880,6,'experimental/allstar.s','team_title:' if 1024880==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch FA888',1026184,6,'experimental/allstar.s','team_title:' if 1026184==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch FA952',1026386,6,'experimental/allstar.s','team_title:' if 1026386==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch FAE56',1027670,6,'experimental/allstar.s','team_title:' if 1027670==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch FB152',1028434,6,'experimental/allstar.s','team_title:' if 1028434==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch FC58C',1033612,6,'experimental/allstar.s','team_title:' if 1033612==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch FC600',1033728,6,'experimental/allstar.s','team_title:' if 1033728==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch FCC62',1035362,6,'experimental/allstar.s','team_title:' if 1035362==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch 8078',32888,8,'experimental/allstar.s','team_title:' if 32888==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch 8250',33360,6,'experimental/allstar.s','team_title:' if 33360==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch 1345E',78942,6,'experimental/allstar.s','team_title:' if 78942==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch 171C2',94658,6,'experimental/allstar.s','team_title:' if 94658==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch F7C7C',1014908,6,'experimental/allstar.s','team_title:' if 1014908==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch 132E4',78564,8,'experimental/allstar.s','team_title:' if 78564==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch 1330A',78602,8,'experimental/allstar.s','team_title:' if 78602==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch 17C1E',97310,6,'experimental/allstar.s','team_title:' if 97310==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch F8948',1018184,6,'experimental/allstar.s','team_title:' if 1018184==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch FAE2A',1027626,6,'experimental/allstar.s','team_title:' if 1027626==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch 2FF422',3142690,6,'experimental/allstar.s','team_title:' if 3142690==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch 2FF4A0',3142816,8,'experimental/allstar.s','team_title:' if 3142816==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch 2220D6',2236630,8,'experimental/allstar.s','team_title:' if 2236630==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch 2220E2',2236642,6,'experimental/allstar.s','team_title:' if 2236642==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch 2D8CD6',2985174,6,'experimental/allstar.s','team_title:' if 2985174==0x8078 else '_start:')]
hooks += [hook('Allstar native dispatch 2DF452',3011666,6,'experimental/allstar.s','team_title:' if 3011666==0x8078 else '_start:')]

hooks += [hook('Allstar native displayed team overall',0xFE172,8,'experimental/allstar.s','team_overall:'),hook('Allstar A own home/away uniform palettes',0x22D00C,64,'experimental/allstar.py','shades='),hook('Allstar B own home/away uniform palettes',0x22D80C,64,'experimental/allstar.py','shades='),hook('Allstar selected-roster team-overall data',0x22CD68,4,'experimental/allstar.py','put(UIDS,uids)')]

hooks += [hook('Automatic replay title/capture exports',0x2EC000,8,'experimental/replaytitle.s','_start:'),hook('Automatic replay lifecycle',0x2E5000,12,'experimental/replay.s','_start:'),hook('Compact/powerplay shot layout exports',0x227000,20,'experimental/shot_panel_left.s','_start:'),hook('Quickgame/Allstar side-policy exports',0x229000,40,'experimental/quickgame.s','_start:'),hook('Separate menu background native code',0x2EA000,32,'experimental/menubg.s','_start:'),hook('Requested P3 neon green fixed palette',0x5995A,2,'experimental/pad_colors.py','def install'),hook('Supplied menu78 image header',0x240000,38,'experimental/build_test.py','menu_picture=')]

# Combined preseason/presentation/refinements release: expose every guarded
# native transfer installer site, plus the named public module entries.
combined_hook_start=len(hooks)
if rom_path.read_bytes()[0x225FF0:0x225FF4]==b'FRCP':
    import ast
    tree=ast.parse((tools/'experimental/transfers.py').read_text())
    for node in ast.walk(tree):
        if isinstance(node,ast.Call) and isinstance(node.func,ast.Name) and node.func.id=='hook' and len(node.args)>=3:
            if all(isinstance(x,ast.Constant)for x in node.args[:3]):
                address,old,entry=[x.value for x in node.args[:3]]
                hooks.append(hook('Transferred native roster getter '+hex(address),address,len(bytes.fromhex(old)),
                                  'experimental/transfers.py','hook('+hex(address).upper().replace('0X','0x')))
    hooks += [
        hook('Preseason price/CRC/validation/load/save/transaction exports',0x234000,36,'experimental/transfers.s','_start:'),
        hook('Transferred native player/cache getter exports',0x236000,144,'experimental/transfers_runtime.s','_start:'),
        hook('Actual OVR-sorted buy/sell/Ready modal',0x250000,4,'experimental/transfers_ui.s','_start:'),
        hook('Transfers preference persistence and played-season locks',0x254000,24,'experimental/transfers_menu.s','_start:'),
        hook('Atomic selected-season reset journal',0x256000,16,'experimental/transfers_reset.s','_start:'),
        hook('Uniform OVR display projection',0x23BA00,32,'experimental/transfers.py','display_order='),
        hook('Current human/Allstar CPU policy exports',0x229000,40,'experimental/quickgame.s','_start:'),
        hook('Units/glass/pass gauge/line-pool preferences',0x258000,28,'experimental/refinements.s','_start:'),
        hook('Momentum bars below actual team names',0x259000,12,'experimental/compact_momentum.s','_start:'),
        hook('Consistent menu controls and crisp native footer',0x25B000,84,'experimental/menu_ui.s','_start:'),
        hook('Framed native powerplay draw/clear',0x2DCC00,8,'experimental/pwrplay_clear.s','_start:'),
        hook('Genuine friendly native B24 receiver admission',0x225800,12,'experimental/friendly_reception.s','_start:'),
        hook('Five native warning beeps at40seconds',0x1960,92,'experimental/build_test.py','0x1960'),
    ]

# Current transfer dollars and native footer frame are presentation-only exports.
if rom_path.read_bytes()[0x25A020:0x25A040] != bytes([255])*32:
    hooks += [
        hook('Unsigned32 transfer dollar formatter; internal units unchanged',0x250004,4,'experimental/transfers_ui.s','D1=payload length returned'),
        hook('Dense original-name position OVR dollar row',0x250008,4,'experimental/transfers_ui.s','player_text:'),
        hook('Centred native framed main-menu footer',0x25B024,4,'experimental/menu_ui.s','footer_main:'),
        hook('Transfer-modal dollar font guard',0x258014,4,'experimental/refinements.s','plus_minus:'),
        hook('Modal-owned native dollar glyph',0x25A020,32,'experimental/refinement_font.py','patch(GLYPH+32'),
    ]

# Read actual current label records: Transfers inserts one row and refinements
# remove the two permanently-On controls. Old fixed offsets must not be labelled
# as a different option after that layout change.
if rom_path.read_bytes()[0x225FF0:0x225FF4]==b'FRCP':
    current_rom=rom_path.read_bytes()
    current_labels={
        'Icing menu label':'Icing','Pass Height label':'Pass Height',
        'Pass Speed label':'Pass Speed','Pass Mode menu label':'Pass Mode',
        'Target Cone menu label':'Target Cone','Penaltys percentage menu label':'Penaltys',
        'Pass Charge menu label':'Pass Charge','Pickup Delay menu label':'Pickup Delay',
        'Alt Goalies menu label':'Alt Goalies','AI Shot Plan menu label':'AI Shot Plan',
        'Momentum menu label':'Momentum',
    }
    for item in hooks:
        if item['title'] in current_labels:
            record=b'\x00\x10'+current_labels[item['title']].encode().ljust(13)+b'\x00'
            slots=[at for at in range(0x2E1200,0x2E13A0,16)if current_rom[at:at+16]==record]
            assert len(slots)==1,(item['title'],slots)
            item.update(address=slots[0],size=16)
        elif item['title']=='Legacy OT SO label; current rule permanent On/hidden':
            item.update(title='Permanent On overtime-shootout load entry; menu row hidden',address=0x222010,size=4,source='experimental/strict_so.s',line_needle='load:')
        elif item['title']=='Legacy dot label; current rule permanent On/hidden':
            item.update(title='Permanent On painted-dot load entry; menu row hidden',address=0x224008,size=4,source='experimental/faceoff_dots.s',line_needle='load:')
        elif item['title']=='Faceoff Dots Off/On values':
            item.update(title='Retained legacy dot value strings; not a current menu control')
    hooks += [
        hook('Transfers menu label directly below Icing',0x2E12D0,16,'experimental/replaymenu.py','Transfers    '),
        hook('Shot Units current menu label',0x2E1380,16,'experimental/replaymenu.py',"b'Shot Units'"),
        hook('Fragile Glass current menu label',0x2E1390,16,'experimental/replaymenu.py',"b'Fragile Glass'"),
    ]

# Exact native additions qualified with the current main-menu/Y/OVR release.
if rom_path.read_bytes()[0x25C010:0x25C014]==b'MM01':
    import sys
    sys.path.insert(0,str(tools/'experimental'))
    from main_menu_scope import sites
    for address,original,replacement in sites():
        assert rom_path.read_bytes()[address:address+len(replacement)]==replacement
        hooks.append(hook('Main-menu guarded input/checksum/audio hook '+hex(address),address,len(replacement),'experimental/main_menu.py','def install'))
    hooks += [hook('Main-menu checksum/input/music helpers and full CCITT tables',0x25C000,0xA00,'experimental/main_menu.s','_start:'),hook('Downed-skater explicit Y keeper request',0xB244,6,'experimental/downed_y.s','_start:'),hook('Downed-Y native guarded helper',0x25CC00,314,'experimental/downed_y.s','_start:'),hook('Consistent actual-roster OVR presentation branch',0x25800C,4,'experimental/team_ovr.py','HOOK='),hook('Actual-roster presentation-only OVR helper',0x25CE00,136,'experimental/team_ovr.s','_start:')]

if base_path.exists() and plain_path.exists() and rom_path.exists():
    base=base_path.read_bytes();plain=plain_path.read_bytes();rom=rom_path.read_bytes()
    assert hashlib.md5(base).hexdigest()=='8f863ecb108c1d213953bada9b98f4f2'
    assert sha(rom)==meta['sha256'] and len(base)==len(rom)
    ranges=[];i=0;rebuilt=bytearray(base)
    while i<len(base):
        if base[i]==rom[i]:i+=1;continue
        start=i
        while i<len(base) and base[i]!=rom[i]:i+=1
        before=base[start:i];after=rom[start:i];ranges.append(dict(offset=f'0x{start:06X}',length=i-start,before=before.hex().upper(),after=after.hex().upper()));rebuilt[start:i]=after
    assert bytes(rebuilt)==rom
    diff=dict(version=meta['version'],base='Unmodified NHL 2026 - 94 Edition v5.1W',base_md5=hashlib.md5(base).hexdigest(),base_sha256=sha(base),output_sha256=sha(rom),scope='Every changed byte: code, roster and assets; ranges are not all CPU hooks',changed_bytes=sum(x['length'] for x in ranges),ranges=ranges)
    (root/'download/nhl94-byte-diff.json').write_text(json.dumps(diff,separators=(',',':'))+'\n')
    with (root/'download/nhl94-byte-diff.csv').open('w',newline='') as f:
        f.write('# version='+meta['version']+'; base_sha256='+sha(base)+'; output_sha256='+sha(rom)+'\n');writer=csv.DictWriter(f,fieldnames=['offset','length','before','after']);writer.writeheader();writer.writerows(ranges)
    for row in hooks:
        address=row['address'];size=row['size'];row.update(offset=f'0x{address:06X}',before=plain[address:address+size].hex(' ').upper(),after=rom[address:address+size].hex(' ').upper())
    hook_map=dict(version=meta['version'],guard_input_sha256=sha(plain),output_sha256=sha(rom),scope='Named forum-related hook sites; the complete original-to-current byte diff is separate',hooks=hooks)
    (root/'download/nhl94-forum-hooks.json').write_text(json.dumps(hook_map,indent=2)+'\n')
else:
    if not a.assets_only:raise SystemExit('Current ROM and canonical original/plain input required to refresh forum hex exports')
    print('Forum hex refresh skipped: canonical original/current ROM inputs unavailable')
    raise SystemExit(0)

if a.assets_only:
    print('Forum byte exports refreshed:',meta['version'],len(ranges),'ranges;',len(hooks),'hooks')
    raise SystemExit(0)

def details(body):return '<details class="forum-tech"><summary>Hooks, hex &amp; source code</summary>'+body+'</details>'
def table(indices):
    rows=[]
    for n in indices:
        r=hooks[n];rows.append('<tr><td>'+html.escape(r['title'])+'<br><code>'+r['offset']+'</code></td><td><code>'+r['before']+'</code></td><td><code>'+r['after']+'</code></td><td>'+source(r['source'],r['line_needle'])+'</td></tr>')
    return '<div class="tblwrap"><table class="tbl"><tr><th>Hook / ROM offset</th><th>Builder input</th><th>Current bytes</th><th>Code</th></tr>'+''.join(rows)+'</table></div>'

thread='https://forum.nhl94.com/index.php?/topic/40511-2-guys-one-ai/'
def card(slug,author,post,title,status,kind,body,technical,extra=''):
    return '<article class="forum-request" id="forum-'+slug+'" data-forum-search="'+html.escape((author+' '+title+' '+str(post)+' '+status).lower(),quote=True)+'"><div class="forum-request-head"><h3>'+html.escape(title)+'</h3><span class="forum-status '+kind+'">'+html.escape(status)+'</span></div><p class="forum-author"><b>'+html.escape(author)+'</b> · <a href="'+thread+'#comment-'+str(post)+'">Forum post #'+str(post)+'</a>'+extra+'</p>'+body+details(technical)+'</article>'

cards=[]
cards.append(card('77-settings','77',217611,'Season settings and penalty minutes','Fixed · 2026-10-28 / r3','fixed',
    '<p>Saved match settings stay locked after the first game and survive restarts. Played games save each player’s PIM in league and playoffs.</p>',
    '<p><b>Settings:</b> per-slot logical SRAM <code>1FF6–1FFD</code>; pre-match settings capture and validation in '+source('experimental/seasonplayers.s','startsettings:')+' and '+source('experimental/seasonplayers.s','loadsettings:')+'. Native game settings: <code>D050 D052 D056 D058 D072 F004</code>.</p><p><b>PIM:</b> code at <code>2EE000</code>; league SRAM <code>09A0 + 25×team</code>, playoff SRAM <code>07C0 + 25×seed slot</code>. '+source('experimental/pim.s','game:')+' · '+source('season/season.s','0x2EE000')+' · <a href="notes.html#SETTINGS_LOCK.md">Tests and legacy-save limits</a>. Simulated games have no penalty model.</p>'))
cards.append(card('tickenest-mechanics','Tickenest',217601,'Goalie lock, runaway speed, penalty shots and passing','5 fixes released · 2026-10-28-r2','fixed',
    '<p>Boxed receivers are skipped; stale human speed flags are cleared; penalty-shot puck state and attacking awareness are corrected; short passes use a minimum flight time.</p><p><b>Current penalty-shot movement:</b> skate freely before the shooter first crosses the attacking blue line. After the first crossing, forward and sideways movement are legal; actual backward movement toward centre ice ends the attempt as a miss. Shooter position activates this rule, not puck position or facing. Awarded penalties, overtime and standalone shootouts cover both directions and CPU where relevant. <a href="notes.html#H32_FOOTER_RELEASE_20261104_R2.md">Current rules and qualification</a>.</p><p class="muted">The one-goal-deficit CPU goalie pull was already in v5.1W, credited to McMarkis.</p>',
    table(range(6,14))+'<p>'+source('experimental/forumfix.py',label='Guarded installer')+' · '+source('experimental/forumfix.s',label='68000 routines')+' · '+source('experimental/verify_forum_fixes.py',label='Native regression cases')+' · <a href="notes.html#FORUM_FIXES.md">Scope, tests and limitations</a>.</p><p>Helpers at <code>2EF000</code> (friction), <code>2EF004</code> (penalty award), <code>2EF008</code> (setup), <code>2EF00C</code> (pass timing). Jump bytes require the matching relocated code.</p>'))
cards.append(card('brodeur-diving','Brodeur30',217608,'Time to pass after a diving save','Released · confirmed on the forum','fixed',
    '<p>A diving catch gets the standing catch’s possession window. This timer edit is separate from the co-op goalkeeper controls.</p>',
    table([14])+'<pre><code>ROM 0x01531C\n35 7C 00 05 00 48  →  35 7C 00 B4 00 48\nmove.w #5,$48(a2)  →  move.w #180,$48(a2)</code></pre><p>'+source('experimental/patch_diving_recovery.py',label='Guarded standalone patch script')+' · <a href="tools/experimental/patch_diving_recovery.py" download>Download script</a> · <a href="notes.html#FORUM_FOLLOWUP.md">Instructions and limits</a>.</p><pre><code>python3 patch_diving_recovery.py input.bin output.bin</code></pre>',
    ' · <a href="'+thread+'#comment-217631">Brodeur30’s confirmation #217631</a>'))
cards.append(card('bcrt-credits','bcrt2000',217604,'Clear credits for the base hack and research','Added · 2026-10-28-r2','fixed',
    '<p>The website and startup card identify the base authors, inherited work and research. The card waits for Start.</p>',
    table([15])+'<p>Credit renderer <code>2D8000</code>, card data <code>2D0000</code>. '+source('experimental/credits.s',label='Credit routine')+' · '+source('experimental/prepare_boot_credits.py',label='Card generator')+' · '+source('experimental/build_test.py','credits =',label='Build integration')+' · <a href="#credits">Full attribution</a>.</p>'))
cards.append(card('floating-ai','floating',217606,'Stronger native Hard AI','Completed · accepted 38-win base','fixed',
    '<p>The current playtest is <b>'+meta['version']+'</b>. Its historical tactical base won <b>38 of 50</b> Edmonton Hard games against Chicago Normal, scoring <b>132–66</b>, with 25 home games per club. The matched r3 control won 32; the immediate parent won 36.</p><p><b>Completed and accepted by jdc.</b> The 38-win tactical base is accepted. Later maintenance includes native receiver positioning and a bounded containment-brake repair; the combined current version has no new win-rate qualification. Optional AI Shot Plan Off/On now implements jdc’s separate local request for covered-shot deferral and lateral native approaches; it makes no new strength claim. <a href="notes.html#AI_SHOT_PLAN_R3.md">Optional attack policy, every hook and exact tests</a>. The earlier stable 39/50 benchmark was not achieved and is archived; no further win-rate search is planned. A separate Penaltys100%/50%/Off row now complements the native Rules choice, preserving offside, icing and awarded penalty shots. Injury-only reports remain, and whole fighting incidents are gated before paired awards. <a href="notes.html#PENALTYS_R8.md">Percentage menu, native hooks, RNG, storage and tests</a>. Free CPU-teammate pickups now distribute control randomly among eligible friendly P1–P4 pads; existing human receivers and keeper requests retain their ownership. <a href="notes.html#FAIR_PICKUP_R6.md">Pickup hook, native RNG, guards and physical-pad input tests</a>. Pass Speed Normal/Slow now complements Pass Height Normal/Low, with Low height and Normal speed defaults, with independent per-season persistence/locking. Standalone preferences move out of Full goalie data to an excluded Light-bank gap. <a href="notes.html#PASS_SPEED_R5.md">Speed, preserved lift, storage and tests</a>. <a href="notes.html#PASS_HEIGHT_R4.md">Lift code and native tests</a>. Downed skaters are now rejected by both pass-target scans and the native stick-contact gate; body deflections and later recovered pickups remain. <a href="notes.html#DOWNED_RECEIVER_R3.md">Downed receiver hooks, native input tests and timing limits</a>. Shootout attempts now start automatically after a short countdown; pre-shot backward/stopped puck approaches end through the native miss event, while a legal unshot forward loose puck may be recovered. Native shot/retouch handling remains. <a href="notes.html#SHOOTOUT_RULES_R2.md">Rules, tolerances, hooks and native tests</a>. Goal nets now require50%more impact speed before the native release can trigger; a hard hit can still dislodge them. <a href="notes.html#NET_CONTACT_20261031.md">Net threshold code, reproduction and tests</a>. A manually controlled, puck-holding keeper now uses the native pass instead of an early player switch. <a href="notes.html#MANUAL_KEEPER_R13.md">Manual keeper code, tests and limits</a>. The fouled physical P1–P4 pad now controls its awarded penalty shot. <a href="notes.html#PENALTY_OWNER_R11.md">Penalty-shot controller code/tests</a>. Human buffered pass shots now wait for native stick contact and use the contact pose; a teammate cannot erase that request. <a href="notes.html#HUMAN_CONTACT_R10.md">Human contact code/tests</a>. Mini now has seven games, one against each divisional rival, including simulated fixtures. <a href="notes.html#MINI_SEVEN_R12.md">Seven-game code and tests</a>. <a href="notes.html#MINI_DIVISION_R9.md">Mini schedule code/tests</a>. Overtime follows the selected5/7/10/20-minute period length, and post-whistle shots/checks cannot build or bank momentum. <a href="notes.html#OVERTIME_MOMENTUM_R8.md">OT and momentum code/tests</a>. The retained keeper fix returns a previously manual keeper to CPU control on a catch and sends requested outlets to the current P1–P4 skater. Fresh defaults follow jdc’s clarified choices: Hard, Replay Off, Crit Off and Icing On, with Auto lines and Edmonton. <a href="notes.html#ICING_DEFAULT_R7.md">Icing On clarification</a>. <a href="notes.html#GOALIE_CATCH_R6.md">Catch control and defaults</a>. The Icing On/Off menu row and Edmonton startup/new-season defaults remain. <a href="notes.html#ICING_MENU_R5.md">Icing code and tests</a>. It also honors accepted keeper pass requests at the possession deadline. The r3 CPU keeper tracking/recovery correction remains; Crit locking, immediate B switching and the Stars panel repair from r2 remain. In 48 controlled approaches, goals fell from 10 to 4 and keeper catches rose from 15 to 22. This is not a new win-rate series.</p><p><b>Exact-build QA:</b> 88 of 89 checks passed. The inherited receiver-hold positioning check still fails, so the strict release gate remains FAILED. This is the same documented AI limitation retained in the user-selected playtest. Rebuild, exact directional-r7 rollback and IPS reconstruction pass. All40 current ordinary scenes are recorded against fresh exactr7controls;37 are raw-exact, and every difference remains recorded. Added native high-event admission instructions can change later HV timing even at100%. No unchanged universal trajectories are claimed. The parent controls retain the complete strict historical comparison chain. The retained net feature separately has38 of40 scenes raw-exact to r13. The other two remain exact until a verified light net impact; subsequent differences are recorded as part of this correction. Fresh r7/r6 controls retain the exact default-only comparison. The retained r6/r5 audit separately records35 raw-exact traces and15 bounded isolated video observations across5 scenes. The 40 fresh exact r2 controls still reproduce the immutable original traces; the retained keeper correction can differ from those older trajectories. <a href="notes.html#OUTLET_DEADLINE_R4.md">Pass deadline code and tests</a> · <a href="notes.html#GOALIE_LATERAL_R3.md">Keeper recovery code</a> · <a href="notes.html#AI_TACTICS.md">Historical AI base scope</a> · <a href="tools/notes/HARD_STRENGTH_RESULT.json">Base comparison</a>.</p>',
    table(range(16,maintenance_start))+'<p><b>Related jdc maintenance hooks:</b> current menu, controls, presentation and keeper corrections.</p>'+table(range(maintenance_start,len(hooks)))+'<p>'+source('experimental/ai_defence.s',label='Weak-side coverage')+' · '+source('experimental/ai_gap.s',label='Defensive pair')+' · '+source('experimental/ai_attack.s',label='Attack/pass decisions')+' · '+source('experimental/ai_playbook.s',label='Pressure and catch/release')+' · '+source('experimental/coach_goalie.s',label='Keeper selection')+' · '+source('experimental/coach_lines.s',label='First forward unit')+'.</p><p>All rejected ROMs, source and test results are retained. A private receiver correction won 39/50 once but only 31/50 in its independent confirmation. The final private central-pass/close-finish variant passed all 74 exact-source checks and seven independent build/rollback/IPS comparisons, but won only 33/50 (94–62); its matched Native control won 32 and r3 won 29. All 150 games completed, with 25 home games per club. All 100 new/control saves passed the statistics audit; four old r3 scorer errors remain recorded. No further confirmation or win-rate experiment is planned. Private QA successes do not approve the different released 38-win ROM. <a href="notes.html#AI_STRENGTH_WIP.md">Full investigation history</a> · <a href="tools/notes/HARD_STRENGTH_PROGRESS.json">Progress data</a>.</p><p><b>Hard AI development: Codex GPT-6.1 Sol on High.</b> A little over 20 hours were spent on Hard experiments and testing, according to jdc’s estimate; configuration and time are user-reported. Released maintenance: Crit Shots locking, immediate B player switching and the reported Stars of the Game graphics correction. The lateral-carry keeper correction is released. Independent AI Shot Plan Off/On, default Off, now covers jdc’s separate shot-zone/native-pass/lateral-approach request while preserving that keeper repair; it makes no new win-rate claim. Human playtests of saved AI variants come after the other work, one at a time. <a href="notes.html#TODO.md">TODO list</a>.</p><p>These maintenance hooks implement jdc’s playtest reports. They require their matching relocated modules.</p>'))
cards.append(card('brodeur-pass-controls','Brodeur30',217609,'Pass-only lift and speed controls','Targeted lift/speed released · r5','fixed',
    '<p>Pass Height Normal/Low and Pass Speed Normal/Slow are released, with Low height and Normal speed defaults, for targeted B passes on both teams. Slow reruns the native moving-target calculation at lower nominal power while preserving the chosen lift. Seasons save and lock both choices; standalone preferences use a separate safe record. Assisted/CPU blind passes retain their original path. Human Directional and vector cone choices are released r7; Pickup Delay Off/On is released, default Off; it adds a minimum six native updates of skater contact cooldown after an ordinary human B release.</p>',
    table(range(height_hook_start,pickup_hook_start))+'<p>Existing targeted-pass path <code>BEBC–BFBA</code>; flight/lift fields <code>BF5E/BF6A</code>. '+source('patch.py','Fix 3:',label='Existing pass code')+' · '+source('experimental/forumfix.s','passflight:',label='Released short-pass correction')+' · <a href="notes.html#FORUM_FOLLOWUP.md">Research and isolation requirements</a>. <a href="notes.html#PASS_HEIGHT_R4.md">Lift hook and historical r4 details</a> · <a href="tools/notes/PASS_HEIGHT_RESULT.json">Exact evidence</a>. <a href="notes.html#PASS_SPEED_R5.md">Released speed hook, combined records and storage regression</a>. Directional/cone controls are released r7. Hold/release power is released as Pass Charge Off/On, default Off. Pickup Delay Off/On is released, default Off. During its short active cooldown skaters cannot take stick possession or deflect the puck with their body; goalkeepers retain native saves. <a href="notes.html#PICKUP_DELAY_R11.md">Behavior, hooks, persistence and tests</a>.</p>'))
cards.append(card('brodeur-directional','Brodeur30',217634,'Directional passing and adjustable targeting cone','Mode/cone/charge released','fixed',
    '<p>Optional start-menu Pass Mode: Assisted/Directional, default Assisted, and Target Cone: Default/15/30/45 degrees, default Default, are released. Directional uses the actual eight-way aim without choosing a receiver; explicit cones use real vector half-angles for Assisted. Height/Speed remain independent, with per-season saving and first-match locks. CPU keeper requests keep their requested teammate. Pass Charge Off/On, default Off, now waits for human B release and scales native power25–100%. The choice saves separately for native play and every season, with the same first-match lock.</p>',
    table(range(directional_hook_start,penalty_rate_hook_start))+table(range(charge_hook_start,pickup_delay_hook_start))+table(range(pickup_delay_hook_start,alt_goalies_hook_start))+'<p>CPU pass and scan instruction paths remain exact r6. The shared-path prototype was rejected after a native AI regression; the r7 human-only design restored all40 ordinary trajectories in its r7 comparison. The current ordinary-scene comparison is recorded against fresh exact Nov1r4 controls; the earlier r11 and prior comparisons remain independent. See the current exact-build QA report for counts and every retained difference. <a href="notes.html#PASS_MODE_R7.md">Behavior, persistence, rejected attempts and tests</a> · <a href="tools/notes/PASS_MODE_RESULT.json">Exact-build evidence</a>. Hold/release B power and independent Pickup Delay Off/On are released. Pickup Delay defaults Off; when On, an ordinary human B release applies six native updates of skater contact cooldown. Goalkeepers, existing longer locks and requested CPU keeper outlets retain their paths. <a href="notes.html#PICKUP_DELAY_R11.md">Pickup delay code and exact tests</a>. <a href="notes.html#PASS_CHARGE_COLORS_R10.md">Charged input, storage, native physics and qualification</a> · <a href="tools/notes/PASS_CHARGE_COLORS_RESULT.json">Charged-passing r10 evidence</a> · <a href="tools/notes/PICKUP_DELAY_RESULT.json">Original feature evidence</a>.</p>',
    ' · <a href="'+thread+'#comment-217609">Original proposal #217609</a>'))
cards.append(card('floating-rebounds','floating',217606,'Alternative goalie reach and rebound model','Model released · default On','fixed',
    '<p><b>Alt Goalies: Off/On</b>, default On, is released. On adds reach21 and experimental catches, cornerward and short/long rinkward rebounds, plus occasional slow/fast goalward returns. Shot origin and screening change explicit experimental odds. Only a proven real shot gets the new classification; native movement, saves, outlets and eventual goals remain responsible for the outcome. Both clubs/ends and P1–P4 are covered. Each season saves and locks the choice after match one.</p>',
    table(range(alt_goalies_hook_start,strict_so_hook_start))+'<p>Native save chains, conservative rebound speed caps, accepted-possession provenance, scorer/keeper/SOG accounting, real advancing physics, cold boots and input tests are recorded. No human-difficulty or real-world probability claim. Off retains the established model; all passing options stay independent. <a href="notes.html#ALT_GOALIES_20261101.md">Behavior, hooks, fields, exact tests and limitations</a> · <a href="tools/notes/ALT_GOALIES_RESULT.json">Exact-build evidence</a>.</p>'))

# Render the actual current matrix on the Hard card; never carry a historical
# hardcoded pass count into a later release.
qa=json.loads((tools/'notes/QA_STATUS.json').read_text());assert qa['rom_sha256']==meta['sha256']
checks=qa['checks'];passed=sum(x['returncode']==0 for x in checks);failed=[x['test'] for x in checks if x['returncode']];maint=qa.get('maintenance_replay',{})
required=int(qa.get('required_count',qa.get('required',len(checks))))
qualified=qa['status']=='passed' and len(checks)==required and not failed
qa_text='<p><b>Exact-build QA:</b> '+str(passed)+' of '+str(required)+' required checks passed on '+meta['version']+'. Strict release gate: '+('PASSED'if qualified else'RUNNING'if qa['status']=='running'else'FAILED')+'.'
if failed:qa_text+=' Failing checks: '+html.escape(', '.join(failed))+'.'
elif qualified:qa_text+=' Every required suite executed freshly, including transfers, native overview, Allstar CPU/humans, controls, HUD/replay and friendly-pass catch regression checks.'
else:qa_text+=' Qualification is still running; unfinished checks are not passes.'
qa_text+=' Native replay/trajectory comparisons retain every difference and the complete historical chain; no universal unchanged timing or new Hard win rate is claimed. <a href="tools/notes/QA_STATUS.json">Exact current matrix</a> · <a href="notes.html#H32_FOOTER_RELEASE_20261104_R2.md">Current behavior, pricing, hooks and limits</a> · <a href="audits/h32-20261104-r2/">Opened current-version screenshots</a>.</p>'

cards=[re.sub(r'<p><b>Exact-build QA:</b>.*?</p>',lambda _:qa_text,c,flags=re.S) for c in cards]
cards=[c.replace('>Exact-build evidence</a>','>Original feature evidence</a>') for c in cards]

section='''  <section id="forum" class="panel">
    <h2>Forum requests, fixes &amp; code</h2>
    <p>Find your name or suggestion from <a href="'''+thread+'''">2 guys one ai</a>. Each item links to the original post, its status and the implementation.</p>
    <div class="dl"><a class="btn btn-dl" href="download/nhl94-tools.zip" download>All code &amp; tools (ZIP)</a><a class="btn btn-dl alt" href="download/nhl94-forum-hooks.json" download>Forum hooks &amp; hex (JSON)</a><a class="btn btn-dl alt" href="download/nhl94-byte-diff.csv" download>Every changed byte (CSV)</a><a class="btn btn-dl alt" href="download/nhl94-byte-diff.json" download>Every changed byte (JSON)</a></div>
    <p class="muted">Current source: '''+meta['version']+''' · <a href="notes.html#HACKING.md">All engine hooks and ROM/RAM notes</a> · <a href="tools/source.php?file=experimental/build_test.py">Complete builder</a> · <a href="tools/notes/QA_STATUS.json">Exact-build test report</a>.</p>
    <div class="forum-controls"><label for="forum-search">Find by name or suggestion</label><input type="search" id="forum-search" placeholder="Brodeur30, passing, 77, goalie…"><button type="button" id="forum-expand">Show all technical details</button></div>
    <p class="forum-hex-note muted">Hex tables show file offsets, checked builder-input bytes and current bytes. Relocated hooks need their accompanying code. The complete byte diff uses the unmodified v5.1W base and includes code, rosters and assets; its ROM hashes identify the exact build.</p>
'''+''.join(cards)+'''<p id="forum-no-results" hidden>No matching request. Try a name or a shorter search.</p>
    <p class="muted">Status is maintained here. A source link is an implementation reference, not a claim that an open proposal is finished.</p>
  </section>
'''

if not a.assets_only:
    p=root.parent/'system/index.html';text=p.read_text();start=re.search(r'<section\b(?=[^>]*\bid="forum")[^>]*>',text).start();end=re.search(r'<section\b(?=[^>]*\bid="credits")[^>]*>',text[start:]).start()+start;text=text[:start]+section+'\n'+text[end:]
    if 'id="forum-filter-script"' not in text:
        script='''<script id="forum-filter-script">
(() => {const field=document.getElementById('forum-search'),items=[...document.querySelectorAll('.forum-request')],toggle=document.getElementById('forum-expand');if(!field)return;field.addEventListener('input',()=>{const q=field.value.trim().toLowerCase();items.forEach(item=>item.hidden=!item.dataset.forumSearch.includes(q));document.getElementById('forum-no-results').hidden=items.some(item=>!item.hidden);});toggle.addEventListener('click',()=>{const open=toggle.getAttribute('aria-expanded')!=='true';document.querySelectorAll('.forum-tech').forEach(d=>d.open=open);toggle.setAttribute('aria-expanded',String(open));toggle.textContent=open?'Hide technical details':'Show all technical details';});})();
</script>
'''
        text=text.replace('</body>',script+'</body>')
    strict_table='<!-- strict-so-hooks:start -->'+table(range(strict_so_hook_start,shot_plan_hook_start))+'<!-- strict-so-hooks:end -->'
    text=re.sub(r'<!-- strict-so-hooks:start -->.*?<!-- strict-so-hooks:end -->',lambda _:strict_table,text,flags=re.S)
    shot_table='<!-- shot-plan-hooks:start -->'+table(range(shot_plan_hook_start,faceoff_dots_hook_start))+'<!-- shot-plan-hooks:end -->'
    text=re.sub(r'<!-- shot-plan-hooks:start -->.*?<!-- shot-plan-hooks:end -->',lambda _:shot_table,text,flags=re.S)
    dot_table='<!-- faceoff-dots-hooks:start -->'+table(range(faceoff_dots_hook_start,r5_hook_start))+'<!-- faceoff-dots-hooks:end -->'
    text=re.sub(r'<!-- faceoff-dots-hooks:start -->.*?<!-- faceoff-dots-hooks:end -->',lambda _:dot_table,text,flags=re.S)
    r5_table='<!-- momentum-left-hooks:start -->'+table(range(r5_hook_start,len(hooks)))+'<!-- momentum-left-hooks:end -->'
    text=re.sub(r'<!-- momentum-left-hooks:start -->.*?<!-- momentum-left-hooks:end -->',lambda _:r5_table,text,flags=re.S)
    p.write_text(text)
print('Forum exports',meta['version'],len(ranges),'changed ranges;',len(hooks),'named hooks;',len(cards),'attributed requests')
