Skip to content

Commit

Permalink
Make rzxplay.py an official SkoolKit command
Browse files Browse the repository at this point in the history
  • Loading branch information
skoolkid committed Feb 16, 2024
1 parent 3036a67 commit 9dadb4d
Show file tree
Hide file tree
Showing 11 changed files with 630 additions and 77 deletions.
26 changes: 26 additions & 0 deletions rzxplay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env python3

# Copyright 2024 Richard Dymond (rjdymond@gmail.com)
#
# This file is part of SkoolKit.
#
# SkoolKit is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# SkoolKit is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# SkoolKit. If not, see <http://www.gnu.org/licenses/>.

import sys

from skoolkit import rzxplay, error, SkoolKitError

try:
rzxplay.main(sys.argv[1:])
except SkoolKitError as e:
error(e.args[0])
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ scripts =
bin2sna.py
bin2tap.py
rzxinfo.py
rzxplay.py
skool2asm.py
skool2bin.py
skool2ctl.py
Expand Down
108 changes: 57 additions & 51 deletions utils/rzxplay.py → skoolkit/rzxplay.py
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,34 +1,38 @@
#!/usr/bin/env python3
# Copyright 2024 Richard Dymond (rjdymond@gmail.com)
#
# This file is part of SkoolKit.
#
# SkoolKit is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# SkoolKit is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# SkoolKit. If not, see <http://www.gnu.org/licenses/>.

import argparse
import contextlib
from functools import partial
import io
import os
import sys
import zlib

with contextlib.redirect_stdout(io.StringIO()) as pygame_io:
try:
import pygame
except ImportError:
except ImportError: # pragma: no cover
pygame = None

SKOOLKIT_HOME = os.environ.get('SKOOLKIT_HOME')
if not SKOOLKIT_HOME:
sys.stderr.write('SKOOLKIT_HOME is not set; aborting\n')
sys.exit(1)
if not os.path.isdir(SKOOLKIT_HOME):
sys.stderr.write(f'SKOOLKIT_HOME={SKOOLKIT_HOME}; directory not found\n')
sys.exit(1)
sys.path.insert(0, SKOOLKIT_HOME)

from skoolkit import ROM48, error, get_dword, get_word, read_bin_file, write
from skoolkit.pagingtracer import Memory
from skoolkit.snapshot import Snapshot
from skoolkit import VERSION, SkoolKitError, get_dword, get_word, read_bin_file, write
from skoolkit.simulator import Simulator, R1
from skoolkit.snapshot import Snapshot
from skoolkit.traceutils import disassemble

if pygame:
if pygame: # pragma: no cover
COLOURS = (
pygame.Color(0x00, 0x00, 0x00), # Black
pygame.Color(0x00, 0x00, 0xc5), # Blue
Expand Down Expand Up @@ -71,7 +75,7 @@ def __init__(self, simulator, frames, tstates):

def next_frame(self):
if self.readings:
error(f'{len(self.readings)} port reading(s) left for frame {self.frame_index}')
raise SkoolKitError(f'{len(self.readings)} port reading(s) left for frame {self.frame_index}')
self.frame_index += 1
if self.frame_index < len(self.frames):
frame = self.frames[self.frame_index]
Expand All @@ -81,7 +85,7 @@ def next_frame(self):
def read_port(self, registers, port):
if self.readings:
return self.readings.pop(0)
error(f'Port readings exhausted for frame {self.frame_index}')
raise SkoolKitError(f'Port readings exhausted for frame {self.frame_index}')

def halt(self, registers):
# HALT
Expand All @@ -99,10 +103,9 @@ def write_port(self, registers, port, value):
self.out7ffd = value

def parse_rzx(rzxfile):
with open(rzxfile, 'rb') as f:
data = f.read()
data = read_bin_file(rzxfile)
if data[:4] != b'RZX!' or len(data) < 10:
error('Not an RZX file')
raise SkoolKitError('Not an RZX file')
i = 10
contents = []
while i < len(data):
Expand All @@ -112,7 +115,7 @@ def parse_rzx(rzxfile):
# Snapshot
flags = data[i + 5]
if flags & 1:
error('Missing snapshot (external file)')
raise SkoolKitError('Missing snapshot (external file)')
ext = ''.join(chr(b) for b in data[i + 9:i + 13] if b)
sdata = data[i + 17:i + block_len]
if flags & 2:
Expand Down Expand Up @@ -143,7 +146,7 @@ def parse_rzx(rzxfile):
i += block_len
return contents

def draw(screen, memory, frame, pixel_rects, cell_rects, prev_scr):
def draw(screen, memory, frame, pixel_rects, cell_rects, prev_scr): # pragma: no cover
current_scr = memory[16384:23296]
flash_change = (frame % 16) == 0
flash_switch = (frame // 16) % 2
Expand Down Expand Up @@ -204,11 +207,11 @@ def check_supported(snapshot, options):

def process_block(block, options, snapshot, screen, p_rectangles, c_rectangles, clock):
if block is None:
error('Unsupported snapshot type')
raise SkoolKitError('Unsupported snapshot type')
if isinstance(block, Snapshot):
error_msg = check_supported(block, options)
if error_msg:
error(error_msg)
raise SkoolKitError(error_msg)
return block
if snapshot is None:
return
Expand Down Expand Up @@ -253,7 +256,7 @@ def process_block(block, options, snapshot, screen, p_rectangles, c_rectangles,
fetch_counter -= 2
else:
fetch_counter -= 2 - ((registers[15] ^ r0) % 2)
if screen:
if screen: # pragma: no cover
draw(screen, memory, frames, p_rectangles, c_rectangles, prev_scr)
pygame.display.update()
for event in pygame.event.get():
Expand All @@ -274,7 +277,7 @@ def process_block(block, options, snapshot, screen, p_rectangles, c_rectangles,
tracefile.close()

def run(infile, options):
if options.screen and pygame:
if options.screen and pygame: # pragma: no cover
print(pygame_io.getvalue())
pygame.init()
scale = options.scale
Expand All @@ -291,27 +294,30 @@ def run(infile, options):
for block in parse_rzx(infile):
snapshot = process_block(block, options, snapshot, *context)

parser = argparse.ArgumentParser(
usage='%(prog)s [options] FILE',
description="Play an RZX file.",
add_help=False
)
parser.add_argument('infile', help=argparse.SUPPRESS, nargs='?')
group = parser.add_argument_group('Options')
group.add_argument('--force', action='store_true',
help="Force playback when unsupported hardware is detected.")
group.add_argument('--fps', type=int, default=50,
help="Run at this many frames per second (default: 50). "
"0 means maximum speed.")
group.add_argument('--no-screen', dest='screen', action='store_false',
help="Run without a screen.")
group.add_argument('--quiet', action='store_true',
help="Don't print progress percentage.")
group.add_argument('--scale', metavar='SCALE', type=int, default=2, choices=(1, 2, 3, 4),
help="Scale display up by this factor (1-4; default: 2).")
group.add_argument('--trace', metavar='FILE',
help="Log executed instructions to a file.")
namespace, unknown_args = parser.parse_known_args()
if unknown_args or namespace.infile is None:
parser.exit(2, parser.format_help())
run(namespace.infile, namespace)
def main(args):
parser = argparse.ArgumentParser(
usage='rzxplay.py [options] FILE',
description="Play an RZX file.",
add_help=False
)
parser.add_argument('infile', help=argparse.SUPPRESS, nargs='?')
group = parser.add_argument_group('Options')
group.add_argument('--force', action='store_true',
help="Force playback when unsupported hardware is detected.")
group.add_argument('--fps', type=int, default=50,
help="Run at this many frames per second (default: 50). "
"0 means maximum speed.")
group.add_argument('--no-screen', dest='screen', action='store_false',
help="Run without a screen.")
group.add_argument('--quiet', action='store_true',
help="Don't print progress percentage.")
group.add_argument('--scale', metavar='SCALE', type=int, default=2, choices=(1, 2, 3, 4),
help="Scale display up by this factor (1-4; default: 2).")
group.add_argument('--trace', metavar='FILE',
help="Log executed instructions to a file.")
group.add_argument('-V', '--version', action='version', version='SkoolKit {}'.format(VERSION),
help='Show SkoolKit version number and exit.')
namespace, unknown_args = parser.parse_known_args(args)
if unknown_args or namespace.infile is None:
parser.exit(2, parser.format_help())
run(namespace.infile, namespace)
1 change: 1 addition & 0 deletions sphinx/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ Changelog

9.2b1
-----
* Added the :ref:`rzxplay.py` command (for playing an RZX file)
* Added the :ref:`rzxinfo.py` command (for showing the blocks in or extracting
the snapshots from an RZX file)
* Added support to :ref:`tap2sna.py` for TZX block type 0x15 (direct recording)
Expand Down
32 changes: 31 additions & 1 deletion sphinx/source/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ For example::

$ rzxinfo.py game.rzx

To list the options supported by rzxinfo.py, run it with no arguments::
To list the options supported by `rzxinfo.py`, run it with no arguments::

usage: rzxinfo.py [options] FILE

Expand All @@ -222,6 +222,36 @@ To list the options supported by rzxinfo.py, run it with no arguments::
| 9.2 | New |
+---------+---------+

.. _rzxplay.py:

rzxplay.py
----------
`rzxplay.py` plays an RZX file. For example::

$ rzxplay.py game.rzx

To list the options supported by `rzxplay.py`, run it with no arguments::

usage: rzxplay.py [options] FILE

Play an RZX file.

Options:
--force Force playback when unsupported hardware is detected.
--fps FPS Run at this many frames per second (default: 50). 0 means
maximum speed.
--no-screen Run without a screen.
--quiet Don't print progress percentage.
--scale SCALE Scale display up by this factor (1-4; default: 2).
--trace FILE Log executed instructions to a file.
-V, --version Show SkoolKit version number and exit.

+---------+---------+
| Version | Changes |
+=========+=========+
| 9.2 | New |
+---------+---------+

.. _skool2asm.py:

skool2asm.py
Expand Down
2 changes: 2 additions & 0 deletions sphinx/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@
'convert a binary file or snapshot into a TAP file', _authors, 1),
('man/rzxinfo.py', 'rzxinfo.py',
'show the blocks in or extract the snapshots from an RZX file', _authors, 1),
('man/rzxplay.py', 'rzxplay.py',
'play an RZX file', _authors, 1),
('man/skool2asm.py', 'skool2asm.py',
'convert a skool file to ASM format', _authors, 1),
('man/skool2bin.py', 'skool2bin.py',
Expand Down
48 changes: 48 additions & 0 deletions sphinx/source/man/rzxplay.py.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
:orphan:

==========
rzxplay.py
==========

SYNOPSIS
========
``rzxplay.py`` [options] FILE

DESCRIPTION
===========
``rzxplay.py`` plays an RZX file.

OPTIONS
=======
--force
Force playback when unsupported hardware is detected.

--fps FPS
Run at this many frames per second (default: 50). 0 means maximum speed.

--no-screen
Run without a screen.

--quiet
Don't print progress percentage during playback.

--scale SCALE
Scale the display up by this factor (1-4; default: 2).

--trace FILE
Log executed instructions to a file.

-V, --version
Show the SkoolKit version number and exit.

EXAMPLES
========
1. Play ``game.rzx``:

|
| ``rzxplay.py game.rzx``
2. Log the instructions executed while playing ``game.rzx``:

|
| ``rzxplay.py --trace game.log game.rzx``
Loading

0 comments on commit 9dadb4d

Please sign in to comment.