-- Stock-ROM paired-input study. All harness writes are pre-trial fixture setup. -- Memory taps observe CPU writes; they never return replacement data. local C=dofile(assert(os.getenv('MASH_CONFIG'))) local OUT=assert(os.getenv('MASH_OUT')) local machine=manager.machine local cpu=machine.devices[':maincpu'] local mem=cpu.spaces.program local function r8(a) return mem:read_u8(a) end local function r16(a) return mem:read_u16(a) end local function r32(a) return mem:read_u32(a) end local function json(v) if v==nil then return 'null' end if type(v)=='number' or type(v)=='boolean' then return tostring(v) end if type(v)=='string' then return '"'..v:gsub('\\','\\\\'):gsub('"','\\"'):gsub('\n','\\n')..'"' end local a={};if #v>0 then for _,x in ipairs(v) do a[#a+1]=json(x) end;return '['..table.concat(a,',')..']' end for k,x in pairs(v) do a[#a+1]=json(k)..':'..json(x) end table.sort(a);return '{'..table.concat(a,',')..'}' end local events=assert(io.open(OUT..'/events.jsonl','w')) local observations=assert(io.open(OUT..'/observations.jsonl','w')) local writes=assert(io.open(OUT..'/setup-writes.jsonl','w')) local results={schemaVersion=1,engine='MAME '..emu.app_version(),configuration=C,trials={}} local f,t,trial,native,eventid=0,0,0,0,0 local phase,live,stage_at='boot',nil,0 local entered,exit_at,hits,chain,peak_chain,peak_byte=false,nil,0,0,0,0 local credit_spends,timer_decrements,timer_reimbursements=0,0,0 local mode,initial=nil,nil local function state() return {callbackIndex=t,nativeInputIndex=native,b0=r8(0xff30b0),state=r16(0xff3008),selector=r8(0xff30e6), held=r16(0xff30d0),previous=r16(0xff30d2),edge=r16(0xff30d4),human=r8(0xff3003),defenderHuman=r8(0xff3403),gate=r8(0xff32b9), timer=C.family=='gold' and r8(0xff304e) or r16(0xff304e),credits=C.family=='ryu' and r16(0xff3382) or C.family=='chun' and r16(0xff3398) or nil, missilePhase=C.family=='gold' and r8(0xff3050) or nil,combo=r8(0xff3120),hp=r16(0xff3670),x1=r16(0xff300c),x2=r16(0xff340c), y1=r16(0xff3010),y2=r16(0xff3410),vy2=r32(0xff341c),defenderState=r16(0xff3408),roundWord=r16(0xff4008)} end local function event(kind,pc,a,before,after,width,mask) eventid=eventid+1 local e={id=eventid,trial=trial,mode=mode,kind=kind,pc=pc,address=a,before=before,after=after,width=width,mask=mask, registers=state(),externalHitOrdinal=hits,externalComboOrdinal=chain,firstMoveActive=entered and not exit_at} events:write(json(e),'\n') end local function setup(a,v,width,reason) assert(phase~='running','forbidden gameplay write') local old=width==1 and r8(a) or r16(a) if old==v then return end writes:write(json({callback=f,phase=phase,address=a,width=width,before=old,after=v,reason=reason}),'\n') if width==1 then mem:write_u8(a,v) else mem:write_u16(a,v) end end local fields={} for _,port in pairs(machine.ioport.ports) do for name,field in pairs(port.fields) do fields[name]=field end end local function clear() for name,field in pairs(fields) do if name:match('^P[12] ') or name:match('Start') or name:match('Coin') then field:set_value(0) end end end local function press(name) assert(fields[name],name):set_value(1) end local PC_KIND={ [0x047abe]='credit_spend',[0x047ac4]='timer_decrement',[0x04e44e]='credit_spend',[0x04e454]='timer_decrement', [0x02a9d8]='timer_reimbursement',[0x02a944]='timer_decrement',[0x02a93a]='forced_timer_exit', [0x047926]='timer_init',[0x04e35a]='timer_init',[0x02a8e2]='timer_init', [0x047902]='credits_init',[0x04e348]='credits_init',[0x02a90e]='missile_phase', } local tap tap=mem:install_write_tap(0xff3000,0xff37ff,'mash_forensic',function(a,d,mask) if phase~='running' then return end local pc=cpu.state.CURPC.value&0xffffff local kind=PC_KIND[pc] if pc==0x0136c0 and a==0xff30d4 then native=native+1;kind='input_sample' end if pc==0x01d1a8 and a==0xff3120 then kind='connected_hit' end if a==0xff3670 then kind='defender_health_write' end if not kind then return end local before=r16(a);local after=(before&(~mask&0xffff))|(d&mask) local addr,width=a,2 if mask==0xff00 then before=before>>8;after=after>>8;width=1 elseif mask==0x00ff then before=before&255;after=after&255;addr=a+1;width=1 end if r8(0xff30b0)==42 and r8(0xff30e6)==C.selector then entered=true end if kind=='connected_hit' and entered then hits=hits+1;chain=before==0 and 1 or chain+1;peak_chain=math.max(chain,peak_chain) end if entered and not exit_at then if kind=='credit_spend' then credit_spends=credit_spends+1 end if kind=='timer_decrement' then timer_decrements=timer_decrements+1 end if kind=='timer_reimbursement' then timer_reimbursements=timer_reimbursements+1 end end event(kind,pc,addr,before,after,width,mask) end) local function load_trial() phase='loading';clear();machine:load(OUT..'/fixture.sta') end local postload postload=emu.add_machine_post_load_notifier(function() if phase~='loading' then return end t,native,eventid=0,0,0;entered=false;exit_at=nil;hits,chain,peak_chain,peak_byte=0,0,0,0 credit_spends,timer_decrements,timer_reimbursements=0,0,0 mode=C.modes[trial];initial=state();phase='running';event('load_complete',nil,nil,nil,nil,nil,nil) end) local function finish_trial(reason) clear() results.trials[#results.trials+1]={trial=trial,mode=mode,initial=initial,final=state(),firstMoveExit=exit_at,entered=entered, peakNativeCombo=peak_byte,externalConnectedHits=hits,externalPeakCombo=peak_chain,creditSpends=credit_spends, timerDecrements=timer_decrements,timerReimbursements=timer_reimbursements,stopReason=reason} observations:flush();events:flush() print('TRIAL '..json(results.trials[#results.trials])) trial=trial+1 if trial>#C.modes then local h=assert(io.open(OUT..'/result.json','w'));h:write(json(results),'\n');h:close() events:close();observations:close();writes:close();phase='done';print('DONE');machine:exit() else load_trial() end end local function mash() local q=t-(C.commandDelay or 0) if C.jump and t>=1 and t<=4 then press('P2 Up') end if q>=1 and q<=4 then press('P1 Down') end if q>=5 and q<=8 then press('P1 Down');press('P1 Right') end if q>=9 and q<=12 then press('P1 Right');press(C.family=='chun' and 'P1 Button 4' or 'P1 Button 1');press(C.family=='chun' and 'P1 Button 5' or 'P1 Button 2') end if q<=12 or exit_at or not entered then return end local p=t+(C.inputPhase or 0) if mode=='hold_all' or (mode=='tap_all' and p%2==0) then for j=1,6 do press('P1 Button '..j) end elseif mode=='tap_lp' and p%2==0 then press('P1 Button 1') elseif mode=='rotate_buttons' then press('P1 Button '..(p%6+1)) elseif mode=='alternate_buttons' then press('P1 Button '..(p%2+1)) elseif mode=='directions' and p%2==0 then press('P1 '..({'Right','Down','Left','Up'})[math.floor(p/2)%4+1]) elseif mode=='directions_dense' then press('P1 '..({'Right','Down','Left','Up'})[p%4+1]) elseif mode=='hold_direction' then press('P1 Right') end end local notifier notifier=emu.add_machine_frame_notifier(function() f=f+1;clear() if f%500==0 then print('PROGRESS '..f..' '..phase..' '..r16(0xff4104));io.stdout:flush() end if phase=='boot' then if f>600 and f%90<5 then press('Coin 1');press('Coin 2') end if f>750 and f%30<5 and r16(0xff4104)~=4 then press('1 Player Start');press('2 Players Start') end if r16(0xff4104)==4 then setup(0xff40b0,C.attacker,2,'pre-match P1 selection fixture') setup(0xff40b4,C.defender,2,'pre-match P2 selection fixture') if f%24<4 then press('P1 Button 1');press('P2 Button 1') end end if r16(0xff4104)==8 and r16(0xff4000)==0x0800 and r8(0xff3003)==1 and r8(0xff3403)==1 then assert(r16(0xff3052)==C.attacker and r16(0xff3452)==C.defender,'wrong native fighter initialization') live=f;phase='settle';print('LIVE '..C.name) elseif f>5500 then error('match initialization timeout') end return elseif phase=='settle' then if f-live==360 then setup(0xff3274,3,1,'legal full super meter');setup(0xff3272,0,2,'meter fraction') setup(0xff3270,144,2,'attacker HP fixture');setup(0xff3670,C.hp or 144,2,C.hp and C.hp>144 and 'high-health duration diagnostic' or 'ordinary-health fixture') setup(0xff300c,C.x1,2,'pre-save horizontal position');setup(0xff340c,C.x2,2,'pre-save horizontal position') end if f-live==390 then machine:save(OUT..'/fixture.sta');phase='saving';stage_at=f end return elseif phase=='saving' then if f-stage_at>=5 then trial=1;load_trial() end return elseif phase~='running' then return end t=t+1 local active=r8(0xff30b0)==42 and r8(0xff30e6)==C.selector if active then entered=true end if entered and not active and not exit_at then exit_at=t;event('first_move_exit',nil,nil,nil,nil,nil,nil) end peak_byte=math.max(peak_byte,r8(0xff3120)) local row=state();row.trial=trial;row.mode=mode;row.externalConnectedHits=hits;observations:write(json(row),'\n') mash() if t==1 or t==130 or (exit_at and t==exit_at+1) then machine.screens[':screen']:snapshot(OUT..'/trial-'..trial..'-'..t..'.png') end if exit_at and t>=exit_at+(C.tail or 100) then finish_trial('first move exited and child-tail window elapsed') elseif t>=(C.maxCallbacks or 650) then finish_trial('bounded callback limit') elseif not entered and t>180+(C.commandDelay or 0) then finish_trial('failed to enter expected move') end end) _MASH_CORE_HANDLES={tap=tap,postload=postload,notifier=notifier}