diff --git a/requirements.txt b/requirements.txt index d7ce66f..ca6e33d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ crossfiledialog bimpy==0.1.1 mymcplus==3.0.4 +diff-match-patch==20200713 diff --git a/setup.py b/setup.py index 66b51e5..9fccd5f 100755 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name='slimseditor', - version='0.0.5', + version='0.0.6', description='A savegame editor for the Ratchet and Clank games', author='Maikel Wever', author_email='maikelwever@gmail.com', diff --git a/slimseditor/frames.py b/slimseditor/frames.py index 0e5dab6..c0d49e4 100644 --- a/slimseditor/frames.py +++ b/slimseditor/frames.py @@ -1,13 +1,15 @@ from collections import defaultdict +from datetime import datetime from io import BytesIO import bimpy import crossfiledialog +from diff_match_patch import diff_match_patch from mymcplus import ps2mc from slimseditor.backends import AbstractBackend, PS2WrappedBinBackend - +from slimseditor.hexdump import hexdump counter = 0 @@ -18,6 +20,11 @@ def get_next_count(): return value +def format_patchline(t, text): + text = text.strip('\n').replace('\n', '\n ') + return f"{'+' if t == 1 else '-'} {text}" + + class FrameBase: def __init__(self): self._size = None @@ -42,6 +49,7 @@ def __init__(self, backend_class, *backend_args, **backend_kwargs): self.load_backend() self.name = '{0}##{1}'.format(self.backend.get_friendly_name(), get_next_count()) + self.diff_string = "" def load_backend(self): self.backend = self.backend_class(*self.backend_args, **self.backend_kwargs) # type: AbstractBackend @@ -62,8 +70,12 @@ def render(self): bimpy.menu_item('Save', 'Cmd+S', self.click_states['save']) bimpy.menu_item('Reload', 'Cmd+R', self.click_states['reload']) bimpy.menu_item('Export', 'Cmd+E', self.click_states['export']) + bimpy.menu_item('Reload & Diff', 'Cmd+D', self.click_states['reload_and_diff']) bimpy.end_menu_bar() + if self.diff_string: + bimpy.columns(2, "hex split") + bimpy.text('Game: ') bimpy.same_line() bimpy.text(self.backend.game.value) @@ -73,8 +85,26 @@ def render(self): for item in section_items: item.render_widget() + if self.diff_string: + bimpy.next_column() + bimpy.text(self.diff_string) + bimpy.end() + def reload_and_diff(self): + pre_reload_hex = hexdump(self.backend.data, print_ascii=False) + self.load_backend() + post_reload_hex = hexdump(self.backend.data, print_ascii=False) + + patcher = diff_match_patch() + text1, text2, line_array = patcher.diff_linesToChars(pre_reload_hex, post_reload_hex) + diffs = patcher.diff_main(text1, text2) + patcher.diff_cleanupSemantic(diffs) + patcher.diff_charsToLines(diffs, line_array) + patch = '\n'.join(format_patchline(t, text) for t, text in diffs if t != 0) + + self.diff_string = f'{datetime.now()}\n{patch}\n\n{self.diff_string}' + def process_events(self): if self.click_states['save'].value: self.backend.write_all_items(self.items) @@ -85,6 +115,10 @@ def process_events(self): self.load_backend() self.click_states['reload'].value = False + if self.click_states['reload_and_diff'].value: + self.reload_and_diff() + self.click_states['reload_and_diff'].value = False + if self.click_states['export'].value: filename = crossfiledialog.save_file() if filename: diff --git a/slimseditor/hexdump.py b/slimseditor/hexdump.py new file mode 100644 index 0000000..c1d7b95 --- /dev/null +++ b/slimseditor/hexdump.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 + +""" +Hexdump by 7hrrAm from https://gist.github.com/7h3rAm/5603718 +Based in part on sbz version from https://gist.github.com/sbz/1080258 +Modified by maikelwever to make ascii printing optional. +""" + +def hexdump(src, length=16, sep='.', print_ascii=True): + """ + >>> print(hexdump('\x01\x02\x03\x04AAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBB')) + 00000000: 01 02 03 04 41 41 41 41 41 41 41 41 41 41 41 41 |....AAAAAAAAAAAA| + 00000010: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 42 42 |AAAAAAAAAAAAAABB| + 00000020: 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 |BBBBBBBBBBBBBBBB| + 00000030: 42 42 42 42 42 42 42 42 |BBBBBBBB| + >>> + >>> print(hexdump(b'\x01\x02\x03\x04AAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBB')) + 00000000: 01 02 03 04 41 41 41 41 41 41 41 41 41 41 41 41 |....AAAAAAAAAAAA| + 00000010: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 42 42 |AAAAAAAAAAAAAABB| + 00000020: 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 |BBBBBBBBBBBBBBBB| + 00000030: 42 42 42 42 42 42 42 42 |BBBBBBBB| + """ + FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or sep for x in range(256)]) + lines = [] + for c in range(0, len(src), length): + chars = src[c:c+length] + hexstr = ' '.join(["%02x" % ord(x) for x in chars]) if type(chars) is str else ' '.join(['{:02x}'.format(x) for x in chars]) + if len(hexstr) > 24: + hexstr = "%s %s" % (hexstr[:24], hexstr[24:]) + if print_ascii: + printable = ''.join(["%s" % ((ord(x) <= 127 and FILTER[ord(x)]) or sep) for x in chars]) if type(chars) is str else ''.join(['{}'.format((x <= 127 and FILTER[x]) or sep) for x in chars]) + lines.append("%08x: %-*s |%s|" % (c, length*3, hexstr, printable)) + else: + lines.append("%08x: %-*s" % (c, length * 3, hexstr)) + return '\n'.join(lines) + +if __name__ == "__main__": + print(hexdump('\x01\x02\x03\x04AAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBB')) + print(hexdump(b'\x01\x02\x03\x04AAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBB'))