experimental/allstar.py

Download file

Release 2026-11-04-r2 · SHA256 3e647eaea53b95ba34a8b146066d971dc220534dec2d41731704d93637ff5997 · Click a line number for a direct link.

1"""Best-by-position Allstar rosters, conditional native getters and real portraits.23Base club records/tables, statistics code and SRAM remain unchanged. Two virtual4clubs use cosmetic ANA/BOS shells only while the Allstar exhibition is active.5"""6from pathlib import Path7import json,struct8from player_cards import identity9HERE=Path(__file__).resolve().parent10ENTRY=0x22C000;TABLE=0x22C800;PHOTO_TABLE=0x22C880;CRIT=0x22C900;GK=0x22CC40;UIDS=0x22CD0011RECORDS=[0x22D000,0x22D800];PHOTOS=[0x22E000,0x22E0A0]1213def catalogue(data):14 meta=json.loads((HERE/'allstar_catalog.json').read_text())['players'];cards=json.loads((HERE/'player_cards/manifest.json').read_text())['players'];result=[]15 for team in range(32):16  ptr=struct.unpack_from('>I',data,0x30e+4*team)[0];off=ptr+struct.unpack_from('>H',data,ptr)[0]17  for slot in range(26):18   n=struct.unpack_from('>H',data,off)[0];name=data[off+2:off+n].rstrip(b'\0').decode('latin1');m=meta[name];key=identity(name);assert cards[key]['nhl_id']==m['nhl_id'];ratings=[int(c,16)for c in data[off+n+1:off+n+8].hex()];goalie=ratings[6]==ratings[8]==ratings[9]==0;assert goalie==(m['position']=='G')19   mask=struct.unpack_from('>I',data,0x19582 if goalie else 0x19420)[0]>>16;weights=data[0xfab2c:0xfab3c]if goalie else data[0xfab1c:0xfab2c];raw=sum(ratings[13-bit]*weights[bit]//2 for bit in range(14)if mask>>bit&1 and bit not in[13,6]);overall=100*(raw-1 if raw>=100 else raw)//max(100,raw)20   result.append(dict(uid=team*26+slot,team=team,slot=slot,name=name,nhl_id=m['nhl_id'],position=m['position'],record=off,record_size=n+8,overall=overall,raw_overall=raw));off+=n+821  assert off==ptr+struct.unpack_from('>H',data,ptr+4)[0]-222 return result2324def select(players):25 unique={}26 for z in players:27  old=unique.get(z['nhl_id'])28  if old is None or(z['position']!='F',z['overall'],z['raw_overall'])>(old['position']!='F',old['overall'],old['raw_overall']):unique[z['nhl_id']]=z29 teams=[[],[]];limits={'G':6,'D':16,'C':10,'LW':10,'RW':10}30 for pos,count in limits.items():31  group=sorted([z for z in unique.values()if z['position']==pos],key=lambda z:(-z['overall'],-z['raw_overall'],z['nhl_id']))[:count];assert len(group)==count32  for i,z in enumerate(group):teams[[0,1,1,0][i%4]].append(z)33 order={'G':0,'LW':1,'C':2,'RW':3,'D':4}34 teams=[sorted(row,key=lambda z:(order[z['position']],-z['overall'],-z['raw_overall'],z['nhl_id']))for row in teams]35 assert len({z['nhl_id']for row in teams for z in row})==5236 for row in teams:assert len(row)==26 and all(sum(z['position']==pos for z in row)==count//2 for pos,count in limits.items())37 return teams3839def string(text):40 b=text.encode('ascii');b+=b'\0'*(len(b)%2);return struct.pack('>H',len(b)+2)+b4142def record(data,shell,row,label):43 ptr=struct.unpack_from('>I',data,0x30e+4*shell)[0];n=struct.unpack_from('>H',data,ptr)[0];head=bytearray(data[ptr:ptr+n]);assert n==14644 # These are the virtual teams' own home/away uniform palettes only.45 # Keep skin/neutral/skate entries0/1/2/6/7/15; cloth, helmet and pants use46 # monochrome black/grey or red shades, in BOTH native uniform banks.47 shades=([0x0000,0x0000,0x0222,0x0000,0x0000,0x0222,0x0000,0x0222,0x0000,0x0444] if shell==0 else [0x000e,0x0006,0x000a,0x000e,0x000e,0x000a,0x0006,0x000e,0x0006,0x000e])48 for bank in range(2):49  for idx,color in zip([3,4,5,8,9,10,11,12,13,14],shades):struct.pack_into('>H',head,0x0c+bank*32+idx*2,color)50 head[0x4f]=0xf8;head[0x50:0x52]=bytes.fromhex('fff0') # 15F,8D and3G, with actual native count routines checked.51 lines=[]52 for d,l,c,r in[(18,3,8,13),(18,3,8,13),(20,4,9,14),(22,5,10,15),(18,3,8,13),(20,4,9,14),(18,3,8,13),(22,5,10,15)]:53  line=[1,d+1,d+2,l+1,c+1,r+1,l+2,0];assert len(set(line[:6]))==6;lines+=line54 head[0x52:0x92]=bytes(lines);body=b''.join(data[z['record']:z['record']+z['record_size']]for z in row);struct.pack_into('>H',head,4,n+len(body)+2)55 return bytes(head)+body+b'\0\2'+string('Allstar '+label)+string('AS'+label)+string('All-Stars')+string('Allstar Arena')5657def install(data,patch,assemble,gk_address):58 players=catalogue(data);teams=select(players);base_table=bytearray(data[0x30e:0x38e]);photo_table=bytearray(data[0x38e:0x40e]);crit=bytearray(data[0x2ff800:0x2ff800+832]);gk=bytearray(data[gk_address:gk_address+128]);uids=b''59 def put(at,blob):patch(at,b'\xff'*len(blob),bytes(blob))60 for i,row in enumerate(teams):61  blob=record(data,i,row,'AB'[i]);assert len(blob)<=0x800;put(RECORDS[i],blob);struct.pack_into('>I',base_table,4*i,RECORDS[i]);struct.pack_into('>I',photo_table,4*i,PHOTOS[i]);photos=bytearray()62  for slot,z in enumerate(row):63   old=struct.unpack_from('>I',data,0x38e+4*z['team'])[0];matches=[struct.unpack_from('>I',data,old+6*j)[0]for j in range(26)if struct.unpack_from('>H',data,old+6*j+4)[0]==z['slot']];assert len(matches)==1;photos+=struct.pack('>IH',matches[0],slot);crit[26*i+slot]=data[0x2ff800+z['uid']];uids+=struct.pack('>H',z['uid'])64  photos+=bytes(4);put(PHOTOS[i],photos);gk[4*i:4*i+4]=bytes([0,1,3,0])65 put(TABLE,base_table);put(PHOTO_TABLE,photo_table);put(CRIT,crit);put(GK,gk);put(UIDS,uids);put(0x22CD68,struct.pack('>2H',*(sum(z['overall']for z in row)//26 for row in teams)));code=assemble('allstar',ENTRY);assert len(code)<=0x800;put(ENTRY,code)66 hooks=[]67 def call(at,old,entry):68  new=bytes.fromhex('4eb9')+struct.pack('>I',ENTRY+entry)+bytes.fromhex('4e71')*((len(old)-6)//2);assert len(new)==len(old);patch(at,old,new);hooks.append(dict(address=at,length=len(old),entry=ENTRY+entry))69 patch(0xFE172,bytes.fromhex('48e77ffe302a0028'),bytes.fromhex('4ef9')+struct.pack('>I',ENTRY+76)+bytes.fromhex('4e71'));hooks.append(dict(address=0xFE172,length=8,entry=ENTRY+76,kind='tail-jump'))70 # Complete six-byte original MOVEA.L instructions.71 for at in[0xf8a3e,0xf99f8,0xf9a2c,0xfa370,0xfa888,0xfa952,0xfae56,0xfb152,0xfc58c,0xfc600,0xfcc62]:72  old=bytes(data[at:at+6]);assert old[2:]==struct.pack('>I',0x30e);entry={0x207c:8,0x227c:12,0x247c:16,0x2a7c:20}[int.from_bytes(old[:2],'big')];call(at,old,entry)73 # Function-entry replacement must tail-jump: preserve the caller's original return.74 patch(0x8078,bytes.fromhex('48e7e0c03f38bf78'),bytes.fromhex('4ef9')+struct.pack('>I',ENTRY+72)+bytes.fromhex('4e71'));hooks.append(dict(address=0x8078,length=8,entry=ENTRY+72,kind='tail-jump'))75 for at,old,entry in[(0x8250,'327c030ee540',40),(0x1345e,'327c030ee540',40),(0x171c2,'307c030ee540',36),(0xf7c7c,'327c030ee543',44),(0x132e4,'327c030e22710000',48),(0x1330a,'327c030e22711000',52),(0x17c1e,'307c030e3012',56)]:call(at,bytes.fromhex(old),entry)76 for at in[0xf8948,0xfae2a]:call(at,bytes.fromhex('207c0000038e'),24)77 def unique(pattern,lo,hi,entry):78  old=bytes.fromhex(pattern);at=data.find(old,lo,hi);assert at>=0 and data.find(old,at+len(old),hi)<0,(pattern,hex(lo));call(at,old,entry)79 if data[0x2ff400:0x2ff800]!=b'\xff'*0x400:80  unique('41f9002ff800',0x2ff400,0x2ff800,28)81  unique('41f8030e226a001e',0x2ff400,0x2ff800,60)82 if data[0x222000:0x223000]!=b'\xff'*0x1000:83  unique('41f8030ebab00000',0x222000,0x223000,64)84  unique('41f9'+struct.pack('>I',gk_address).hex(),0x222000,0x223000,32)85 if data[0x2d8c80:0x2d8d40]!=b'\xff'*0xc0:86  unique('41f9'+struct.pack('>I',gk_address).hex(),0x2d8c80,0x2d8d40,32)87 if data[0x2df400:0x2df800]!=b'\xff'*0x400:88  unique('43f9'+struct.pack('>I',gk_address).hex(),0x2df400,0x2df800,68)89 return dict(selection='Top6G/16D/10C/10LW/10RW by native zero-hot-cold overall, raw score then NHLID tiebreak; snake A,B,B,A within each natural position. Specific-position alias preferred;52unique NHLIDs.',records=RECORDS,tables=dict(roster=TABLE,photos=PHOTO_TABLE,critical=CRIT,goalies=GK,source_uids=UIDS,team_overall=0x22CD68),teams=teams,hooks=hooks,native_ordinary_records_untouched=True,cosmetics='Allstar A all-black, Allstar B all-red own uniform palettes in both banks. Original rink/centre-logo shells; actual Allstar A/B text replaces city title bitmaps. Team overall is floor(mean26 native player base-overalls). Real selected players/attributes/portraits/lines. Ordinary clubs unchanged outside mode.')90