using System; using System.IO; using System.Text.Json; using System.Linq; using System.Collections; using System.Text; using System.Runtime.InteropServices; namespace CheatTest; using Module = Memory; using LockKey = List; using LockHole = (string, List< List >); struct Lock { public string name; public List holes; } enum ParserState { WaitLock, ReadLock, WaitHole, ReadHole, ReadKey, WaitPart, NeedPart, } enum EntType { Names, Cmds, } class Program { static string[] PASSHOLE = { "off", "text" }; static uint Align(uint n, uint basic) { var ext = n % basic; return ext == 0 ? n : n + basic - ext; } static int FindIndex( List t, byte[] tr ) { var rlen = tr.Length; var tlen = t.Count; var limit = tlen - rlen; for( var i=0; i < limit; ++i ){ if( t[i+rlen] != 0 ) continue; var eq = true; for( var j=0; j < rlen; ++j ){ if( t[i+j] != tr[j] ){ eq = false; break; } } if( eq ) return i; } return -1; } static List AssembleCheat( List list, Dictionary dup, ushort hole ){ Func fromTaddr = (string taddr) => { var n = Convert.ToUInt32(taddr, 16); if( n == 0 || n > 0x41fffff) { Console.WriteLine($"get an invalid address {taddr}"); Environment.Exit(-1); } if( n <= 0x3ffff ){ return n | 0x2000000; } else if( 0 == (n&0xf000000) ){ return (n & 0xffff) | 0x3000000; } else return n; }; var addrval = new Dictionary(); foreach( var command in list ){ if( command.Count == 0 ){ continue; } var addr = fromTaddr( command[0] ); var len = command.Count - 1; for( uint pos = 0; pos < len; ++pos ){ addrval[addr+pos] = Convert.ToUInt32(command[(int)pos+1], 16); } } var ordered = addrval.Select(kvp => (kvp.Key, kvp.Value)).ToList(); ordered.Sort( ( (uint,uint) a, (uint,uint) b ) => { return (int)a.Item1 - (int)b.Item1; } ); var blocks = new List(); (uint, uint, uint) curr = (0,0,0); foreach( var (addr, value) in ordered){ dup.TryAdd(addr, hole); if( curr.Item3 == 0 ){ curr = (addr, value, 1); } else { if( curr.Item2 == value && curr.Item1 + curr.Item3 == addr ){ ++curr.Item3; } else { blocks.Add( curr.Item1 ); blocks.Add( (curr.Item3<<8) | curr.Item2 ); curr = (addr, value, 1); } } } if( curr.Item3 != 0 ){ blocks.Add( curr.Item1 ); blocks.Add( (curr.Item3<<8) | curr.Item2 ); } return blocks; } static (uint, Memory) Lade( List locks, string serial, string order ) { var rootstr = new List(); var names = new List(); var enttable = new List<(uint, EntType, List?, List?)>(); Func makeID = (uint a, uint b) => a | (b << 16); for (var i = 0; i < locks.Count; ++i){ Lock l = locks[i]; rootstr.Add(l.name); names.Add(l.name); var holestr = new List(); bool colname = l.holes.Count > 1; for (var index=0; index < l.holes.Count; ++index){ var (name, cmd) = l.holes[index]; holestr.Add(name); if (colname) names.Add(name); var id = makeID((uint)(i + 1), (uint)(index + 1)); enttable.Add( (id, EntType.Cmds, null, cmd) ); } enttable.Add( (makeID((uint)(i + 1), 0), EntType.Names, holestr, null) ); } enttable.Add( (makeID(0, 0), EntType.Names, rootstr, null) ); if(enttable.Count > 0xffff) { throw new Exception($"Too many entries in file {order}"); } enttable = enttable.OrderBy(n => n.Item1).ToList(); names.Sort( (string a, string b) => { if( a.Length == b.Length ){ return a.CompareTo(b); } else return b.Length - a.Length; } ); List strtable = new List(1024); List idxtable = new List(128); List cmdtable = new List(2048); var strcache = new Dictionary(); Func ut_push = (byte[] tr) => { var off = strtable.Count; strtable.EnsureCapacity( off + tr.Length + 1 ); strtable.AddRange( tr ); strtable.Add(0); return (ushort)off; }; Func ut_addr = (string tr) => { if( strcache.ContainsKey(tr) ){ return (ushort)strcache[tr]; } else { var code = Encoding.UTF8.GetBytes(tr); var idx = FindIndex(strtable, code); ushort off = (ushort)(idx < 0 ? ut_push(code) : idx); strcache[tr] = off; return off; } }; Func, ushort[]> addString = (List list) => { var retval = new ushort[list.Count]; for( var i=0; i < list.Count; i++ ){ retval[i] = ut_addr(list[i]); } return retval; }; Func addStrIndex = (ushort[] list) => { if( list.Length == 0 ) return 0; if( strtable.Count > 0xffff ){ Console.WriteLine($"string table is too big {order}"); } var ret = idxtable.Count; idxtable.EnsureCapacity( ret + list.Length ); idxtable.AddRange( list ); return (ushort)(ret * 2); }; Func, uint> addCommand = (List list) => { var ret = cmdtable.Count; cmdtable.EnsureCapacity( ret + list.Count ); cmdtable.AddRange( list ); return (uint)(ret * 4); }; addString(names); var entbytelist = new List<(uint, uint, uint)>(); var emptylist = new List(); var holedup = new Dictionary(); foreach( var (id, type, labels, cmds) in enttable ){ if( type == EntType.Names ){ var idxlist = addString( id == 0 ? labels : labels.Count > 1 ? labels : emptylist); var location = addStrIndex( idxlist ); entbytelist.Add(((uint, uint, uint))(id, location, idxlist.Length)); } else { var armbytecode = AssembleCheat( cmds, holedup, (ushort)(id & 0xffff)); var location = addCommand( armbytecode ); entbytelist.Add(((uint, uint, uint))(id, location, armbytecode.Count)); } } uint code = 0; foreach( var ch in serial ){ code = (code<<8) | ((uint)ch & 0xff); } foreach( var cmd in cmdtable ){ code = code ^ cmd; } return (code, Pack( entbytelist, strtable, idxtable, cmdtable )); } static Memory Pack( List<(uint,uint,uint)> entlist, List strlist, List idxlist, List cmdlist ){ uint entrysize = 12; uint strbase = (uint)(2 + entlist.Count * entrysize); uint idxbase = (uint)(strbase + strlist.Count); uint cmdbase = (uint)(idxbase + idxlist.Count*2); uint nonalign_size = (uint)(cmdbase + cmdlist.Count * 4); uint size = Align(nonalign_size, 32); var result = new Memory(new byte[size]); var span = result.Span; MemoryMarshal.Write( span.Slice(0, 2), (ushort)entlist.Count ); for(var i=0; i < entlist.Count; ++i){ var entry = entlist[i]; var offset = 2 + i * entrysize; var locbase = entry.Item1 > 0xffff ? cmdbase : idxbase; MemoryMarshal.Write( span.Slice((int)offset, 4), entry.Item1 ); MemoryMarshal.Write( span.Slice((int)(offset + 4), 4), (uint)(locbase + entry.Item2) ); MemoryMarshal.Write( span.Slice((int)(offset + 8), 4), entry.Item3 ); } // string table CollectionsMarshal.AsSpan( strlist ).CopyTo( span.Slice((int)strbase, strlist.Count ) ); // idxes table MemoryMarshal.AsBytes( CollectionsMarshal.AsSpan(idxlist) ) .CopyTo( span.Slice((int)idxbase, idxlist.Count * 2) ); // inst table MemoryMarshal.AsBytes( CollectionsMarshal.AsSpan(cmdlist) ) .CopyTo( span.Slice((int)cmdbase, cmdlist.Count * 4) ); return result; } static List<(uint, Memory)> Parse( string data, string serial, string order ) { var retval = new List<(uint, Memory)>(); var state = ParserState.WaitLock; var token = new List(); var line = 1; var locks = new List(); Lock? currlock = null; LockHole? currhole = null; Func token_str = () => { var r = new string( CollectionsMarshal.AsSpan(token) ); token.Clear(); return r; }; Action error = (string msg) => { Console.WriteLine($"Error<{msg}> at line {line}: {msg}"); Environment.Exit(1); }; Action incr = (char ch) => { switch(ch) { case '[': { if(state == ParserState.WaitLock || state == ParserState.ReadHole || state == ParserState.NeedPart ){ state = ParserState.ReadLock; if( currlock != null ) { locks.Add( currlock.Value ); } currlock = new Lock(); } else if(state == ParserState.WaitPart) { } else error($"error occur [ on {state}"); } break; case ']': { if(state == ParserState.ReadLock) { state = ParserState.WaitHole; var name = token_str(); if(name.ToLower() == "gameinfo") { retval.Add( Lade(locks, serial, order) ); state = ParserState.WaitPart; locks = new List(); currhole = null; currlock = null; } else if( name.Length > 0 ) { currlock = new Lock { name = name, holes = new List() }; currhole = null; } else error($"empty lock name in {order}"); } else if(state == ParserState.WaitPart) {} else if(state == ParserState.NeedPart) { state = ParserState.WaitPart; } else error($"error occur ] on {state}"); } break; case '\r': case '\n': { if(state == ParserState.WaitHole) { state = ParserState.ReadHole; } else if( state == ParserState.ReadKey){ var token = token_str(); var pass = false; if( currhole != null ){ var (name, keys) = currhole.Value; if( token.Length > 0 ){ keys.Last().Add(token); } if( PASSHOLE.Contains(name.ToLower()) ){ pass = true; } } if( !pass ){ if( currlock != null ){ if( currhole == null ) error("不应该来到这里"); currlock.Value.holes.Add( currhole.Value ); } currhole = null; } state = ParserState.ReadHole; } else if(state == ParserState.ReadLock){ error($"error occur newline on {state}"); } else if(state == ParserState.ReadHole){ if(token.Count > 0 && token.Find(c => c != ' ' && c != '\t') != 0 ){ error($"error occur newline on {state}"); } else token.Clear(); } else if(state == ParserState.WaitPart){ state = ParserState.NeedPart; } else {} if( ch == '\n' ) ++line; } break; case '=': { if(state == ParserState.ReadHole){ state = ParserState.ReadKey; currhole = (token_str(), new List>{ new List() }); } else if( state == ParserState.ReadLock) { token.Add( ch ); } else if( state == ParserState.WaitPart ) {} else if( state == ParserState.NeedPart ) { state = ParserState.WaitPart; } else error($"error occur = on {state}"); } break; case ',': { if( state == ParserState.ReadKey ){ if( token.Count > 0 ){ if( currhole == null ) error("不应该来到这里"); var token = token_str(); var (name, keys) = currhole.Value; keys.Last().Add(token); } } else if( state == ParserState.ReadHole ){ var (name, cmd) = currlock.Value.holes.Last(); currlock.Value.holes.RemoveAt(currlock.Value.holes.Count-1); if( token.Count > 0 ) cmd.Last().Add(token_str()); currhole = (name, cmd); state = ParserState.ReadKey; } else if( state == ParserState.ReadLock){ token.Add( ch ); } else if( state == ParserState.WaitPart ) {} else if( state == ParserState.NeedPart ) { state = ParserState.WaitPart; } else error($"error occur , on {state}"); } break; case ';': { if( state == ParserState.ReadKey){ if(token.Count > 0) currhole.Value.Item2.Last().Add(token_str()); currhole.Value.Item2.Add(new List()); } else if( state == ParserState.ReadLock){ token.Add( ch ); } else if( state == ParserState.ReadHole){ var (name, cmd) = currlock.Value.holes.Last(); currlock.Value.holes.RemoveAt(currlock.Value.holes.Count-1); if( token.Count > 0 ) cmd.Last().Add(token_str()); cmd.Add(new List()); currhole = (name, cmd); state = ParserState.ReadKey; } else if( state == ParserState.WaitPart) {} else if( state == ParserState.NeedPart ) { state = ParserState.WaitPart; } else error($"error occur ; on {state}"); } break; case ' ': { if( state == ParserState.ReadLock || state == ParserState.ReadHole ){ token.Add( ch ); } else if( state == ParserState.NeedPart ) { state = ParserState.WaitPart; } else {} } break; default: { if( state == ParserState.ReadLock || state == ParserState.ReadHole ){ token.Add( ch ); } else if( state == ParserState.ReadKey ){ if( "0123456789abcdefABCDEF".Contains(ch) ){ token.Add( ch ); } else if(currhole != null) { if(currhole.Value.Item1 == "text") token.Add( ch ); } else error($"error occur {ch} on {state}"); } else if( state == ParserState.WaitPart ) {} else if( state == ParserState.NeedPart ) { state = ParserState.WaitPart; } else error($"error occur {ch} on {state}"); } break; } }; Action done = () => { if (state != ParserState.WaitPart && state != ParserState.NeedPart) { error($"Unexpected end of file {state} in {order}"); } }; foreach (char ch in data){ incr(ch); } done(); return retval; } static List<(string, List<(uint, Memory)>)> Transform( List<(string, string)> list ) { var retval = new List<(string, List<(uint, Memory)>)>(); foreach (var (order, serial) in list) { var file = $"./gba/{order}.u8"; try{ var chtdata = File.ReadAllText(file); if (chtdata.Length > 0) { var cheats = Parse( chtdata, serial, order ); retval.Add((serial, cheats)); } else { //Console.WriteLine($"{order} is empty"); retval.Add((serial, new List<(uint, Memory)>())); } } catch( Exception e ){ Console.WriteLine($"{e} not found"); retval.Add((serial, new List<(uint, Memory)>())); } } return retval; } static List<(string, string)> LoadList() { var retval = new List<(string, string)>(); var file_content = File.ReadAllText("serial.json"); using (JsonDocument doc = JsonDocument.Parse(file_content)) { JsonElement root = doc.RootElement; HashSet set = new HashSet(); foreach (var item in root.EnumerateObject()) { string? serial = item.Value.GetString(); if( set.Contains(item.Name) ) Console.WriteLine($"重复 {item.Name}"); else if (serial != null && serial != "????") { set.Add( item.Name ); retval.Add(( item.Name, serial )); } } } return retval; } static byte[] Format( List<(string, List<(uint, Module)>)> cheats ) { cheats.Sort( ((string, List<(uint, Module)>) a, (string, List<(uint, Module)>) b) => { return a.Item1.CompareTo(b.Item1); } ); Console.WriteLine($"valid rom has {cheats.Count}"); List sers; List offs; uint chtc; uint maxl; { (List, List, uint, uint, string) ret = (new List(), new List(), 0, 0, ""); uint last = 0; foreach(var (serial, cheat) in cheats){ var val = serial.Substring(0, 3); if( ret.Item5 != val ){ if( ret.Item2.Count > 0 && ret.Item3 - last > ret.Item4 ){ ret.Item4 = ret.Item3 - last; } ret.Item1.AddRange( Encoding.ASCII.GetBytes(val) ); ret.Item2.Add((ushort)ret.Item3); last = ret.Item3; ret.Item5 = val; } ret.Item3 += (uint)cheat.Count; } if( ret.Item2.Count > 0 ){ if( ret.Item3 - last > ret.Item4 ){ ret.Item4 = ret.Item3 - last; } ret.Item2.Add((ushort)ret.Item3); } sers = ret.Item1; offs = ret.Item2; chtc = ret.Item3; maxl = ret.Item4; } List chts; List expanded; { (List, List, uint) ret = (new List(), new List(), Align((uint)(8 + sers.Count + offs.Count*2 + chtc * 8), 32)); foreach(var (serial, cheat) in cheats){ var val = serial[3]; foreach(var (id, bin) in cheat) { var off = ret.Item3 + ret.Item2.Count; ret.Item1.Add((uint)((val) | (off<<3))); ret.Item1.Add( id ); ret.Item2.EnsureCapacity( ret.Item2.Count + bin.Length ); ret.Item2.AddRange( MemoryMarshal.ToEnumerable(bin) ); } } chts = ret.Item1; expanded = ret.Item2; } Console.WriteLine($"name: {sers.Count} cheats: {chtc} maxl: {maxl}"); var serialbase = 8; var offsetbase = serialbase + sers.Count; var cheatbase = offsetbase + offs.Count * 2; var expandbase = Align((uint)(cheatbase + chts.Count*4), 32 ); var total = expandbase + expanded.Count; var retval = new byte[total]; var output = new Memory(retval); var span = output.Span; span[0] = (byte)'A'; span[1] = (byte)'C'; span[2] = (byte)'L'; span[3] = 1; MemoryMarshal.Write(span.Slice(4, 2), (ushort)(sers.Count / 3)); MemoryMarshal.Write(span.Slice(6, 2), (ushort)maxl); CollectionsMarshal.AsSpan( sers ).CopyTo( span.Slice(serialbase, sers.Count) ); MemoryMarshal.AsBytes( CollectionsMarshal.AsSpan(offs) ) .CopyTo( span.Slice(offsetbase, cheatbase - offsetbase) ); MemoryMarshal.AsBytes( CollectionsMarshal.AsSpan(chts) ) .CopyTo( span.Slice(cheatbase, chts.Count * 4) ); CollectionsMarshal.AsSpan( expanded ).CopyTo( span.Slice((int)expandbase, expanded.Count) ); return retval; } static void Main(string[] args) { var list = LoadList(); var roms = Transform(list); Console.WriteLine($"all rom has {roms.Count}"); var idx = new Dictionary>(); foreach( var (serial, cheat) in roms ){ if(cheat.Count == 0 ){ continue; } if( !idx.ContainsKey(serial) ){ idx[serial] = new List<(uint, Module)>(); } idx[serial].AddRange( cheat ); } var content = Format( idx.Select(g => (g.Key, g.Value)).ToList() ); File.WriteAllBytes("gba.acl", content); } }