import { test } from 'node:test'; import assert from 'node:assert/strict'; import * as e from './extract.mjs'; import * as rounds from './rounds.mjs'; import * as stages from './stages.mjs'; import * as portals from './portals.mjs'; import * as trailblaze from './trailblaze.mjs'; import * as jade from './jade.mjs'; import { indexed_entries, verify_dynamic_dispatch } from './native_codec.mjs'; import { text_rows } from './ui_names.mjs'; import { transaction, manifest, verify_bundle, replace } from './install_common.mjs'; const B=(...a)=>Buffer.from(a), v=e.encode_varint, join=e.cat; const throws=fn=>assert.throws(fn); test('truncation and overflow', ()=>{ for(const data of [B(128), Buffer.concat([Buffer.alloc(9, 255), B(2)])])throws(()=>new e.Cursor(data).varint()); }); test('invalid collection and bool', ()=>{ for(const [b, t]of [[B(3), 'uint[]'], [B(2), 'bool'], [B(4, 1), 'uint[]']])throws(()=>e.field(new e.Cursor(b), t)); }); test('unknown presence bit', ()=>throws(()=>e.record(new e.Cursor(B(2, 1)), [{ name:'ID', type:'uint' }]))); test('TextID signed hash and 64 bit hash', ()=>{ const cols=[{ name:'TraitName', type:'RPG.Client.TextID' }], row={ TraitName:{ hash:-2147483648, hash64:18446744073709551615n } }; assert.deepEqual(e.record(new e.Cursor(e.encode_record(row, cols)), cols), row); }); test('odds must be complete and normalized', ()=>{ const rows=Array.from({ length:11 }, (_, i)=>({ PlayerLevel:i+1, Rarity1Weight:100 })); assert.ok(e.valid_odds(rows)); rows[5].Rarity1Weight=99; assert.equal(e.valid_odds(rows), false); rows[5].Rarity1Weight=100; rows[5].PlayerLevel=7; assert.equal(e.valid_odds(rows), false); }); test('duplicate role IDs rejected', ()=>{ const rows=Array.from({ length:20 }, (_, i)=>({ ID:1000+i, AvatarID:1001, Rarity:1, SeasonID:1, TraitList:[1001] })); assert.ok(e.valid_roles(rows)); rows.at(-1).ID=rows[0].ID; assert.equal(e.valid_roles(rows), false); }); test('struct names are not assumed to be enums', ()=>throws(()=>e.field(new e.Cursor(B(1)), 'RPG.GameCore.GridFightUnrecognizedRow'))); test('TextID hash overflow', ()=>throws(()=>e.field(new e.Cursor(join(Buffer.alloc(5, 255), B(1, 1))), 'RPG.Client.TextID'))); test('signed enum distinct from uint', ()=>{ assert.equal(e.field(new e.Cursor(B(2)), 'uint'), 2); assert.equal(e.field(new e.Cursor(B(2)), 'RPG.GameCore.OfferingRewardType'), 1); assert.equal(e.field(new e.Cursor(B(1)), 'RPG.GameCore.GridFightNodeType'), -1); assert.deepEqual(e.encode_field(1, 'RPG.GameCore.OfferingRewardType'), B(2)); }); test('trait layers require coverage and unique thresholds', ()=>{ const rows=[{ TraitID:1001, Layer:2, MazebuffID:35100101 }, { TraitID:2501, Layer:3, MazebuffID:35100102 }]; assert.ok(e.valid_trait_layers(rows, new Set([1001]))); assert.equal(e.valid_trait_layers(rows, new Set([1001, 1002])), false); assert.equal(e.valid_trait_layers([...rows, rows[0]], new Set([1001])), false); }); test('trait bases require names and coverage', ()=>{ const r={ ID:1001, TraitName:{ hash:1, hash64:2 }, SeasonID:1, IconPath:'SpriteOutput/GridFight/1.png' }; assert.ok(e.valid_trait_bases([r], new Set([1001]))); assert.equal(e.valid_trait_bases([r, r], new Set([1001])), false); assert.equal(e.valid_trait_bases([r], new Set([1002])), false); }); test('TextMap missing or conflicting names', ()=>{ const key={ hash:-1, hash64:123 }, encoded=e.encode_field(key, 'RPG.Client.TextID'), entry=t=>join(B(3), encoded, e.encode_field(t, 'string')); assert.equal(e.text_matches(entry('Express Cohort'), key).text, 'Express Cohort'); for(const data of [B(), join(entry('first'), entry('different'))])throws(()=>e.text_matches(data, key)); }); test('season selection and panel must match', ()=>{ const d=[{ ID:10101, SeasonID:1 }, { ID:20101, SeasonID:2 }], m=[{ SeasonID:1, SubSeasonID:1, ActivityModuleID:7100201 }, { SeasonID:2, SubSeasonID:1, ActivityModuleID:7110101 }], a=[{ ActivityID:71002, ActivityPanelID:71002, ActivityModuleIDList:[7100201] }, { ActivityID:71101, ActivityPanelID:71101, ActivityModuleIDList:[7110101] }]; assert.deepEqual(e.season_contract(1, d, m, a).divisions, [10101]); assert.equal(e.season_contract(2, d, m, a).panel_id, 71101); throws(()=>e.season_contract(3, d, m, a)); throws(()=>e.season_contract(1, d, m, a.slice(1))); }); test('locator rejects unframed/conflicting collections', ()=>{ const cols=[{ name:'ID', type:'uint' }, { name:'Value', type:'uint' }], table=x=>join(v(4), e.encode_record({ ID:1, Value:x }, cols), e.encode_record({ ID:2, Value:10 }, cols)), tmp=e.fs.mkdtempSync(e.path.join(e.os.tmpdir(), 'gf-locator-')); try{ const blob=e.path.join(tmp, 'StarRail_Data/test.bytes'); e.fs.mkdirSync(e.path.dirname(blob)); const locate=()=>e.find_keyed_table(tmp, 'test.bytes', cols, 'test', 1, r=>r.length===2&&r[0].ID===1&&r[1].ID===2, { }, 3); e.fs.writeFileSync(blob, join(B(0), table(20))); assert.equal(locate().records, 2); e.fs.writeFileSync(blob, join(B(153), table(20))); throws(locate); e.fs.writeFileSync(blob, join(B(0), table(20), B(0), table(30))); throws(locate); } finally{ e.fs.rmSync(tmp, { recursive:true, force:true }); } }); test('item and avatar grants use numbered params', ()=>{ const [g, complete]=portals.parse_grants('Obtains #2 "" and #4 "."', [355100302, 1, 1501, 1]); assert.deepEqual(g, [{ kind:'item', count:1, id:355100302 }, { kind:'role', count:1, id:1501 }]); assert.equal(complete, true); }); test('conditional income not an immediate grant', ()=>assert.deepEqual(portals.parse_grants('Obtain #2 Gold Coin(s) for every #1 5-cost character(s) purchased.', [3, 3]), [[], false])); test('delayed clause reported but not granted', ()=>assert.deepEqual(portals.parse_grants('Obtains #1 "." After #2 Node(s), obtains #3 "."', [1, 4, 1]), [[{ kind:'item', count:1, id:350105 }], false])); test('unknown Obtain phrase grants nothing', ()=>assert.deepEqual(portals.parse_grants('Obtain 1 "furbo." It starts with #1 Gold Coin(s) as an initial fund.', [5]), [null, false])); test('fractional count rejected', ()=>assert.deepEqual(portals.parse_grants('Obtain #1[i] free refresh(es).', [0.5]), [null, false])); test('bare Obtain names one item', ()=>assert.deepEqual(portals.parse_grants('The "Energy" Bond\'s activated Ultimate effect can trigger 1 extra time. Obtain "" and #3[i] Gold Coin(s).', [35402057, 1, 5]), [[{ kind:'item', count:1, id:350208 }, { kind:'gold', count:5 }], false])); test('item param not granted fails', ()=>throws(()=>portals.check_grants(1, [{ kind:'gold', count:5 }], [350208, 5], new Set([350208])))); function roundFixture(){ return { GridFightStageRouteConfigRow:[{ ID:801, ChapterID:1, SectionID:1, NodeTemplateID:10 }, { ID:803, ChapterID:1, SectionID:1, NodeTemplateID:11 }], GridFightNodeTemplateConfigRow:[{ NodeTemplateID:10, StageID:100, NodeType:2, ParamList:[801101] }, { NodeTemplateID:11, StageID:101, NodeType:5, ParamList:[1] }], GridFightStageNodeDisplayConfigRow:[{ NodeType:2 }, { NodeType:5 }], StageRow:[{ StageID:100, MonsterList:[] }, { StageID:101 }], GridFightPlayerLevelConfigRow:[{ PlayerLevel:1, LevelUpExp:2 }, { PlayerLevel:2 }], GridFightLevelConfigRow:[{ GridFightLevel:1, LevelUpExp:999 }] }; } const project=t=>rounds.project(t, { 2:{ text:'Battle' }, 5:{ text:'Supply' } }, { 2:'CampMonster', 5:'Supply' }); test('round route identity survives repeated chapter/section', ()=>{ const [r]=project(roundFixture()); assert.deepEqual(r.map(r=>r.stage), ['1-1', '1-1']); assert.deepEqual(r.map(r=>r.route_id), [801, 803]); }); test('round Supply and monster group not invented', ()=>{ const [r]=project(roundFixture()); assert.equal(r[1].kind, null); assert.ok(r.every(r=>r.monster_group===null)); assert.deepEqual(r[0].raw.node_template.ParamList, [801101]); }); test('wrong level curve never used', ()=>{ const [, costs]=project(roundFixture()); assert.deepEqual(costs, { '1':2, '2':0 }); assert.ok(!('raw'in costs)); }); test('round missing stage fails', ()=>{ const t=roundFixture(); t.StageRow.pop(); throws(()=>project(t)); }); test('round duplicate native route rejected', ()=>{ const t=roundFixture(); t.GridFightStageRouteConfigRow.push({ ...t.GridFightStageRouteConfigRow[0] }); throws(()=>project(t)); }); test('round gap in levels rejected', ()=>{ const t=roundFixture(); t.GridFightPlayerLevelConfigRow[1].PlayerLevel=3; throws(()=>project(t)); }); test('truncated dynamic double fails', ()=>throws(()=>new rounds.Codec().dynamic(new e.Cursor(B(2, 0))))); test('unknown dynamic tag fails', ()=>throws(()=>new rounds.Codec().dynamic(new e.Cursor(B(14))))); test('signed/nested dynamic values round trip', ()=>{ const codec=new rounds.Codec(), value={ tag:4, value:[[{ tag:0, value:-1 }, { tag:3, value:[{ tag:5, value:'tutorial' }] }]] }, data=codec.encode_dynamic(value), cur=new e.Cursor(data); assert.deepEqual(codec.dynamic(cur), value); assert.equal(cur.at, data.length); }); test('named hash matches previously verified odds', ()=>assert.equal(rounds.asset_hash('BakedConfig/ExcelOutputGameCore/GridFightRarityWeight.bytes'), 0xeb77d1b5a1204b00n)); export function stageFixture(){ return { GridFightStageRouteConfigRow:[{ ID:801, ChapterID:1, SectionID:1, NodeTemplateID:10 }, { ID:1000, ChapterID:1, SectionID:1, NodeTemplateID:11 }], GridFightNodeTemplateConfigRow:[{ NodeTemplateID:10, StageID:70000001, NodeType:1, ParamList:[900] }, { NodeTemplateID:11, StageID:70000001, NodeType:3, ParamList:[113] }], StageRow:[{ StageID:70000001 }], GridFightStageNodeDisplayConfigRow:[{ NodeType:1, NodeName:{ hash:-1, hash64:2 } }, { NodeType:3, NodeName:{ hash:-3, hash64:4 } }], GridFightPlayerLevelConfigRow:[{ PlayerLevel:1, LevelUpExp:2 }, { PlayerLevel:2 }], GridFightLevelConfigRow:[{ GridFightLevel:1, LevelUpExp:99 }], GridFightTutorialStageConfigRow:[{ DivisionID:1, ForbiddenSellRoleList:[{ GAJICDKPHPA:1220 }] }], GridFightTutorialStageNodeConfigRow:[{ DivisionID:1, ChapterID:1, SectionID:1 }], GridFightDivisionStageRow:[{ DivisionID:1, ScoreRule:801 }] }; } test('named player curve and missing EXP', ()=>{ const o=stages.contracts(stageFixture()); assert.deepEqual(o['levels.json'], { '1':2, '2':0 }); assert.ok(!('LevelUpExp'in o['levels.raw.json'][1])); }); test('stage route identity and uncertain monster group preserved', ()=>{ const r=stages.contracts(stageFixture())['rounds.json']; assert.deepEqual(r.map(r=>r.route_id), [801, 1000]); assert.deepEqual(r.map(r=>r.stage), ['1-1', '1-1']); assert.ok(r.every(r=>r.monster_group===null)); assert.deepEqual(r[1].raw.GridFightNodeTemplateConfigRow.ParamList, [113]); }); test('first-play constant does not replace tutorial selector', ()=>{ const t=stageFixture(); t.ConstValueCommonRow=[{ ConstValueName:'GridFight_FirstPlay_StageRouteID', Value:{ wire_tag:0, value:803 } }]; const o=stages.contracts(t)['tutorial.json']; assert.equal(o.first_play_route_id, 803); assert.deepEqual(o.observed_start_gameplay.values, [1, 1]); }); test('stages DynamicValue signed integer/map round trip', ()=>{ const codec=new stages.Codec(), value={ wire_tag:4, value:[[{ wire_tag:0, value:-803 }, { wire_tag:3, value:[{ wire_tag:5, value:'raw' }, { wire_tag:2, value:false }] }]] }, wire=codec.encode_field(value, 'RPG.GameCore.DynamicValue'), cur=new e.Cursor(wire); assert.deepEqual(codec.field(cur, 'RPG.GameCore.DynamicValue'), value); assert.equal(cur.at, wire.length); assert.deepEqual(codec.field(new e.Cursor(B(12)), 'RPG.GameCore.DynamicValue'), {wire_tag:6,value:null}); throws(()=>codec.field(new e.Cursor(B(14)), 'RPG.GameCore.DynamicValue')); }); test('stage duplicate route rejected', ()=>{ const t=stageFixture(); t.GridFightStageRouteConfigRow.push({ ...t.GridFightStageRouteConfigRow[0] }); throws(()=>stages.contracts(t)); }); test('stage missing native stage rejected', ()=>{ const t=stageFixture(); t.StageRow=[]; throws(()=>stages.contracts(t)); }); test('obfuscated tutorial fields unchanged', ()=>assert.deepEqual(stages.contracts(stageFixture())['tutorial.json'].stages[0].raw.GridFightTutorialStageConfigRow.ForbiddenSellRoleList, [{ GAJICDKPHPA:1220 }])); const row=(mask, ...parts)=>join(v(mask), ...parts), table=rows=>join(B(0), v(rows.length*2), ...rows); test('Trailblaze goods and father chain', ()=>{ const data=table([row(7, v(101), v(6), join(v(2), v(110000021))), row(7|32|2048, v(201), v(6), join(v(2), v(110000022)), v(10), v(101)), row(7|32, v(501), v(10), join(v(4), v(122402), v(1)), v(15))]), goods=trailblaze.goods(trailblaze.decode(data)); assert.deepEqual(goods.map(g=>g.kind), ['maze_buff', 'maze_buff', 'role']); assert.equal(goods[1].father, 101); assert.deepEqual(goods[2].params, [122402, 1]); assert.equal(goods[0].cost, 0); }); test('Trailblaze rejects trailing bytes/missing father', ()=>{ const good=row(7, v(101), v(6), join(v(2), v(110000021))); throws(()=>trailblaze.decode(join(table([good]), B(0)))); const orphan=row(7|2048, v(201), v(6), join(v(2), v(1)), v(999)); throws(()=>trailblaze.goods(trailblaze.decode(table([orphan])))); }); // Additional coverage for Node-specific precision, publication and dependency ports. test('lossless JSON integers, numeric key order and Unicode formatting', ()=>{ const raw='{\n "9": 18446744073709551615,\n "1": "\\u0e01"\n}\n', parsed=e.parseJSON(raw); assert.equal(parsed[9], 18446744073709551615n); assert.equal(e.json(parsed, 1, true), raw); throws(()=>e.parseJSON('{}garbage')); }); test('DesignV rejects size mismatch, duplicate hashes and overlaps', ()=>{ const id='0123456789abcdef0123456789abcdef', b=Buffer.alloc(60); Buffer.from(id, 'hex').copy(b); b.writeBigUInt64BE(20n, 16); b.writeUInt32BE(2, 24); b.writeBigUInt64BE(1n, 28); b.writeUInt32BE(10, 36); b.writeBigUInt64BE(2n, 44); b.writeUInt32BE(10, 52); b.writeUInt32BE(10, 56); assert.equal(indexed_entries(b, id, 20).length, 2); throws(()=>indexed_entries(b, id, 21)); b.writeUInt32BE(9, 56); throws(()=>indexed_entries(b, id, 20)); b.writeUInt32BE(10, 56); b.writeBigUInt64BE(1n, 44); throws(()=>indexed_entries(b, id, 20)); }); test('Jade rejects cost types, duplicate IDs and invalid resource amounts', ()=>{ throws(()=>jade.goods([{ id:1 }])); throws(()=>jade.goods([{ id:1, cost_type:1 }, { id:1, cost_type:2 }])); throws(()=>jade.goods([{ id:1, cost_type:1, name:{ }, param:1, amounts:[] }])); assert.deepEqual(jade.goods([{ id:1, cost_type:1, name:{ }, param:1, amounts:[5] }]), [{ id:1, cost:'gold', price:0, param:1, resource:'gold', amount:5 }]); }); test('TextMap flags, count and key conflicts rejected', ()=>{ const text=e.encode_field('name', 'string'), entry=join(B(2), v(123), text); assert.equal(text_rows(join(v(2), entry, v(2)))[123], 'name'); throws(()=>text_rows(join(v(2), B(8)))); throws(()=>text_rows(join(v(2), entry, v(4)))); throws(()=>text_rows(join(v(4), entry, B(2), v(123), e.encode_field('other', 'string'), v(4)))); }); test('installer rollback restores old files and removes new files', ()=>{ const tmp=e.fs.mkdtempSync(e.path.join(e.os.tmpdir(), 'gf-install-')); try{ const old=e.path.join(tmp, 'old'), fresh=e.path.join(tmp, 'fresh'), backup=e.path.join(tmp, 'backup'); replace(old, 'prior'); throws(()=>transaction([['source', old], ['source', fresh]], backup, write=>{ write(old, 'new'); write(fresh, 'new'); }, ()=>e.fail('post-publish check failed'))); assert.equal(e.readText(old), 'prior'); assert.equal(e.fs.existsSync(fresh), false); assert.ok(e.fs.readdirSync(e.path.join(backup, 'source')).some(n=>n.includes('.bak.'))); } finally{ e.fs.rmSync(tmp, { recursive:true, force:true }); } }); test('managed manifests reject symlinks and traversal', ()=>{ const tmp=e.fs.mkdtempSync(e.path.join(e.os.tmpdir(), 'gf-manifest-')); try{ e.fs.writeFileSync(e.path.join(tmp, 'data'), 'x'); e.fs.symlinkSync('data', e.path.join(tmp, 'link')); throws(()=>manifest(tmp)); e.fs.unlinkSync(e.path.join(tmp, 'link')); replace(e.path.join(tmp, 'BUILD-INFO.txt'), 'client_version=OSBETAWin1.2.3\nmode=full\n'); replace(e.path.join(tmp, 'SHA256SUMS'), '0'.repeat(64)+' ../outside\n'); throws(()=>verify_bundle(tmp, 'OSBETAWin1.2.3')); } finally{ e.fs.rmSync(tmp, { recursive:true, force:true }); } }); test('stage installer publishes only verified data with backups',async()=>{ const {spawnSync}=await import('node:child_process'); const tmp=e.fs.mkdtempSync(e.path.join(e.os.tmpdir(),'gf-install-full-')); try { const launcher=e.path.join(tmp,'launcher'),source=e.path.join(tmp,'source'),stage=e.path.join(tmp,'stage'); const bundle=e.path.join(tmp,'bundle'),runtime=e.path.join(tmp,'runtime'),client=e.path.join(tmp,'client'); const version='OSBETAWin1.2.3',write=(p,b)=>e.replace(p,b); const clientNames=['StarRail.exe','GameAssembly.dll','StarRail_Data/il2cpp_data/Metadata/global-metadata.dat','StarRail_Data/il2cpp_data/Metadata/startup-metadata.dat','StarRail_Data/StreamingAssets/BinaryVersion.bytes']; for(const n of clientNames)write(e.path.join(client,n),n.endsWith('BinaryVersion.bytes')?version:n); const identity={version,files:clientNames.map(n=>({path:n,size:e.fs.statSync(e.path.join(client,n)).size,sha256:e.sha256_file(e.path.join(client,n))}))}; for(const n of ['server/gameserver','server/dispatch','server/config.json','server/hotfix.json','server/resources/AvatarConfig.json','protocol/root.zig','protocol/protocol.pb.zig','client-patch/launcher.exe','client-patch/hkrpg.dll','dump/design-data/catalogue.json'])write(e.path.join(bundle,n),n); e.atomic_json(e.path.join(bundle,'dump/client.json'),identity); write(e.path.join(bundle,'BUILD-INFO.txt'),`client_version=${version}\nmode=full\n`); write(e.path.join(runtime,'PROFILE-BUILD'),version); for(const n of ['root.zig','protocol.pb.zig'])write(e.path.join(runtime,'protocol/src',n),e.fs.readFileSync(e.path.join(bundle,'protocol',n))); write(e.path.join(runtime,'zig-out/bin/gameserver'),'protected binary'); const roots=[e.path.join(source,'resources/grid_fight'),e.path.join(bundle,'server/resources/grid_fight'),e.path.join(runtime,'resources/grid_fight')]; for(const root of roots)write(e.path.join(root,'rounds.json'),'previous'); e.atomic_json(e.path.join(launcher,'client-profiles.json'),{beta:{bundle_dir:bundle,runtime_dir:runtime,client_dir:client,version}}); write(e.path.join(launcher,'.pearl-cache/runtime-refresh.lock'),''); e.atomic_json(e.path.join(stage,'rounds.json'),[{route_id:803}]); e.atomic_json(e.path.join(stage,'stage-schemas.json'),{}); e.atomic_json(e.path.join(stage,'stage-provenance.json'),{game_assembly_sha256:e.sha256_file(e.path.join(client,'GameAssembly.dll')),catalogues:[],tables:[],outputs:{'rounds.json':e.sha256_file(e.path.join(stage,'rounds.json'))}}); write(e.path.join(bundle,'SHA256SUMS'),manifest(bundle)); const script=e.path.join(e.HERE,'install_stages.mjs'),report=e.path.join(tmp,'report.json'); const result=spawnSync(process.execPath,[script,'--source',source,'--launcher',launcher,'--stage',stage,'--report',report],{encoding:'utf8',env:{...process.env,NODE_TEST_CONTEXT:''}}); assert.equal(result.status,0,result.stderr); const installed=e.loadJSON(report); assert.equal(installed.source_bundle_runtime_identical,true); assert.equal(installed.processes_restarted,false); assert.equal(e.readText(e.path.join(runtime,'zig-out/bin/gameserver')),'protected binary'); for(const root of roots)assert.equal(e.sha256_file(e.path.join(root,'rounds.json')),e.sha256_file(e.path.join(stage,'rounds.json'))); const backup=e.path.join(launcher,installed.backup,'source'); assert.ok(e.fs.readdirSync(backup).some(n=>n.startsWith('rounds.json.bak.gridfight-stage-'))); write(e.path.join(stage,'rounds.json'),'tampered'); const rejected=spawnSync(process.execPath,[script,'--source',source,'--launcher',launcher,'--stage',stage,'--report',report],{encoding:'utf8',env:{...process.env,NODE_TEST_CONTEXT:''}}); assert.notEqual(rejected.status,0); for(const root of roots)assert.deepEqual(e.loadJSON(e.path.join(root,'rounds.json')),[{route_id:803}]); } finally {e.fs.rmSync(tmp,{recursive:true,force:true});} }); test('4.6.53 native DynamicValue dispatch is proved at metadata RVA', ()=>{ const dis=e.fs.readFileSync(new URL('./fixtures/dynamic-4.6.53.asm',import.meta.url),'utf8'), address=0x19e745df0n, offsets=[0x85,0xc5,0x102,0x14f,0x18a,0x1cf], read=(ta,size)=>{ assert.equal(ta,address+0x2b0n);assert.equal(size,24); const raw=Buffer.alloc(24);offsets.forEach((v,i)=>raw.writeInt32LE(v-0x2b0,i*4));return raw; }; assert.equal(verify_dynamic_dispatch(dis,address,read).tags.length,7); throws(()=>verify_dynamic_dispatch(dis.replace('ja 0x19e745fb6','ja 0x19e745fbf'),address,read)); throws(()=>verify_dynamic_dispatch(dis.replace('shr rdx,1','shr rdx,2'),address,read)); throws(()=>verify_dynamic_dispatch(dis,address,(ta,size)=>{const b=read(ta,size);b.writeInt32LE(0xc5-0x2b0,0);return b;})); throws(()=>verify_dynamic_dispatch(dis.replace('add rbx,0x20','add rbx,0x18'),address,read)); // The 4.6.51 dispatch uses the same relative case/table offsets at its older RVA. const oldAddress=0x19e58e120n, delta=address-oldAddress, relocated=dis.replace(/(?:0x)?19e[0-9a-f]{6}/g,v=>{const prefix=v.startsWith('0x')?'0x':'';const n=BigInt('0x'+v.replace(/^0x/,''));return prefix+(n-delta).toString(16);}); assert.equal(verify_dynamic_dispatch(relocated,oldAddress,(_,size)=>read(address+0x2b0n,size)).dispatch_offset,0x2b0); }); test('DynamicValue exact bytes for all beta tags, nested null and 64-bit extremes', ()=>{ const values=[{wire_tag:0,value:-(1n<<63n)},{wire_tag:0,value:(1n<<63n)-1n}, {wire_tag:1,value:{double_wire:'010000000000f87f'}}, {wire_tag:2,value:true}, {wire_tag:3,value:[{wire_tag:6,value:null}]}, {wire_tag:4,value:[[{wire_tag:5,value:'beta'}, {wire_tag:6,value:null}]]}, {wire_tag:5,value:'4.6.53 ทดสอบ'}, {wire_tag:6,value:null}]; for(const codec of [new stages.Codec(),new rounds.Codec()])for(const value of values){ const v=codec.mode==='rounds'?JSON.parse(JSON.stringify(value,(_,x)=>typeof x==='bigint'?x.toString():x).replaceAll('"wire_tag"','"tag"')):value, wire=codec.encode_dynamic(v),cursor=new e.Cursor(wire),decoded=codec.dynamic(cursor); assert.equal(cursor.at,wire.length);assert.deepEqual(codec.encode_dynamic(decoded),wire); for(let n=0;ncodec.dynamic(new e.Cursor(wire.subarray(0,n)))); } }); test('4.6.53 Phainon schema excludes stack writes from object field candidates', ()=>{ const dump=e.fs.readFileSync(new URL('./fixtures/phainon-4.6.53.metadata.txt',import.meta.url),'utf8'), dis=e.fs.readFileSync(new URL('./fixtures/phainon-4.6.53.asm',import.meta.url),'utf8'), codec=new stages.Codec(dump); codec.disassemble=()=>[0x19e825440n,dis];codec.proof=()=>{}; assert.deepEqual(codec.schema('RPG.GameCore.GridFightPhanionUniqueTraitConfigRow').map(r=>r.name), ['Rarity','Star','SavedValueAddList','SpecialSavedValueAddList','OwnerPropertyList','AllMemberPropertyList','SpecialOwnerPropertyList','SpecialAllMemberPropertyList','MaxStarBonusID']); }); test('native double dictionaries preserve NaN payloads and signed zero', ()=>{ const codec=new stages.Codec(),typ='System.Collections.Generic.Dictionary', raw=Buffer.from('0401010000000000f87f020000000000000080','hex'),cursor=new e.Cursor(raw), decoded=codec.field(cursor,typ); assert.equal(cursor.at,raw.length);assert.deepEqual(codec.encode_field(decoded,typ),raw); throws(()=>codec.field(new e.Cursor(raw.subarray(0,-1)),typ)); }); test('SpotLight rows are the only source of Origin Bonds, 100705 carries Apprenticeship', ()=>{ const root=new URL('../../resources/grid_fight/', import.meta.url), read=name=>JSON.parse(e.fs.readFileSync(new URL(name, root), 'utf8')), spot=read('mechanics.json').GridFightSpotLightConfigRow, traits=read('traits.json'), roles=read('roles.json'), carriers=spot.filter(row=>row.TraitID); assert.deepEqual(carriers.map(row=>[row.ID, row.TraitID]).sort((x, y)=>x[0]-y[0]), [[100705, 400502], [100800, 400102], [100801, 400302], [110801, 400202], [120902, 400402]]); for(const row of carriers){ const trait=traits.find(t=>t.id===row.TraitID); assert.ok(trait, 'trait '+row.TraitID); assert.equal(trait.raw.base.TraitType, 3); assert.ok(!roles.some(r=>r.traits.includes(row.TraitID)), 'no natural bearer of '+row.TraitID); } assert.equal(traits.find(t=>t.id===400502).name, 'Apprenticeship'); });