Skip to content

Commit

Permalink
Add the --patch option to snapmod.py
Browse files Browse the repository at this point in the history
  • Loading branch information
skoolkid committed Jan 20, 2025
1 parent a83d3e7 commit 3c81f9d
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 2 deletions.
6 changes: 5 additions & 1 deletion skoolkit/snapmod.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2015-2017, 2023 Richard Dymond (rjdymond@gmail.com)
# Copyright 2015-2017, 2023, 2025 Richard Dymond (rjdymond@gmail.com)
#
# This file is part of SkoolKit.
#
Expand All @@ -21,6 +21,7 @@

def run(infile, options, outfile):
snapshot = Snapshot.get(infile)
snapshot.patch(options.patches)
snapshot.move(options.moves)
snapshot.poke(options.pokes)
snapshot.set_registers_and_state(options.reg, options.state)
Expand All @@ -37,6 +38,9 @@ def main(args):
group = parser.add_argument_group('Options')
group.add_argument('-m', '--move', dest='moves', metavar='[s:]src,size,[d:]dest', action='append', default=[],
help='Copy a block of bytes of the given size from src in RAM bank s to dest in RAM bank d. This option may be used multiple times.')
group.add_argument('--patch', dest='patches', metavar='[p:]a,file', action='append', default=[],
help="Apply a binary patch file at address 'a' in RAM bank 'p'. "
"This option may be used multiple times.")
group.add_argument('-p', '--poke', dest='pokes', metavar='[p:]a[-b[-c]],[^+]v', action='append', default=[],
help="POKE N,v in RAM bank p for N in {a, a+c, a+2c..., b}. "
"Prefix 'v' with '^' to perform an XOR operation, or '+' to perform an ADD operation. "
Expand Down
23 changes: 22 additions & 1 deletion skoolkit/snapshot.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2009-2013, 2015-2024 Richard Dymond (rjdymond@gmail.com)
# Copyright 2009-2013, 2015-2025 Richard Dymond (rjdymond@gmail.com)
#
# This file is part of SkoolKit.
#
Expand Down Expand Up @@ -177,6 +177,10 @@ def get(cls, sfile, ext=None):
def ram(self, page=None):
return self.memory.ram(page)

def patch(self, specs):
for spec in specs:
patch(self.memory, spec)

def move(self, specs):
for spec in specs:
move(self.memory, spec)
Expand Down Expand Up @@ -807,6 +811,23 @@ def _get_page(param, desc, spec, default=None):
raise SkoolKitError(f'Invalid page number in {desc} spec: {spec}')
return default, param

def patch(snapshot, spec):
addr, sep, fname = spec.partition(',')
if not sep:
raise SkoolKitError(f'Filename missing in patch spec: {spec}')
page, addr = _get_page(addr, 'patch', spec)
try:
address = get_int_param(addr, True)
except ValueError:
raise SkoolKitError(f'Invalid address in patch spec: {spec}')
data = read_bin_file(fname, 0xC000)
if page is None:
snapshot[address:address + len(data)] = data
elif hasattr(snapshot, 'banks'):
dest = address % 0x4000
size = min(0x4000 - dest, len(data))
snapshot.banks[page % 8][dest:dest + size] = data[:size]

def move(snapshot, param_str):
try:
src, length, dest = param_str.split(',', 2)
Expand Down
2 changes: 2 additions & 0 deletions sphinx/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Changelog
:ref:`tap2sna.py <tap2sna-conf>` (to specify the User-Agent header in
HTTP/HTTPS requests)
* Added support to the :ref:`CALL` macro for calling an arbitrary function
* Added the ``--patch`` option to :ref:`snapmod.py` (for applying a binary
patch file)
* The :ref:`REG` macro now always renders the IXh, IXl, IYh and IYl registers
with a lower case 'h' or 'l'
* Added the :ref:`FRAMES` macro (as an alternative to the ``#UDGARRAY*`` macro,
Expand Down
4 changes: 4 additions & 0 deletions sphinx/source/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,8 @@ To list the options supported by `snapmod.py`, run it with no arguments::
Copy a block of bytes of the given size from src in
RAM bank s to dest in RAM bank d. This option may be
used multiple times.
--patch [p:]a,file Apply a binary patch file at address 'a' in RAM bank
'p'. This option may be used multiple times.
-p [p:]a[-b[-c]],[^+]v, --poke [p:]a[-b[-c]],[^+]v
POKE N,v in RAM bank p for N in {a, a+c, a+2c..., b}.
Prefix 'v' with '^' to perform an XOR operation, or
Expand All @@ -1539,6 +1541,8 @@ To list the options supported by `snapmod.py`, run it with no arguments::
+---------+-------------------------------------------------------------------+
| Version | Changes |
+=========+===================================================================+
| 9.5 | Added the ``--patch`` option |
+---------+-------------------------------------------------------------------+
| 9.1 | Added support for modifying SZX snapshots and 128K snapshots; the |
| | ``--move`` and ``--poke`` options can modify specific RAM banks |
+---------+-------------------------------------------------------------------+
Expand Down
5 changes: 5 additions & 0 deletions sphinx/source/man/snapmod.py.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ OPTIONS
'dest' must each be a decimal number, or a hexadecimal number prefixed by
'0x'.

--patch `[p:]a,file`
Apply a binary patch file at address 'a' in RAM bank 'p'. This option may be
used multiple times. 'a' must be a decimal number, or a hexadecimal number
prefixed by '0x'.

-p, --poke `[p:]a[-b[-c]],[^+]v`
POKE N,v in RAM bank p for N in {a, a+c, a+2c..., b}. Prefix 'v' with '^' to
perform an XOR operation, or '+' to perform an ADD operation. This option may
Expand Down
76 changes: 76 additions & 0 deletions tests/test_snapmod.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,82 @@ def test_option_m_invalid_values(self):
self._test_bad_spec('-m 1,y,3', infile, 'Invalid integer in move spec: 1,y,3')
self._test_bad_spec('-m 1,2,z', infile, 'Invalid integer in move spec: 1,2,z')

def test_option_patch_z80(self):
addr = 32768
data = (1, 2, 3, 4, 5)
pfile = self.write_bin_file(data)
header = [0] * 86
header[30] = 54 # Version 3
ram = [0] * 49152
exp_ram = ram[:]
exp_ram[addr - 0x4000:addr - 0x4000 + len(data)] = data
exp_header = header[:]
self._test_z80(f'--patch {addr},{pfile}', header, exp_header, ram, exp_ram, 3, False)

def test_option_patch_szx(self):
bank, addr = 2, 40000
data = (2, 4, 6, 8, 10)
pfile = self.write_bin_file(data)
exp_block_diffs = None
exp_ram_diffs = {bank: [0] * 0x4000}
exp_ram_diffs[bank][addr % 0x4000:addr % 0x4000 + len(data)] = data
self._test_szx(f'--patch {addr},{pfile}', exp_block_diffs, exp_ram_diffs, 48)

def test_option_patch_with_page_number(self):
page, addr = 3, 0x1000
data = (3, 6, 9, 12, 15)
pfile = self.write_bin_file(data)
header = self._get_header(3, True)
exp_header = header[:]
ram = [0] * 0x20000
exp_ram = ram[:]
p_addr = page * 0x4000 + addr
exp_ram[p_addr:p_addr + len(data)] = data
self._test_z80_128k(f'--patch {page}:{addr},{pfile}', header, exp_header, ram, exp_ram, 3)

def test_option_patch_0x_address(self):
addr = 0xABCD
data = (4, 8, 12)
pfile = self.write_bin_file(data)
header = [0] * 86
header[30] = 54 # Version 3
ram = [0] * 49152
exp_ram = ram[:]
exp_ram[addr - 0x4000:addr - 0x4000 + len(data)] = data
exp_header = header[:]
self._test_z80(f'--patch 0x{addr:04x},{pfile}', header, exp_header, ram, exp_ram, 3, False)

def test_option_patch_multiple(self):
patches = (
(24576, (1, 10, 11, 54)),
(32768, (2, 3, 5)),
(50000, (9, 17, 43, 1))
)
header = [0] * 86
header[30] = 54 # Version 3
exp_header = header[:]
ram = [0] * 49152
exp_ram = ram[:]
options = []
for addr, data in patches:
exp_ram[addr - 0x4000:addr - 0x4000 + len(data)] = data
pfile = self.write_bin_file(data)
options.append(f'--patch {addr},{pfile}')
self._test_z80(' '.join(options), header, exp_header, ram, exp_ram, 3, False)

def test_option_patch_nonexistent_patch_file(self):
infile = self.write_z80_file([1] * 30, [0] * 49152, 1)
pfile = 'non-existent.bin'
with self.assertRaises(SkoolKitError) as cm:
self.run_snapmod(f'--patch 32768,{pfile} {infile}')
self.assertEqual(cm.exception.args[0], f'{pfile}: file not found')

def test_option_patch_invalid_values(self):
infile = self.write_z80_file([1] * 30, [0] * 49152, 1)
self._test_bad_spec('--patch 1', infile, 'Filename missing in patch spec: 1')
self._test_bad_spec('--patch x,p.bin', infile, 'Invalid address in patch spec: x,p.bin')
self._test_bad_spec('--patch q:0,p.bin', infile, 'Invalid page number in patch spec: q:0,p.bin')

@patch.object(snapmod, 'run', mock_run)
def test_options_p_poke(self):
for option in ('-p', '--poke'):
Expand Down

0 comments on commit 3c81f9d

Please sign in to comment.