From dafe66191c080a8110908730098b1dafea9559b0 Mon Sep 17 00:00:00 2001 From: itsmattkc <34096995+itsmattkc@users.noreply.github.com> Date: Mon, 19 Jun 2023 10:56:53 -0700 Subject: [PATCH 1/3] mxomni: fixed minor inaccuracy --- LEGO1/mxomni.cpp | 1 + LEGO1/mxomni.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/LEGO1/mxomni.cpp b/LEGO1/mxomni.cpp index 7268224f..9752f6ea 100644 --- a/LEGO1/mxomni.cpp +++ b/LEGO1/mxomni.cpp @@ -15,6 +15,7 @@ MxOmni::~MxOmni() Destroy(); } +// OFFSET: LEGO1 0x100af080 void MxOmni::Init() { m_windowHandle = NULL; diff --git a/LEGO1/mxomni.h b/LEGO1/mxomni.h index 8b794ebe..4c7d29e8 100644 --- a/LEGO1/mxomni.h +++ b/LEGO1/mxomni.h @@ -57,7 +57,7 @@ class MxOmni : public MxCore MxCriticalSection m_criticalsection; // 0x48 - int m_unk64; // 0x64 + unsigned char m_unk64; // 0x64 }; __declspec(dllexport) MxTimer * Timer(); From ec12b8f30f2c75f18185c0e64d3bd1422ffb5da4 Mon Sep 17 00:00:00 2001 From: itsmattkc <34096995+itsmattkc@users.noreply.github.com> Date: Mon, 19 Jun 2023 10:57:13 -0700 Subject: [PATCH 2/3] improved compare script performance and reliability --- tools/reccomp/reccomp.py | 168 +++++++++++++++++++++++++++------------ 1 file changed, 115 insertions(+), 53 deletions(-) diff --git a/tools/reccomp/reccomp.py b/tools/reccomp/reccomp.py index dd601e53..73180333 100755 --- a/tools/reccomp/reccomp.py +++ b/tools/reccomp/reccomp.py @@ -89,85 +89,148 @@ def read(self, offset, size): self.file.seek(self.get_addr(offset)) return self.file.read(size) -line_dump = None - -origfile = Bin(original) -recompfile = Bin(recomp) - class RecompiledInfo: addr = None size = None name = None -print() +def get_wine_path(fn): + return subprocess.check_output(['winepath', '-w', fn]).decode('utf-8').strip() -def get_recompiled_address(filename, line): - global line_dump, sym_dump +def get_unix_path(fn): + return subprocess.check_output(['winepath', fn]).decode('utf-8').strip() - def get_wine_path(fn): - return subprocess.check_output(['winepath', '-w', fn]).decode('utf-8').strip() +# Declare a class that parses the output of cvdump for fast access later +class SymInfo: + funcs = {} + lines = {} - # Load source lines from PDB - if not line_dump: + def __init__(self, pdb, file): call = [os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), 'cvdump.exe'), '-l', '-s'] if os.name != 'nt': # Run cvdump through wine and convert path to Windows-friendly wine path call.insert(0, 'wine') - call.append(get_wine_path(syms)) + call.append(get_wine_path(pdb)) else: - call.append(syms) + call.append(pdb) + + print('Parsing %s...' % pdb) line_dump = subprocess.check_output(call).decode('utf-8').split('\r\n') - # Find requested filename/line in PDB - if os.name != 'nt': - # Convert filename to Wine path - filename = get_wine_path(filename) + current_section = None - #print('Looking for ' + filename + ' line ' + str(line)) + for i, line in enumerate(line_dump): + if line.startswith('***'): + current_section = line[4:] - addr = None - found = False + if current_section == 'SYMBOLS' and 'S_GPROC32' in line: + addr = int(line[26:34], 16) - for i, s in enumerate(line_dump): - try: - sourcepath = s.split()[0] - if os.path.isfile(sourcepath) and os.path.samefile(sourcepath, filename): - lines = line_dump[i + 2].split() - if line == int(lines[0]): - # Found address - addr = int(lines[1], 16) - found = True - break - except IndexError: - pass + info = RecompiledInfo() + info.addr = addr + recompfile.imagebase + recompfile.textvirt + info.size = int(line[41:49], 16) + info.name = line[77:] - if found: - # Find size of function - for i, s in enumerate(line_dump): - if 'S_GPROC32' in s: - if int(s[26:34], 16) == addr: - obj = RecompiledInfo() - obj.addr = addr + recompfile.imagebase + recompfile.textvirt - obj.size = int(s[41:49], 16) - obj.name = s[77:] + self.funcs[addr] = info + elif current_section == 'LINES' and line.startswith(' ') and not line.startswith(' '): + sourcepath = line.split()[0] - return obj + if os.name != 'nt': + # Convert filename to Unix path for file compare + sourcepath = get_unix_path(sourcepath) + + if sourcepath not in self.lines: + self.lines[sourcepath] = {} + + j = i + 2 + while True: + ll = line_dump[j].split() + if len(ll) == 0: + break + + k = 0 + while k < len(ll): + linenum = int(ll[k + 0]) + address = int(ll[k + 1], 16) + if linenum not in self.lines[sourcepath]: + self.lines[sourcepath][linenum] = address + k += 2 + + j += 1 + + def get_recompiled_address(self, filename, line): + addr = None + found = False + + #print('Looking for ' + filename + ' line ' + str(line)) + + for fn in self.lines: + # Sometimes a PDB is compiled with a relative path while we always have + # an absolute path. Therefore we must + if os.path.samefile(fn, filename): + filename = fn + break + + if filename in self.lines and line in self.lines[fn]: + addr = self.lines[fn][line] + + if addr in self.funcs: + return self.funcs[addr] + else: + print('Failed to find function symbol with address: %s' % hex(addr)) + else: + print('Failed to find function symbol with filename and line: %s:%s' % (filename, str(line))) + +origfile = Bin(original) +recompfile = Bin(recomp) +syminfo = SymInfo(syms, recompfile) + +print() md = Cs(CS_ARCH_X86, CS_MODE_32) +def sanitize(file, mnemonic, op_str): + if mnemonic == 'call' or mnemonic == 'jmp': + # Filter out "calls" because the offsets we're not currently trying to + # match offsets. As long as there's a call in the right place, it's + # probably accurate. + op_str = '' + else: + # Filter out dword ptrs where the pointer is to an offset + try: + start = op_str.index('dword ptr [') + 11 + end = op_str.index(']', start) + + # This will throw ValueError if not hex + inttest = int(op_str[start:end], 16) + + op_str = op_str[0:start] + op_str[end:] + except ValueError: + pass + + # Use heuristics to filter out any args that look like offsets + words = op_str.split(' ') + for i, word in enumerate(words): + try: + inttest = int(word, 16) + if inttest >= file.imagebase + file.textvirt: + words[i] = '' + except ValueError: + pass + op_str = ' '.join(words) + + return mnemonic, op_str + def parse_asm(file, addr, size): asm = [] data = file.read(addr, size) for i in md.disasm(data, 0): - if i.mnemonic == 'call': - # Filter out "calls" because the offsets we're not currently trying to - # match offsets. As long as there's a call in the right place, it's - # probably accurate. - asm.append(i.mnemonic) - else: - asm.append("%s %s" % (i.mnemonic, i.op_str)) + # Use heuristics to disregard some differences that aren't representative + # of the accuracy of a function (e.g. global offsets) + mnemonic, op_str = sanitize(file, i.mnemonic, i.op_str) + asm.append("%s %s" % (mnemonic, op_str)) return asm function_count = 0 @@ -197,9 +260,8 @@ def parse_asm(file, addr, size): find_open_bracket = srcfile.readline() line_no += 1 - recinfo = get_recompiled_address(srcfilename, line_no) + recinfo = syminfo.get_recompiled_address(srcfilename, line_no) if not recinfo: - print('Failed to find recompiled address of ' + hex(addr)) continue origasm = parse_asm(origfile, addr, recinfo.size) @@ -207,7 +269,7 @@ def parse_asm(file, addr, size): diff = difflib.SequenceMatcher(None, origasm, recompasm) ratio = diff.ratio() - print('%s (%s / %s) is %.2f%% similar to the original' % (recinfo.name, hex(addr), hex(recinfo.addr), ratio * 100)) + print(' %s (%s / %s) is %.2f%% similar to the original' % (recinfo.name, hex(addr), hex(recinfo.addr), ratio * 100)) function_count += 1 total_accuracy += ratio From ff85548c85b4b60662cca5ec24c02907552f708a Mon Sep 17 00:00:00 2001 From: itsmattkc <34096995+itsmattkc@users.noreply.github.com> Date: Mon, 19 Jun 2023 11:02:44 -0700 Subject: [PATCH 3/3] project update --- isle.mak | 70 ++++--------------------------------------------------- isle.mdp | Bin 49152 -> 48128 bytes 2 files changed, 5 insertions(+), 65 deletions(-) diff --git a/isle.mak b/isle.mak index 20480ff7..fea5a64e 100644 --- a/isle.mak +++ b/isle.mak @@ -539,7 +539,6 @@ DEP_CPP_LEGOO=\ SOURCE=.\LEGO1\mxcriticalsection.cpp DEP_CPP_MXCRI=\ - ".\LEGO1\legoinc.h"\ ".\LEGO1\mxcriticalsection.h"\ @@ -553,7 +552,6 @@ DEP_CPP_MXCRI=\ SOURCE=.\LEGO1\mxautolocker.cpp DEP_CPP_MXAUT=\ - ".\LEGO1\legoinc.h"\ ".\LEGO1\mxautolocker.h"\ ".\LEGO1\mxcriticalsection.h"\ @@ -640,7 +638,6 @@ DEP_CPP_MXVID=\ SOURCE=.\LEGO1\mxvideoparamflags.cpp DEP_CPP_MXVIDE=\ - ".\LEGO1\mxbool.h"\ ".\LEGO1\mxvideoparamflags.h"\ @@ -654,7 +651,6 @@ DEP_CPP_MXVIDE=\ SOURCE=.\LEGO1\mxomnicreateparam.cpp DEP_CPP_MXOMNI=\ - ".\LEGO1\legoinc.h"\ ".\LEGO1\mxbool.h"\ ".\LEGO1\mxcore.h"\ ".\LEGO1\mxomnicreateflags.h"\ @@ -678,7 +674,6 @@ DEP_CPP_MXOMNI=\ SOURCE=.\LEGO1\mxomnicreateparambase.cpp DEP_CPP_MXOMNIC=\ - ".\LEGO1\legoinc.h"\ ".\LEGO1\mxbool.h"\ ".\LEGO1\mxcore.h"\ ".\LEGO1\mxomnicreateflags.h"\ @@ -821,7 +816,6 @@ DEP_CPP_ISLE_=\ ".\LEGO1\legobuildingmanager.h"\ ".\LEGO1\legoentity.h"\ ".\LEGO1\legogamestate.h"\ - ".\LEGO1\legoinc.h"\ ".\LEGO1\legoinputmanager.h"\ ".\LEGO1\legomodelpresenter.h"\ ".\LEGO1\legonavcontroller.h"\ @@ -873,9 +867,6 @@ DEP_CPP_ISLE_=\ # Begin Source File SOURCE=.\ISLE\main.cpp - -!IF "$(CFG)" == "ISLE - Win32 Release" - DEP_CPP_MAIN_=\ ".\ISLE\define.h"\ ".\ISLE\isle.h"\ @@ -926,77 +917,26 @@ DEP_CPP_MAIN_=\ $(CPP) $(CPP_PROJ) $(SOURCE) -!ELSEIF "$(CFG)" == "ISLE - Win32 Debug" - -DEP_CPP_MAIN_=\ - ".\ISLE\define.h"\ - ".\ISLE\isle.h"\ - ".\LEGO1\lego3dmanager.h"\ - ".\LEGO1\lego3dview.h"\ - ".\LEGO1\legoentity.h"\ - ".\LEGO1\legogamestate.h"\ - ".\LEGO1\legoinc.h"\ - ".\LEGO1\legoinputmanager.h"\ - ".\LEGO1\legonavcontroller.h"\ - ".\LEGO1\legoomni.h"\ - ".\LEGO1\legoroi.h"\ - ".\LEGO1\legovideomanager.h"\ - ".\LEGO1\mxatomid.h"\ - ".\LEGO1\mxbackgroundaudiomanager.h"\ - ".\LEGO1\mxbool.h"\ - ".\LEGO1\mxcore.h"\ - ".\LEGO1\mxcriticalsection.h"\ - ".\LEGO1\mxdsaction.h"\ - ".\LEGO1\mxdsfile.h"\ - ".\LEGO1\mxdsobject.h"\ - ".\LEGO1\mxeventmanager.h"\ - ".\LEGO1\mxmusicmanager.h"\ - ".\LEGO1\mxnotificationmanager.h"\ - ".\LEGO1\mxobjectfactory.h"\ - ".\LEGO1\mxomni.h"\ - ".\LEGO1\mxomnicreateflags.h"\ - ".\LEGO1\mxomnicreateparam.h"\ - ".\LEGO1\mxomnicreateparambase.h"\ - ".\LEGO1\mxpalette.h"\ - ".\LEGO1\mxrect32.h"\ - ".\LEGO1\mxresult.h"\ - ".\LEGO1\mxsoundmanager.h"\ - ".\LEGO1\mxstreamcontroller.h"\ - ".\LEGO1\mxstreamer.h"\ - ".\LEGO1\mxstring.h"\ - ".\LEGO1\mxticklemanager.h"\ - ".\LEGO1\mxtimer.h"\ - ".\LEGO1\mxtransitionmanager.h"\ - ".\LEGO1\mxvariabletable.h"\ - ".\LEGO1\mxvideomanager.h"\ - ".\LEGO1\mxvideoparam.h"\ - ".\LEGO1\mxvideoparamflags.h"\ - ".\LEGO1\viewmanager.h"\ - - -"$(INTDIR)\main.obj" : $(SOURCE) $(DEP_CPP_MAIN_) "$(INTDIR)" - $(CPP) $(CPP_PROJ) $(SOURCE) - - -!ENDIF - # End Source File ################################################################################ # Begin Source File SOURCE=.\ISLE\res\isle.rc +DEP_RSC_ISLE_R=\ + ".\ISLE\res\resource.h"\ + !IF "$(CFG)" == "ISLE - Win32 Release" -"$(INTDIR)\isle.res" : $(SOURCE) "$(INTDIR)" +"$(INTDIR)\isle.res" : $(SOURCE) $(DEP_RSC_ISLE_R) "$(INTDIR)" $(RSC) /l 0x409 /fo"$(INTDIR)/isle.res" /i "ISLE\res" /d "NDEBUG" $(SOURCE) !ELSEIF "$(CFG)" == "ISLE - Win32 Debug" -"$(INTDIR)\isle.res" : $(SOURCE) "$(INTDIR)" +"$(INTDIR)\isle.res" : $(SOURCE) $(DEP_RSC_ISLE_R) "$(INTDIR)" $(RSC) /l 0x409 /fo"$(INTDIR)/isle.res" /i "ISLE\res" /d "_DEBUG" $(SOURCE) diff --git a/isle.mdp b/isle.mdp index 93e690ea4aec5678b4f2cae8eb3bbdf233280e9b..f31e19c58e9a2653cf5210cbf3d5aeac9fd27672 100644 GIT binary patch literal 48128 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 A(f|Me