diff --git a/jazz2.asl b/jazz2.asl index c32c0ed..84f5ad3 100644 --- a/jazz2.asl +++ b/jazz2.asl @@ -1,128 +1,72 @@ -state("Jazz2", "v1.20") { - int menuItem : 0x0CE514; - int fullScreenImg : 0x144E40; - int demo : 0x145060; - int inGame : 0x1B8524; - int levelFinished : 0x1B8464; - int levelEndTimer : 0x1B8468; -} - -state("Jazz2", "v1.20 s Shareware") { - int menuItem : 0x0CC2D4; - int fullScreenImg : 0x142C00; - int demo : 0x142E20; - int inGame : 0x1B62E4; - int levelFinished : 0x1B6224; - int levelEndTimer : 0x1B6228; -} - -state("Jazz2", "v1.22") { - int menuItem : 0x0E609C; - int fullScreenImg : 0x15C860; - int demo : 0x15CA80; - int inGame : 0x1D00C4; - int levelFinished : 0x1D0004; - int levelEndTimer : 0x1D0008; -} - -state("Jazz2", "v1.23") { - int menuItem : 0x0E606C; - int fullScreenImg : 0x15CA40; - int demo : 0x15CC60; - int inGame : 0x1D0284; - int levelFinished : 0x1D01C4; - int levelEndTimer : 0x1D01C8; -} - -state("Jazz2", "v1.23 s Shareware") { - int menuItem : 0x0E4FDC; - int fullScreenImg : 0x15B9A0; - int demo : 0x15BBC0; - int inGame : 0x1CF1E4; - int levelFinished : 0x1CF124; - int levelEndTimer : 0x1CF128; -} - -state("Jazz2", "v1.23 se Special Edition Shareware") { - int menuItem : 0x0E4FFC; - int fullScreenImg : 0x15B9C0; - int demo : 0x15BBE0; - int inGame : 0x1CF204; - int levelFinished : 0x1CF144; - int levelEndTimer : 0x1CF148; -} +state("Jazz2") {} -state("Jazz2", "v1.24") { - int menuItem : 0x0E06B4; - int fullScreenImg : 0x180060; - int demo : 0x180280; - int inGame : 0x1F38A4; - int levelFinished : 0x1F37E4; - int levelEndTimer : 0x1F37E8; -} +init { + (vars as IDictionary).Clear(); + vars.initialized = false; -state("Jazz2", "v1.24 (LK Avalon)") { - int menuItem : 0x0E0714; - int fullScreenImg : 0x1800C0; - int demo : 0x1802E0; - int inGame : 0x1F3904; - int levelFinished : 0x1F3844; - int levelEndTimer : 0x1F3848; -} + var w1 = vars.memoryWatcherList1 = new MemoryWatcherList(); + var w2 = vars.memoryWatcherList2 = new MemoryWatcherList(); + var patterns = new dynamic[,] { + { w1, "menuItem" , 11, "C7 05 ?? ?? ?? ?? 01 00 00 00 A3 ?? ?? ?? ?? A3 ?? ?? ?? ??" }, + { w2, "fullScreenImg", 6, "BF ?? ?? ?? ?? A3 ?? ?? ?? ?? 83 C4 08" }, + { w1, "demo" , 4, "8B C6 8B 0D ?? ?? ?? ?? 85 C9" }, + { w2, "inGame" , 12, "C7 05 ?? ?? ?? ?? 05 00 00 00 89 35 ?? ?? ?? ?? E8 ?? ?? ?? ??" }, + { w2, "levelFinished", 3, "7E 42 A1 ?? ?? ?? ?? 85 C0" }, + { w2, "levelEndTimer", 6, "A3 ?? ?? ?? ?? A3 ?? ?? ?? ?? 8B 75 24" } + }; -state("Jazz2", "v1.24 x Christmas Chronicles '99 (LK Avalon)") { - int menuItem : 0x0E05DC; - int fullScreenImg : 0x17FFA0; - int demo : 0x1801C0; - int inGame : 0x1F37E4; - int levelFinished : 0x1F3724; - int levelEndTimer : 0x1F3728; -} + // Find variables + var scanTarget = new SigScanTarget(); + for (int i = 0; i < patterns.GetLength(0); ++i) { + var offset = patterns[i, 2]; + var pattern = patterns[i, 3]; + scanTarget.AddSignature(offset, pattern); + } + var mainModule = modules.First(); + var addrs = new SignatureScanner(game, mainModule.BaseAddress, mainModule.ModuleMemorySize).ScanAll(scanTarget).ToArray(); + if (addrs.Length != scanTarget.Signatures.Count) { + print("ScanAll failed"); + return; + } -init { - var versions = new dynamic[,] - { - { 0x352D28C2, 0x0C8, "v1.20" }, - { 0x352D2937, 0x0C8, "v1.20 s Shareware" }, - { 0x35AE2E29, 0x0C8, "v1.22" }, - { 0x35D00674, 0x0C8, "v1.23" }, - { 0x35D006DE, 0x0C8, "v1.23 s Shareware" }, - { 0x36068ADE, 0x0C8, "v1.23 se Special Edition Shareware" }, - { 0x36D13F77, 0x0F0, "v1.24" }, - { 0x376A194C, 0x110, "v1.24 (LK Avalon)" }, - { 0x383A85E8, 0x110, "v1.24 x Christmas Chronicles '99 (LK Avalon)" } - }; - var baseAddr = modules.First().BaseAddress; - for (int i = 0; i < versions.GetLength(0); ++i) - { - var timestamp = versions[i, 0]; - IntPtr posTimestamp = baseAddr + versions[i, 1]; - var name = versions[i, 2]; - if (memory.ReadValue(posTimestamp) == timestamp) - { - version = name; - break; + // Create memory watchers + for (int i = 0; i < patterns.GetLength(0); ++i) { + var watcherList = patterns[i, 0]; + var name = patterns[i, 1]; + if (addrs[i] == IntPtr.Zero) { + print("Cannot determine address of \"" + name + "\""); + return; } + var watcher = new MemoryWatcher(memory.ReadPointer(addrs[i])); + watcherList.Add(watcher); + (vars as IDictionary).Add(name, watcher); } + + vars.initialized = true; } update { - return version != ""; + if (!vars.initialized) return false; + vars.memoryWatcherList2.UpdateAll(game); } start { - return current.menuItem == 0 && current.demo == 0 && current.inGame > old.inGame; + vars.memoryWatcherList1.UpdateAll(game); + var inGame = vars.inGame; + return vars.menuItem.Current == 0 && vars.demo.Current == 0 && inGame.Current > inGame.Old; } isLoading { - return current.fullScreenImg != 0 && current.inGame != 0; + return vars.fullScreenImg.Current != 0 && vars.inGame.Current != 0; } split { - if(current.levelFinished == 1) - return current.levelEndTimer >= 32828 && old.levelEndTimer < 32828; - else if(current.levelFinished == 2) - return current.levelEndTimer >= 32780 && old.levelEndTimer < 32780; - else - return false; + int cmpEndTimer; + switch ((int)vars.levelFinished.Current) { + case 1: cmpEndTimer = 32828; break; + case 2: cmpEndTimer = 32780; break; + default: return false; + } + var endTimer = vars.levelEndTimer; + return endTimer.Current >= cmpEndTimer && endTimer.Old < cmpEndTimer; } diff --git a/write_jj2_asl.py b/write_jj2_asl.py deleted file mode 100644 index 0c5d282..0000000 --- a/write_jj2_asl.py +++ /dev/null @@ -1,210 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import pathlib -import pefile -import re -import sys - -PROCESS_NAME = 'Jazz2' -ASL_INDENT = ' ' - -def main(): - ap = argparse.ArgumentParser(description='Create a Jazz Jackrabbit 2 ' - 'ASL autosplitter for the provided game executables') - ap.add_argument('exe_dir', nargs='?', default='.', type=pathlib.Path, - help='the directory to search for Jazz Jackrabbit 2 exe files') - ap.add_argument('--asl', default=sys.stdout, type=argparse.FileType('w'), - help='write ASL script to this file rather than to stdout') - ap.add_argument('-r', '--recursive', action='store_true', - help='search for exe files recursively') - args = ap.parse_args() - - write_jj2_asl(**vars(args)) - -def write_jj2_asl(exe_dir, asl, recursive): - # Search for Jazz Jackrabbit 2 exe files and analyze them - versions = {} - for exe_file in exe_dir.glob(('**/' if recursive else '') + '*.exe'): - try: - version = GameVersion(exe_file) - except RuntimeError as err: - print(f'{exe_file}: {err}', file=sys.stderr) - continue - versions[version.tmstmp] = version - - # Create ASL file - versions = tuple(versions[tmstmp] for tmstmp in sorted(versions)) - AslWriter(versions).write(asl) - -class GameVersion: - def __init__(self, exe_path): - self.pe = pefile.PE(exe_path) - self.exe = self.pe.__data__ - self.tmstmp = self.pe.FILE_HEADER.TimeDateStamp - self.tmstmp_pos = \ - self.pe.FILE_HEADER.get_field_absolute_offset('TimeDateStamp') - self.name = self.detect_version() - self.addrs = self.find_addrs() - - def detect_version(self): - matches = re.findall( - br'\[Levelname\[\.j2l\]\]\0.*?' - br'([^\0]+)\0+([^\0]*Jazz Jackrabbit 2[^\0]*)\0', - self.exe - ) - if len(matches) != 1: - raise RuntimeError('Version detection failed') - - ver, name = matches[0] - append_sw = \ - self.exe.find(b'shareware') != -1 and b'Shareware' not in name - return b' '.join(s for s in ( - b'v' + ver.strip(), - name.lstrip(b'Jazz Jackrabbit 2').strip(), - b'Shareware' if append_sw else b'', - b'(LK Avalon)' if self.exe.find(b'avalon') != -1 else b'' - ) if s).decode('ascii') - - def find_addrs(self): - return { - 'int menuItem': self.find_addr( - br'\xC7\x05....\x01\x00\x00\x00' # mov , 1 - br'\xA3(....)' # mov menuItem, eax - br'\xA3....' # mov , eax - )[0], - 'int fullScreenImg': self.find_addr( - br'\xBF....' # mov edi, - br'\xA3(....)' # mov fullScreenImg, eax - br'\x83\xC4\x08' # add esp, 8 - )[0], - 'int demo': self.find_addr( - br'\x8B\xC6' # mov eax, esi - br'\x8B\x0D(....)' # mov ecx, demo - br'\x85\xC9' # test ecx, ecx - )[0], - 'int inGame': self.find_addr( - br'\xC7\x05....\x05\x00\x00\x00' # mov , 5 - br'\x89\x35(....)' # mov inGame, esi - br'\xE8....' # call - )[0], - 'int levelFinished': self.find_addr( - br'\x7E\x42' # jle - br'\xA1(....)' # mov eax, levelFinished - br'\x85\xC0' # test eax, eax - )[0], - 'int levelEndTimer': self.find_addr( - br'\xA3....' # mov levelFinished, eax - br'\xA3(....)' # mov eax, levelFinished - br'\x8B\x75\x24' # mov esi, [ebp+24h] - )[0] - } - - def find_addr(self, pattern): - matches = re.findall(pattern, self.exe) - if len(matches) == 0: - raise RuntimeError('No matches found') - if len(matches) > 1: - raise RuntimeError('Multiple matches found') - match = (matches[0],) if type(matches[0]) is bytes else matches[0] - - return tuple(self.va2rva(int.from_bytes(m, byteorder='little')) - for m in match) - - def va2rva(self, va): - return va - self.pe.OPTIONAL_HEADER.ImageBase - -class AslWriter: - def __init__(self, versions): - self.versions = versions - - def write(self, out): - out.write('\n'.join(self.create_state_descriptors() + ( - self.create_init_action(), - self.create_update_action(), - self.create_start_action(), - self.create_isLoading_action(), - self.create_split_action() - )).replace('\t', ASL_INDENT)) - - def create_state_descriptors(self): - return tuple(self.create_state_descriptor(ver) - for ver in self.versions) - - def create_state_descriptor(self, ver): - max_name_len = max(len(name) for name in ver.addrs.keys()) - return f'''\ -state("{PROCESS_NAME}", "{ver.name}") {{ -''' + \ -'\n'.join( - f'\t{name: <{max_name_len}} : 0x{addr:06X};' - for name, addr in ver.addrs.items() -) + f''' -}} -''' - - def create_init_action(self): - return '''\ -init { - var versions = new dynamic[,] - { -''' + \ -',\n'.join( - f'\t\t{{ 0x{ver.tmstmp:08X}, 0x{ver.tmstmp_pos:03X}, "{ver.name}" }}' - for ver in self.versions -) + ''' - }; - var baseAddr = modules.First().BaseAddress; - for (int i = 0; i < versions.GetLength(0); ++i) - { - var timestamp = versions[i, 0]; - IntPtr posTimestamp = baseAddr + versions[i, 1]; - var name = versions[i, 2]; - if (memory.ReadValue(posTimestamp) == timestamp) - { - version = name; - break; - } - } -} -''' - - @staticmethod - def create_update_action(): - return '''\ -update { - return version != ""; -} -''' - - @staticmethod - def create_start_action(): - return '''\ -start { - return current.menuItem == 0 && current.demo == 0 && current.inGame > old.inGame; -} -''' - - @staticmethod - def create_isLoading_action(): - return '''\ -isLoading { - return current.fullScreenImg != 0 && current.inGame != 0; -} -''' - - @staticmethod - def create_split_action(): - return '''\ -split { - if(current.levelFinished == 1) - return current.levelEndTimer >= 32828 && old.levelEndTimer < 32828; - else if(current.levelFinished == 2) - return current.levelEndTimer >= 32780 && old.levelEndTimer < 32780; - else - return false; -} -''' - -if __name__ == '__main__': - main()