import { spawnSync } from 'node:child_process'; import * as c from './common.mjs'; export * from './common.mjs'; const { fs, path, os, fail, small, zigzag, cat, matches, escapeRE, sha256, sha256_file, atomic_json, dict } =c; export const ENUM_TYPES=new Set(['AvatarPropertyType', 'OfferingRewardType', 'GridFightActivationType', 'GridFightChargeType', 'GridFightPreset', 'GridFightPropertyBindType', 'GridFightRoleSkillTag', 'GridFightTrailLayerQuality', 'GridFightTraitType', 'GridFightNodeType'].map(n=>'RPG.GameCore.'+n)); const ENUM_PROOFS=new Set(); export class Cursor { constructor(data, at=0){ this.data=data; this.at=at; } varint(){ let v=0n; for(let shift=0n; shift<70n; shift+=7n){ if(this.at>=this.data.length)fail('truncated varint'); const byte=this.data[this.at++]; if(shift===63n&&byte>1)fail('varint overflow'); v|=BigInt(byte&127)<BigInt(limit))fail('invalid collection header'); return Number(v/2n); } } export function pe_method(assembly, rva, size=8192){ const fd=fs.openSync(assembly, 'r'); try{ const h=Buffer.alloc(64); fs.readSync(fd, h, 0, 64, 0); const pe=h.readUInt32LE(60), head=Buffer.alloc(264); fs.readSync(fd, head, 0, 264, pe); if(head.subarray(0, 4).toString('hex')!=='50450000')fail('invalid PE'); const count=head.readUInt16LE(6), sections=pe+24+head.readUInt16LE(20), base=head.readBigUInt64LE(48), s=Buffer.alloc(count*40); fs.readSync(fd, s, 0, s.length, sections); for(let i=0; i\[\]]+) (\w+) \/\/ Offset: (0x[\da-fA-F]+)/gm).map(m=>[Number(m[3]), [m[2], m[1]]])); const method=block.match(/public static void FromBinary\([^\n]+RVA: (0x[\da-fA-F]+)/); if(!fields.size||!method)fail('no binary reader for '+name); const rva=Number(method[1]), next=matches(dump, /RVA: (0x[\da-fA-F]+)/g).map(m=>Number(m[1])).filter(v=>rvaparseInt(x[1], 16)).filter(x=>fields.has(x))); if(found.size!==1){ if(ordered.size===fields.size)break; fail(`${name}: cannot prove field order for mask 0x${bit.toString(16)}`); } const [field_name, field_type]=fields.get([...found][0]); if(ENUM_TYPES.has(field_type)){ const en=dump.match(new RegExp('// Full Name: '+escapeRE(field_type)+'\\n(.*?)(?=\\n// TypeDefIndex:|$)', 's')); if(!en||!/public int value__/.test(en[1]))fail(field_type+': unsupported enum storage'); const reader=part.match(/mov\s+rdx,(?:rsi|rdi)[^\n]*\n[^\n]*call\s+(0x[\da-f]+)/); if(!reader)fail(field_type+': enum reader not proved'); const rr=Number(BigInt(reader[1])-(address-BigInt(rva))), pk=[assembly, rr, evidence_dir].join(':'); if(!ENUM_PROOFS.has(pk)){ const [, ed]=disassemble(assembly, rr, 1104); if(![/shr\s+ebx,1/, /and\s+esi,0x1/, /neg\s+esi/, /xor\s+ebx,esi/].every(r=>r.test(ed)))fail('native enum codec changed'); fs.mkdirSync(evidence_dir, { recursive:true }); fs.writeFileSync(path.join(evidence_dir, `EnumReader_${rr.toString(16)}.asm`), ed.slice(ed.indexOf('Disassembly'))); ENUM_PROOFS.add(pk); } } ordered.set(bit.toString(2).length-1, { name:field_name, type:field_type }); if(ordered.size===fields.size)break; } if(!ordered.size||!Array.from({ length:ordered.size }, (_, i)=>ordered.has(i)).every(Boolean))fail(name+': incomplete native schema'); fs.mkdirSync(evidence_dir, { recursive:true }); fs.writeFileSync(path.join(evidence_dir, name+'.asm'), dis.slice(dis.indexOf('Disassembly of section .data:'))); return Array.from({ length:ordered.size }, (_, i)=>ordered.get(i)); } export function field(cursor, typ, schemas){ if(typ.endsWith('[]'))return Array.from({ length:cursor.length() }, ()=>field(cursor, typ.slice(0, -2), schemas)); if(typ==='System.Collections.Generic.List')return Array.from({ length:cursor.length() }, ()=>field(cursor, 'uint', schemas)); if(typ==='RPG.Client.TextID'){ const v=cursor.varint(); if(v>0xffffffff)fail('TextID hash overflow'); return { hash:zigzag(v), hash64:cursor.varint() }; } if(typ==='RPG.GameCore.FixPoint')return { wire:cursor.varint() }; if(typ==='RPG.GameCore.AvatarPropertyValue'&&schemas)return record(cursor, schemas.AvatarPropertyValue, schemas); if(typ==='string'){ const n=cursor.varint(); if(n>16384||cursor.at+Number(n)>cursor.data.length)fail('invalid string'); const raw=cursor.data.subarray(cursor.at, cursor.at+Number(n)); cursor.at+=Number(n); return new TextDecoder('utf-8', { fatal:true }).decode(raw); } if(typ==='bool'){ if(cursor.at>=cursor.data.length)fail('truncated bool'); const v=cursor.data[cursor.at++]; if(v>1)fail('invalid bool'); return !!v; } if(typ==='uint'||ENUM_TYPES.has(typ)){ const v=cursor.varint(); if(v>0xffffffff)fail('uint overflow'); return ENUM_TYPES.has(typ)?zigzag(v):v; } fail('unsupported type '+typ); } export function record(cursor, columns, schemas){ const mask=BigInt(cursor.varint()); if(!mask||mask>>BigInt(columns.length))fail('invalid presence mask'); return Object.fromEntries(columns.flatMap((col, i)=>mask&(1n<0xffffffffffffffffn)fail('varint overflow'); const out=[]; while(v>=128n){ out.push(Number(v&127n)|128); v>>=7n; } out.push(Number(v)); return Buffer.from(out); } export function encode_field(v, t, s){ if(t.endsWith('[]'))return cat(encode_varint(2*v.length), v.map(x=>encode_field(x, t.slice(0, -2), s))); if(t==='System.Collections.Generic.List')return cat(encode_varint(2*v.length), v.map(encode_varint)); if(t==='RPG.Client.TextID'){ const h=BigInt(v.hash); return cat(encode_varint((h<<1n)^(h>>31n)), encode_varint(v.hash64)); } if(t==='RPG.GameCore.FixPoint')return encode_varint(v.wire); if(t==='RPG.GameCore.AvatarPropertyValue'&&s)return encode_record(v, s.AvatarPropertyValue, s); if(ENUM_TYPES.has(t)){ const x=BigInt(v); return encode_varint((x<<1n)^(x>>31n)); } if(t==='bool')return Buffer.from([Number(v)]); if(t==='string'){ const b=Buffer.from(v); return cat(encode_varint(b.length), b); } return encode_varint(v); } export function encode_record(row, cols, s){ return cat(encode_varint(cols.reduce((m, col, i)=>col.name in row?m|(1n<col.name in row).map(col=>encode_field(row[col.name], col.type, s))); } export const valid_roles=rows=>rows.length>=20&&c.uniqueValues(rows, r=>r.ID)&&rows.every(r=>r.ID>0&&r.ID<1000000&&r.AvatarID>=1000&&r.AvatarID<10000&&r.Rarity>=1&&r.Rarity<=5&&r.SeasonID>0&&r.SeasonID<100&&r.TraitList?.length&&r.TraitList.every(t=>t>0&&t<1000000)); export const valid_odds=rows=>rows.length>=5&&rows.length<=30&&rows.every((r, i)=>r.PlayerLevel===i+1&&[1, 2, 3, 4, 5].reduce((s, n)=>s+(r[`Rarity${n}Weight`]??0), 0)===100); const tableProof=(name, blob, start, end, rows, data)=>({ table:name, blob, offset:start, end, records:rows.length, rows, table_sha256:sha256(data.subarray(start, end)), blob_sha256:sha256(data) }); const choose=(found, msg)=>{ if(!found.length)fail(msg); if(new Set(found.map(m=>m.table_sha256)).size!==1)fail('ambiguous table; refusing to pick one'); return found[0]; }; export function find_table(client, columns, name, anchor_blob=null){ const roles=name==='GridFightRoleConfigRow'; if(roles&&columns.slice(0, 4).map(c=>c.name).join(',')!=='ID,AvatarID,FrontBackType,Rarity')fail('role locator schema changed'); if(!roles&&columns[0].name!=='PlayerLevel')fail('odds locator schema changed'); for(const layer of ['Persistent', 'StreamingAssets']){ const found=[]; for(const p of c.globBytes(path.join(client, 'StarRail_Data', layer, 'DesignData/Windows'))){ const blob=path.relative(path.join(client, 'StarRail_Data'), p); if(path.basename(p, '.bytes').length!==32||!fs.statSync(p).size||(anchor_blob&&anchor_blob!==blob))continue; const data=fs.readFileSync(p); if(roles&&data.indexOf('SpriteOutput/GridFight')<0)continue; for(let pos=0; pos=128&&(data[pos]&11)===11):!(data[pos]<=63&&(data[pos]&3)===3&&data[pos+1]===1))continue; try{ if(roles){ const prefix=new Cursor(data, pos), mask=BigInt(prefix.varint()); if(mask>>BigInt(columns.length)||(mask&0x208bn)!==0x208bn)continue; const id=prefix.varint(), avatar=prefix.varint(); if(mask&4n)prefix.varint(); const rarity=prefix.varint(); if(!(id>0&&id<1000000&&avatar>=1000&&avatar<10000&&rarity>=1&&rarity<=5))continue; } const first=record(new Cursor(data, pos), columns); if(roles?!(first.AvatarID>=1000&&first.AvatarID<10000&&first.Rarity>=1&&first.Rarity<=5&&first.TraitList?.length):first.PlayerLevel!==1)continue; for(let start=Math.max(0, pos-5); startrecord(cursor, columns)); if(!(roles?valid_roles:valid_odds)(rows))continue; const encoded=cat(encode_varint(count*2), rows.map(r=>encode_record(r, columns))); if(!data.subarray(start, cursor.at).equals(encoded))continue; found.push(tableProof(name, blob, start, cursor.at, rows, data)); } catch(e){ if(!(e instanceof c.DecodeError||e instanceof RangeError||e instanceof TypeError))throw e; } } } catch(e){ if(!(e instanceof c.DecodeError||e instanceof RangeError||e instanceof TypeError))throw e; } } } if(found.length)return choose(found, name+': no verified complete table found'); } fail(name+': no verified complete table found'); } export function find_keyed_table(client, anchor, columns, name, first_key, valid, schemas, required_mask=null){ const data=fs.readFileSync(path.join(client, 'StarRail_Data', anchor)), candidates=[]; if(required_mask!==null){ if(columns.length>10)fail('bounded locator requires at most ten fields'); const prefixes=[]; for(let m=1; m<2**columns.length; m++)if((m&required_mask)===required_mask)prefixes.push(cat(encode_varint(m), first_key===null?Buffer.alloc(0):encode_varint(first_key))); const groups=new Map(); for(const p of prefixes){ if(!groups.has(p[0]))groups.set(p[0], []); groups.get(p[0]).push(p); } for(let pos=0; posdata.subarray(pos, pos+p.length).equals(p)); if(p){ candidates.push([pos, [pos]]); pos+=p.length-1; } } } else{ const needle=encode_varint(first_key); for(let at=data.indexOf(needle); at>=0; at=data.indexOf(needle, at+needle.length))candidates.push([at, Array.from({ length:Math.min(at, 10) }, (_, i)=>Math.max(0, at-10)+i)]); } const found=[]; for(const [pos, starts]of candidates)for(const first of starts){ try{ const prefix=new Cursor(data, first), mask=BigInt(prefix.varint()); if((required_mask===null&&prefix.at!==pos)||!(mask&1n)||mask>>BigInt(columns.length))continue; for(let start=Math.max(0, first-5); startrecord(cursor, columns, schemas)); if(!valid(rows))continue; const encoded=cat(encode_varint(count*2), rows.map(r=>encode_record(r, columns, schemas))); if(!data.subarray(start, cursor.at).equals(encoded))continue; found.push(tableProof(name, anchor, start, cursor.at, rows, data)); } catch(e){ if(!(e instanceof c.DecodeError||e instanceof RangeError||e instanceof TypeError))throw e; } } } catch(e){ if(!(e instanceof c.DecodeError||e instanceof RangeError||e instanceof TypeError))throw e; } } return choose(found, name+': no complete related table found'); } export function find_ui_blob(client){ for(const layer of ['Persistent', 'StreamingAssets']){ const found=c.globBytes(path.join(client, 'StarRail_Data', layer, 'DesignData/Windows')).filter(p=>{ if(!fs.statSync(p).size)return false; const b=fs.readFileSync(p); return b[0]!==123&&b.includes(Buffer.from('GridFightEntrancePage'))&&b.includes(Buffer.from('SpriteOutput/Rogue/Tourn/Titan/RankIcon/Small/')); }); if(found.length===1)return path.relative(path.join(client, 'StarRail_Data'), found[0]); if(found.length)fail('ambiguous GridFight UI Excel blob'); } fail('no installed GridFight UI Excel blob'); } export const valid_divisions=(rows, seasons)=>rows.length>=20&&c.uniqueValues(rows, r=>r.ID)&&c.subset(seasons, new Set(rows.map(r=>r.SeasonID)))&&rows.every(r=>r.ID>0&&r.ID<1000000&&seasons.has(r.SeasonID)&&(r.DivisionLevel??0)>=0&&(r.DivisionLevel??0)<=100&&r.Progress>0&&r.Progress<1000&&(!r.DivisionIcon||r.DivisionIcon.startsWith('SpriteOutput/Rogue/Tourn/Titan/RankIcon/')))&&rows.filter(r=>r.DivisionIcon).length>Math.floor(rows.length/2); export const valid_season_modules=(rows, seasons)=>c.subset(seasons, new Set(rows.map(r=>r.SeasonID)))&&c.uniqueValues(rows, r=>[r.SeasonID, r.SubSeasonID].join(':'))&&rows.every(r=>seasons.has(r.SeasonID)&&r.SubSeasonID>0&&r.SubSeasonID<100&&r.ActivityModuleID>100000&&r.MaxRewardExp>0&&r.OfferingID>0); const moduleList=r=>[...(r.ResidentModuleList??[]), ...(r.ActivityModuleIDList??[])]; export const valid_activities=(rows, modules)=>rows.length>=20&&c.uniqueValues(rows, r=>r.ActivityID)&&rows.every(r=>r.ActivityID>0&&r.ActivityID<10000000&&moduleList(r).every(m=>m>0))&&c.subset(modules, new Set(rows.flatMap(moduleList))); export const valid_offering_levels=(rows, required)=>rows.length>20&&c.subset(required, new Set(rows.map(r=>r.TypeID)))&&c.uniqueValues(rows, r=>[r.TypeID, r.Level].join(':'))&&rows.every(r=>r.TypeID>0&&r.TypeID<10000&&r.Level>0&&r.Level<10000&&[0, 1].includes(r.Type??0)&&r.RewardID>0); export const valid_offering_types=(rows, required, maxima)=>c.subset(required, new Set(rows.map(r=>r.ID)))&&c.uniqueValues(rows, r=>r.ID)&&rows.every(r=>r.ID>0&&r.ItemID>1000&&r.MaxLevel>0&&(!(r.ID in maxima)||r.MaxLevel===maxima[r.ID])); export function season_contract(id, divisions, modules, activities){ const selected=modules.filter(r=>r.SeasonID===id).sort((a, b)=>a.SubSeasonID-b.SubSeasonID); if(!selected.length)fail('requested season is absent'); const module=selected[0], found=activities.filter(r=>moduleList(r).includes(module.ActivityModuleID)); if(found.length!==1||!found[0].ActivityPanelID)fail('season module has no unique activity panel'); const activity=found[0]; return { season_id:id, divisions:divisions.filter(r=>r.SeasonID===id).map(r=>r.ID), activity_id:activity.ActivityID, panel_id:activity.ActivityPanelID, selection:'explicit season; first configured sub-season; not an inferred live schedule', raw:{ selected_module:module, selected_activity:activity, divisions, season_modules:modules, activities:activities.filter(r=>modules.some(m=>moduleList(r).includes(m.ActivityModuleID))) } }; } export const valid_trait_bases=(rows, required)=>c.subset(required, new Set(rows.map(r=>r.ID)))&&c.uniqueValues(rows, r=>r.ID)&&rows.every(r=>r.TraitName&&r.SeasonID>0&&(r.IconPath??'').startsWith('SpriteOutput/GridFight/')); export const valid_trait_layers=(rows, required)=>c.subset(required, new Set(rows.map(r=>r.TraitID)))&&c.uniqueValues(rows, r=>[r.TraitID, r.Layer].join(':'))&&rows.every(r=>r.TraitID>0&&r.Layer>0&&r.Layer<100&&r.MazebuffID>=1000000); export function text_matches(data, id){ const needle=encode_field(id, 'RPG.Client.TextID'), found=[]; for(let at=data.indexOf(needle); at>=0; at=data.indexOf(needle, at+needle.length)){ if(!at||(data[at-1]&3)!==3||data[at-1]>=128)continue; try{ const cursor=new Cursor(data, at+needle.length), text=field(cursor, 'string'); if(text&&!/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(text))found.push({ offset:at-1, end:cursor.at, text }); } catch(e){ if(!(e instanceof c.DecodeError||e instanceof TypeError))throw e; } } if(new Set(found.map(f=>f.text)).size!==1)fail('TextMap key missing or conflicting'); return found[0]; } export function trait_names(client, rows, language){ const keys=dict(rows.map(r=>[r.ID, r.TraitName])); for(const layer of ['Persistent', 'StreamingAssets']){ const found=[]; for(const p of c.globBytes(path.join(client, 'StarRail_Data', layer, 'DesignData/Windows', language))){ if(!fs.statSync(p).size)continue; const data=fs.readFileSync(p); let resolved; try{ resolved=dict(Object.keys(keys).map(k=>[k, text_matches(data, keys[k])])); } catch(e){ if(e instanceof c.DecodeError)continue; throw e; } found.push([resolved, { blob:path.relative(path.join(client, 'StarRail_Data'), p), blob_sha256:sha256(data), keys:dict(Object.keys(keys).map(k=>[k, { text_id:keys[k], ...resolved[k] }])) }]); } if(found.length){ if(new Set(found.map(m=>c.json(m[0]))).size!==1)fail('ambiguous TextMap blobs'); return found[0]; } } fail('no complete installed '+language+' TextMap for traits'); } export function main(){ const a=c.cli(['client', 'dump', 'output'], { language:'en', 'evidence-dir':null, 'season-id':null }, ['traits']); if(!['en', 'cn', 'cht', 'jp', 'kr', 'de', 'es', 'fr', 'id', 'pt', 'ru', 'th', 'vi'].includes(a.language))fail('Invalid language'); const dump_bytes=fs.readFileSync(a.dump), dump=c.readText(a.dump), names=['GridFightRoleConfigRow', 'GridFightRarityWeightConfigRow']; if(a.traits)names.push('AvatarPropertyValue', 'GridFightTraitBaseConfigRow', 'GridFightTraitLayerConfigRow'); if(a['season-id']!==null)names.push('GridFightDivisionConfigRow', 'GridFightSeasonModuleConfigRow', 'ActivityConfigRow', 'OfferingLevelConfigRow', 'OfferingTypeConfigRow'); const schemas=Object.fromEntries(names.map(n=>[n, schema(dump, path.join(a.client, 'GameAssembly.dll'), n, a['evidence-dir']??path.join(a.output, 'evidence'))])), rt=find_table(a.client, schemas.GridFightRoleConfigRow, 'GridFightRoleConfigRow'), wt=find_table(a.client, schemas.GridFightRarityWeightConfigRow, 'GridFightRarityWeightConfigRow', rt.blob), tables={ [rt.table]:rt, [wt.table]:wt }; const roles=rt.rows.map(r=>({ id:r.ID, cost:r.Rarity, avatar_id:r.AvatarID, traits:r.TraitList, raw:r })), odds=dict(wt.rows.map(r=>[r.PlayerLevel, [1, 2, 3, 4, 5].map(i=>r[`Rarity${i}Weight`]??0)])), outputs={ 'roles.json':roles, 'shop_odds.json':odds }; let text_evidence=null; if(a.traits){ let required=new Set(roles.flatMap(r=>r.traits)); const base=find_keyed_table(a.client, rt.blob, schemas.GridFightTraitBaseConfigRow, 'GridFightTraitBaseConfigRow', Math.min(...required), r=>valid_trait_bases(r, required), schemas); required=new Set(base.rows.map(r=>r.ID)); const layers=find_keyed_table(a.client, rt.blob, schemas.GridFightTraitLayerConfigRow, 'GridFightTraitLayerConfigRow', Math.min(...required), r=>valid_trait_layers(r, required), schemas); const [resolved, proof]=trait_names(a.client, base.rows, a.language); text_evidence=proof; outputs['traits.json']=base.rows.map(r=>({ id:r.ID, name:resolved[r.ID].text, thresholds:layers.rows.filter(t=>t.TraitID===r.ID).map(t=>t.Layer).sort((a, b)=>a-b), raw:{ base:r, layers:layers.rows.filter(t=>t.TraitID===r.ID) } })); outputs['trait_layers.json']=layers.rows; tables[base.table]=base; tables[layers.table]=layers; } if(a['season-id']!==null){ const seasons=new Set(roles.map(r=>r.raw.SeasonID)), ui=find_ui_blob(a.client), div=find_keyed_table(a.client, ui, schemas.GridFightDivisionConfigRow, 'GridFightDivisionConfigRow', 1, r=>valid_divisions(r, seasons), schemas, 0x163), modules=find_keyed_table(a.client, ui, schemas.GridFightSeasonModuleConfigRow, 'GridFightSeasonModuleConfigRow', Math.min(...seasons), r=>valid_season_modules(r, seasons), schemas, 0x1f), ids=new Set(modules.rows.map(r=>r.ActivityModuleID)), activities=find_keyed_table(a.client, ui, schemas.ActivityConfigRow, 'ActivityConfigRow', null, r=>valid_activities(r, ids), schemas, 0xd); outputs['season.json']=season_contract(Number(a['season-id']), div.rows, modules.rows, activities.rows); for(const t of [div, modules, activities])tables[t.table]=t; const offerings=new Set(modules.rows.map(r=>r.OfferingID)), levels=find_keyed_table(a.client, ui, schemas.OfferingLevelConfigRow, 'OfferingLevelConfigRow', 1, r=>valid_offering_levels(r, offerings), schemas, 3), normal=levels.rows, maxima={ }; for(const r of normal)if(!r.Type)maxima[r.TypeID]=Math.max(maxima[r.TypeID]??0, r.Level); const types=find_keyed_table(a.client, ui, schemas.OfferingTypeConfigRow, 'OfferingTypeConfigRow', 1, r=>valid_offering_types(r, offerings, maxima), schemas, 7), selected=outputs['season.json'].raw.selected_module.OfferingID; outputs['season.json'].raw.offering_type=types.rows.find(r=>r.ID===selected); outputs['season.json'].raw.offering_levels=normal.filter(r=>r.TypeID===selected); outputs['offering_levels.json']=normal; outputs['offering_types.json']=types.rows; tables[levels.table]=levels; tables[types.table]=types; } for(const [n, v]of Object.entries(outputs))atomic_json(path.join(a.output, n), v); atomic_json(path.join(a.output, 'schemas.json'), schemas); const manifest={ schema:1, source:'installed-client-files', dump_sha256:sha256(dump_bytes), game_assembly_sha256:sha256_file(path.join(a.client, 'GameAssembly.dll')), outputs:Object.fromEntries(Object.keys(outputs).map(n=>[n, sha256_file(path.join(a.output, n))])), tables:Object.values(tables).map(t=>Object.fromEntries(Object.entries(t).filter(([k])=>k!=='rows'))), mapping:{ 'roles.cost':'GridFightRoleConfigRow.Rarity (shop tier; not a currency price)', 'roles.raw.SeasonID':'Caller must select its active season; rows from multiple seasons are retained.', 'shop_odds':'GridFightRarityWeightConfigRow.Rarity1Weight..Rarity5Weight; missing weights are zero.' } }; if(text_evidence)manifest.trait_textmap=text_evidence; atomic_json(path.join(a.output, 'provenance.json'), manifest); console.log(`Validated ${roles.length} roles, ${Object.keys(odds).length} shop levels and ${Object.keys(tables).length} tables from installed client bytes.`); } c.runMain(import.meta.url, main);