Re-copy fixed repos/zvm source into src/mudlib/zmachine

Copies the cleaned-up zvm source (ruff-compliant, ty-clean) back into
the zmachine module. Adds __init__.py with proper exports and updates
.gitignore for debug.log/disasm.log.
This commit is contained in:
Jared Miller 2026-02-09 20:00:42 -05:00
parent dcc952d4c5
commit 5ea030a0ac
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
20 changed files with 2345 additions and 2209 deletions

View file

@ -20,7 +20,10 @@ Telnet MUD engine built on telnetlib3. Python 3.12+, managed with uv.
- `DREAMBOOK.md` - the vision, philosophy, wild ideas. not a spec - `DREAMBOOK.md` - the vision, philosophy, wild ideas. not a spec
- `scripts/` - standalone tools (map renderer, etc) - `scripts/` - standalone tools (map renderer, etc)
- `build/` - generated output (gitignored) - `build/` - generated output (gitignored)
- `repos/` - symlinked reference repos (telnetlib3, miniboa). gitignored, not our code - `repos/` - symlinked reference repos, gitignored, not our code. includes:
- `repos/viola/` - DFillmore/viola z-machine interpreter (working, global state)
- `repos/zvm/` - sussman/zvm z-machine interpreter (clean architecture, half-built)
- `repos/telnetlib3/`, `repos/miniboa/` - telnet libraries
## Docs ## Docs

2
.gitignore vendored
View file

@ -5,3 +5,5 @@ data
.worktrees .worktrees
.testmondata .testmondata
*.z* *.z*
debug.log
disasm.log

View file

@ -1,5 +1,4 @@
"""Hybrid z-machine interpreter based on sussman/zvm. """Hybrid z-machine interpreter based on sussman/zvm.
Original: https://github.com/sussman/zvm (BSD license) Original: https://github.com/sussman/zvm (BSD license)
Extended with opcode implementations ported from DFillmore/viola. Extended with opcode implementations ported from DFillmore/viola.
""" """

View file

@ -9,6 +9,7 @@
# root directory of this distribution. # root directory of this distribution.
# #
class BitField: class BitField:
"""An bitfield gives read/write access to the individual bits of a """An bitfield gives read/write access to the individual bits of a
value, in array and slice notation. value, in array and slice notation.
@ -58,5 +59,4 @@ class BitField:
def to_str(self, len): def to_str(self, len):
"""Print the binary representation of the bitfield.""" """Print the binary representation of the bitfield."""
return ''.join(["%d" % self[i] return "".join([f"{self[i]}" for i in range(len - 1, -1, -1)])
for i in range(len-1,-1,-1)])

View file

@ -80,37 +80,36 @@ evtype_Redraw = 6
evtype_SoundNotify = 7 evtype_SoundNotify = 7
evtype_Hyperlink = 8 evtype_Hyperlink = 8
class event_t(ctypes.Structure):
_fields_ = [("type", glui32),
("win", winid_t),
("val1", glui32),
("val2", glui32)]
keycode_Unknown = 0xffffffff class event_t(ctypes.Structure):
keycode_Left = 0xfffffffe _fields_ = [("type", glui32), ("win", winid_t), ("val1", glui32), ("val2", glui32)]
keycode_Right = 0xfffffffd
keycode_Up = 0xfffffffc
keycode_Down = 0xfffffffb keycode_Unknown = 0xFFFFFFFF
keycode_Return = 0xfffffffa keycode_Left = 0xFFFFFFFE
keycode_Delete = 0xfffffff9 keycode_Right = 0xFFFFFFFD
keycode_Escape = 0xfffffff8 keycode_Up = 0xFFFFFFFC
keycode_Tab = 0xfffffff7 keycode_Down = 0xFFFFFFFB
keycode_PageUp = 0xfffffff6 keycode_Return = 0xFFFFFFFA
keycode_PageDown = 0xfffffff5 keycode_Delete = 0xFFFFFFF9
keycode_Home = 0xfffffff4 keycode_Escape = 0xFFFFFFF8
keycode_End = 0xfffffff3 keycode_Tab = 0xFFFFFFF7
keycode_Func1 = 0xffffffef keycode_PageUp = 0xFFFFFFF6
keycode_Func2 = 0xffffffee keycode_PageDown = 0xFFFFFFF5
keycode_Func3 = 0xffffffed keycode_Home = 0xFFFFFFF4
keycode_Func4 = 0xffffffec keycode_End = 0xFFFFFFF3
keycode_Func5 = 0xffffffeb keycode_Func1 = 0xFFFFFFEF
keycode_Func6 = 0xffffffea keycode_Func2 = 0xFFFFFFEE
keycode_Func7 = 0xffffffe9 keycode_Func3 = 0xFFFFFFED
keycode_Func8 = 0xffffffe8 keycode_Func4 = 0xFFFFFFEC
keycode_Func9 = 0xffffffe7 keycode_Func5 = 0xFFFFFFEB
keycode_Func10 = 0xffffffe6 keycode_Func6 = 0xFFFFFFEA
keycode_Func11 = 0xffffffe5 keycode_Func7 = 0xFFFFFFE9
keycode_Func12 = 0xffffffe4 keycode_Func8 = 0xFFFFFFE8
keycode_Func9 = 0xFFFFFFE7
keycode_Func10 = 0xFFFFFFE6
keycode_Func11 = 0xFFFFFFE5
keycode_Func12 = 0xFFFFFFE4
keycode_MAXVAL = 28 keycode_MAXVAL = 28
style_Normal = 0 style_Normal = 0
@ -126,9 +125,10 @@ style_User1 = 9
style_User2 = 10 style_User2 = 10
style_NUMSTYLES = 11 style_NUMSTYLES = 11
class stream_result_t(ctypes.Structure): class stream_result_t(ctypes.Structure):
_fields_ = [("readcount", glui32), _fields_ = [("readcount", glui32), ("writecount", glui32)]
("writecount", glui32)]
wintype_AllTypes = 0 wintype_AllTypes = 0
wintype_Pair = 1 wintype_Pair = 1
@ -141,17 +141,17 @@ winmethod_Left = 0x00
winmethod_Right = 0x01 winmethod_Right = 0x01
winmethod_Above = 0x02 winmethod_Above = 0x02
winmethod_Below = 0x03 winmethod_Below = 0x03
winmethod_DirMask = 0x0f winmethod_DirMask = 0x0F
winmethod_Fixed = 0x10 winmethod_Fixed = 0x10
winmethod_Proportional = 0x20 winmethod_Proportional = 0x20
winmethod_DivisionMask = 0xf0 winmethod_DivisionMask = 0xF0
fileusage_Data = 0x00 fileusage_Data = 0x00
fileusage_SavedGame = 0x01 fileusage_SavedGame = 0x01
fileusage_Transcript = 0x02 fileusage_Transcript = 0x02
fileusage_InputRecord = 0x03 fileusage_InputRecord = 0x03
fileusage_TypeMask = 0x0f fileusage_TypeMask = 0x0F
fileusage_TextMode = 0x100 fileusage_TextMode = 0x100
fileusage_BinaryMode = 0x000 fileusage_BinaryMode = 0x000
@ -190,17 +190,26 @@ CORE_GLK_LIB_API = [
(None, "glk_exit", ()), (None, "glk_exit", ()),
(None, "glk_tick", ()), (None, "glk_tick", ()),
(glui32, "glk_gestalt", (glui32, glui32)), (glui32, "glk_gestalt", (glui32, glui32)),
(glui32, "glk_gestalt_ext", (glui32, glui32, ctypes.POINTER(glui32), (glui32, "glk_gestalt_ext", (glui32, glui32, ctypes.POINTER(glui32), glui32)),
glui32)),
(winid_t, "glk_window_get_root", ()), (winid_t, "glk_window_get_root", ()),
(winid_t, "glk_window_open", (winid_t, glui32, glui32, glui32, glui32)), (winid_t, "glk_window_open", (winid_t, glui32, glui32, glui32, glui32)),
(None, "glk_window_close", (winid_t, ctypes.POINTER(stream_result_t))), (None, "glk_window_close", (winid_t, ctypes.POINTER(stream_result_t))),
(None, "glk_window_get_size", (winid_t, ctypes.POINTER(glui32), (
ctypes.POINTER(glui32)) ), None,
"glk_window_get_size",
(winid_t, ctypes.POINTER(glui32), ctypes.POINTER(glui32)),
),
(None, "glk_window_set_arrangement", (winid_t, glui32, glui32, winid_t)), (None, "glk_window_set_arrangement", (winid_t, glui32, glui32, winid_t)),
(None, "glk_window_get_arrangement", (winid_t, ctypes.POINTER(glui32), (
None,
"glk_window_get_arrangement",
(
winid_t,
ctypes.POINTER(glui32), ctypes.POINTER(glui32),
ctypes.POINTER(winid_t))), ctypes.POINTER(glui32),
ctypes.POINTER(winid_t),
),
),
(winid_t, "glk_window_iterate", (winid_t, ctypes.POINTER(glui32))), (winid_t, "glk_window_iterate", (winid_t, ctypes.POINTER(glui32))),
(glui32, "glk_window_get_rock", (winid_t,)), (glui32, "glk_window_get_rock", (winid_t,)),
(glui32, "glk_window_get_type", (winid_t,)), (glui32, "glk_window_get_type", (winid_t,)),
@ -213,8 +222,7 @@ CORE_GLK_LIB_API = [
(strid_t, "glk_window_get_echo_stream", (winid_t,)), (strid_t, "glk_window_get_echo_stream", (winid_t,)),
(None, "glk_set_window", (winid_t,)), (None, "glk_set_window", (winid_t,)),
(strid_t, "glk_stream_open_file", (frefid_t, glui32, glui32)), (strid_t, "glk_stream_open_file", (frefid_t, glui32, glui32)),
(strid_t, "glk_stream_open_memory", (ctypes.c_char_p, (strid_t, "glk_stream_open_memory", (ctypes.c_char_p, glui32, glui32, glui32)),
glui32, glui32, glui32)),
(None, "glk_stream_close", (strid_t, ctypes.POINTER(stream_result_t))), (None, "glk_stream_close", (strid_t, ctypes.POINTER(stream_result_t))),
(strid_t, "glk_stream_iterate", (strid_t, ctypes.POINTER(glui32))), (strid_t, "glk_stream_iterate", (strid_t, ctypes.POINTER(glui32))),
(glui32, "glk_stream_get_rock", (strid_t,)), (glui32, "glk_stream_get_rock", (strid_t,)),
@ -236,14 +244,11 @@ CORE_GLK_LIB_API = [
(None, "glk_stylehint_set", (glui32, glui32, glui32, glsi32)), (None, "glk_stylehint_set", (glui32, glui32, glui32, glsi32)),
(None, "glk_stylehint_clear", (glui32, glui32, glui32)), (None, "glk_stylehint_clear", (glui32, glui32, glui32)),
(glui32, "glk_style_distinguish", (winid_t, glui32, glui32)), (glui32, "glk_style_distinguish", (winid_t, glui32, glui32)),
(glui32, "glk_style_measure", (winid_t, glui32, glui32, (glui32, "glk_style_measure", (winid_t, glui32, glui32, ctypes.POINTER(glui32))),
ctypes.POINTER(glui32))),
(frefid_t, "glk_fileref_create_temp", (glui32, glui32)), (frefid_t, "glk_fileref_create_temp", (glui32, glui32)),
(frefid_t, "glk_fileref_create_by_name", (glui32, ctypes.c_char_p, (frefid_t, "glk_fileref_create_by_name", (glui32, ctypes.c_char_p, glui32)),
glui32)),
(frefid_t, "glk_fileref_create_by_prompt", (glui32, glui32, glui32)), (frefid_t, "glk_fileref_create_by_prompt", (glui32, glui32, glui32)),
(frefid_t, "glk_fileref_create_from_fileref", (glui32, frefid_t, (frefid_t, "glk_fileref_create_from_fileref", (glui32, frefid_t, glui32)),
glui32)),
(None, "glk_fileref_destroy", (frefid_t,)), (None, "glk_fileref_destroy", (frefid_t,)),
(frefid_t, "glk_fileref_iterate", (frefid_t, ctypes.POINTER(glui32))), (frefid_t, "glk_fileref_iterate", (frefid_t, ctypes.POINTER(glui32))),
(glui32, "glk_fileref_get_rock", (frefid_t,)), (glui32, "glk_fileref_get_rock", (frefid_t,)),
@ -252,8 +257,7 @@ CORE_GLK_LIB_API = [
(None, "glk_select", (ctypes.POINTER(event_t),)), (None, "glk_select", (ctypes.POINTER(event_t),)),
(None, "glk_select_poll", (ctypes.POINTER(event_t),)), (None, "glk_select_poll", (ctypes.POINTER(event_t),)),
(None, "glk_request_timer_events", (glui32,)), (None, "glk_request_timer_events", (glui32,)),
(None, "glk_request_line_event", (winid_t, ctypes.c_char_p, glui32, (None, "glk_request_line_event", (winid_t, ctypes.c_char_p, glui32, glui32)),
glui32)),
(None, "glk_request_char_event", (winid_t,)), (None, "glk_request_char_event", (winid_t,)),
(None, "glk_request_mouse_event", (winid_t,)), (None, "glk_request_mouse_event", (winid_t,)),
(None, "glk_cancel_line_event", (winid_t, ctypes.POINTER(event_t))), (None, "glk_cancel_line_event", (winid_t, ctypes.POINTER(event_t))),
@ -269,21 +273,25 @@ UNICODE_GLK_LIB_API = [
(None, "glk_put_buffer_uni", (ctypes.POINTER(glui32), glui32)), (None, "glk_put_buffer_uni", (ctypes.POINTER(glui32), glui32)),
(None, "glk_put_char_stream_uni", (strid_t, glui32)), (None, "glk_put_char_stream_uni", (strid_t, glui32)),
(None, "glk_put_string_stream_uni", (strid_t, ctypes.POINTER(glui32))), (None, "glk_put_string_stream_uni", (strid_t, ctypes.POINTER(glui32))),
(None, "glk_put_buffer_stream_uni", (strid_t, ctypes.POINTER(glui32), (None, "glk_put_buffer_stream_uni", (strid_t, ctypes.POINTER(glui32), glui32)),
glui32)),
(glsi32, "glk_get_char_stream_uni", (strid_t,)), (glsi32, "glk_get_char_stream_uni", (strid_t,)),
(glui32, "glk_get_buffer_stream_uni", (strid_t, ctypes.POINTER(glui32), (glui32, "glk_get_buffer_stream_uni", (strid_t, ctypes.POINTER(glui32), glui32)),
glui32)), (glui32, "glk_get_line_stream_uni", (strid_t, ctypes.POINTER(glui32), glui32)),
(glui32, "glk_get_line_stream_uni", (strid_t, ctypes.POINTER(glui32),
glui32)),
(strid_t, "glk_stream_open_file_uni", (frefid_t, glui32, glui32)), (strid_t, "glk_stream_open_file_uni", (frefid_t, glui32, glui32)),
(strid_t, "glk_stream_open_memory_uni", (ctypes.POINTER(glui32), (
glui32, glui32, glui32)), strid_t,
"glk_stream_open_memory_uni",
(ctypes.POINTER(glui32), glui32, glui32, glui32),
),
(None, "glk_request_char_event_uni", (winid_t,)), (None, "glk_request_char_event_uni", (winid_t,)),
(None, "glk_request_line_event_uni", (winid_t, ctypes.POINTER(glui32), (
glui32, glui32)) None,
"glk_request_line_event_uni",
(winid_t, ctypes.POINTER(glui32), glui32, glui32),
),
] ]
class GlkLib: class GlkLib:
"""Encapsulates the ctypes interface to a Glk shared library. When """Encapsulates the ctypes interface to a Glk shared library. When
instantiated, it wraps the shared library with the appropriate instantiated, it wraps the shared library with the appropriate
@ -299,7 +307,7 @@ class GlkLib:
self.__bind_prototypes(CORE_GLK_LIB_API) self.__bind_prototypes(CORE_GLK_LIB_API)
if self.glk_gestalt(gestalt_Unicode, 0) == 1: if self.glk_gestalt(gestalt_Unicode, 0) == 1: # type: ignore[unresolved-attribute]
self.__bind_prototypes(UNICODE_GLK_LIB_API) self.__bind_prototypes(UNICODE_GLK_LIB_API)
else: else:
self.__bind_not_implemented_prototypes(UNICODE_GLK_LIB_API) self.__bind_not_implemented_prototypes(UNICODE_GLK_LIB_API)
@ -324,8 +332,7 @@ class GlkLib:
support some optional extension of the Glk API.""" support some optional extension of the Glk API."""
def notImplementedFunction(*args, **kwargs): def notImplementedFunction(*args, **kwargs):
raise NotImplementedError( "Function not implemented " \ raise NotImplementedError("Function not implemented by this Glk library.")
"by this Glk library." )
for function_prototype in function_prototypes: for function_prototype in function_prototypes:
_, function_name, _ = function_prototype _, function_name, _ = function_prototype

View file

@ -29,31 +29,41 @@ from .zlogging import log
# 4-byte chunkname, 4-byte length, length bytes of data # 4-byte chunkname, 4-byte length, length bytes of data
# ... # ...
class QuetzalError(Exception): class QuetzalError(Exception):
"General exception for Quetzal classes." "General exception for Quetzal classes."
pass pass
class QuetzalMalformedChunk(QuetzalError): class QuetzalMalformedChunk(QuetzalError):
"Malformed chunk detected." "Malformed chunk detected."
class QuetzalNoSuchSavefile(QuetzalError): class QuetzalNoSuchSavefile(QuetzalError):
"Cannot locate save-game file." "Cannot locate save-game file."
class QuetzalUnrecognizedFileFormat(QuetzalError): class QuetzalUnrecognizedFileFormat(QuetzalError):
"Not a valid Quetzal file." "Not a valid Quetzal file."
class QuetzalIllegalChunkOrder(QuetzalError): class QuetzalIllegalChunkOrder(QuetzalError):
"IFhd chunk came after Umem/Cmem/Stks chunks (see section 5.4)." "IFhd chunk came after Umem/Cmem/Stks chunks (see section 5.4)."
class QuetzalMismatchedFile(QuetzalError): class QuetzalMismatchedFile(QuetzalError):
"Quetzal file dosen't match current game." "Quetzal file dosen't match current game."
class QuetzalMemoryOutOfBounds(QuetzalError): class QuetzalMemoryOutOfBounds(QuetzalError):
"Decompressed dynamic memory has gone out of bounds." "Decompressed dynamic memory has gone out of bounds."
class QuetzalMemoryMismatch(QuetzalError): class QuetzalMemoryMismatch(QuetzalError):
"Savefile's dynamic memory image is incorrectly sized." "Savefile's dynamic memory image is incorrectly sized."
class QuetzalStackFrameOverflow(QuetzalError): class QuetzalStackFrameOverflow(QuetzalError):
"Stack frame parsing went beyond bounds of 'Stks' chunk." "Stack frame parsing went beyond bounds of 'Stks' chunk."
@ -67,7 +77,6 @@ class QuetzalParser:
self._seen_mem_or_stks = False self._seen_mem_or_stks = False
self._last_loaded_metadata = {} # metadata for tests & debugging self._last_loaded_metadata = {} # metadata for tests & debugging
def _parse_ifhd(self, data): def _parse_ifhd(self, data):
"""Parse a chunk of type IFhd, and check that the quetzal file """Parse a chunk of type IFhd, and check that the quetzal file
really belongs to the current story (by comparing release number, really belongs to the current story (by comparing release number,
@ -87,10 +96,10 @@ class QuetzalParser:
chunk_pc = (data[10] << 16) + (data[11] << 8) + data[12] chunk_pc = (data[10] << 16) + (data[11] << 8) + data[12]
self._zmachine._opdecoder.program_counter = chunk_pc self._zmachine._opdecoder.program_counter = chunk_pc
log(" Found release number %d" % chunk_release) log(f" Found release number {chunk_release}")
log(" Found serial number %d" % int(chunk_serial)) log(f" Found serial number {int(chunk_serial)}")
log(" Found checksum %d" % chunk_checksum) log(f" Found checksum {chunk_checksum}")
log(" Initial program counter value is %d" % chunk_pc) log(f" Initial program counter value is {chunk_pc}")
self._last_loaded_metadata["release number"] = chunk_release self._last_loaded_metadata["release number"] = chunk_release
self._last_loaded_metadata["serial number"] = chunk_serial self._last_loaded_metadata["serial number"] = chunk_serial
self._last_loaded_metadata["checksum"] = chunk_checksum self._last_loaded_metadata["checksum"] = chunk_checksum
@ -108,7 +117,6 @@ class QuetzalParser:
raise QuetzalMismatchedFile raise QuetzalMismatchedFile
log(" Quetzal file correctly verifies against original story.") log(" Quetzal file correctly verifies against original story.")
def _parse_cmem(self, data): def _parse_cmem(self, data):
"""Parse a chunk of type Cmem. Decompress an image of dynamic """Parse a chunk of type Cmem. Decompress an image of dynamic
memory, and place it into the ZMachine.""" memory, and place it into the ZMachine."""
@ -123,7 +131,7 @@ class QuetzalParser:
savegame_mem = list(pmem[pmem._dynamic_start : (pmem._dynamic_end + 1)]) savegame_mem = list(pmem[pmem._dynamic_start : (pmem._dynamic_end + 1)])
memlen = len(savegame_mem) memlen = len(savegame_mem)
memcounter = 0 memcounter = 0
log(" Dynamic memory length is %d" % memlen) log(f" Dynamic memory length is {memlen}")
self._last_loaded_metadata["memory length"] = memlen self._last_loaded_metadata["memory length"] = memlen
runlength_bytes = data runlength_bytes = data
@ -137,13 +145,13 @@ class QuetzalParser:
savegame_mem[memcounter] = byte ^ pmem[memcounter] savegame_mem[memcounter] = byte ^ pmem[memcounter]
memcounter += 1 memcounter += 1
bytecounter += 1 bytecounter += 1
log(" Set byte %d:%d" % (memcounter, savegame_mem[memcounter])) log(f" Set byte {memcounter}:{savegame_mem[memcounter]}")
else: else:
bytecounter += 1 bytecounter += 1
num_extra_zeros = runlength_bytes[bytecounter] num_extra_zeros = runlength_bytes[bytecounter]
memcounter += (1 + num_extra_zeros) memcounter += 1 + num_extra_zeros
bytecounter += 1 bytecounter += 1
log(" Skipped %d unchanged bytes" % (1 + num_extra_zeros)) log(f" Skipped {1 + num_extra_zeros} unchanged bytes")
if memcounter >= memlen: if memcounter >= memlen:
raise QuetzalMemoryOutOfBounds raise QuetzalMemoryOutOfBounds
@ -153,7 +161,6 @@ class QuetzalParser:
cmem[cmem._dynamic_start : (cmem._dynamic_end + 1)] = savegame_mem cmem[cmem._dynamic_start : (cmem._dynamic_end + 1)] = savegame_mem
log(" Successfully installed new dynamic memory.") log(" Successfully installed new dynamic memory.")
def _parse_umem(self, data): def _parse_umem(self, data):
"""Parse a chunk of type Umem. Suck a raw image of dynamic memory """Parse a chunk of type Umem. Suck a raw image of dynamic memory
and place it into the ZMachine.""" and place it into the ZMachine."""
@ -166,7 +173,7 @@ class QuetzalParser:
cmem = self._zmachine._mem cmem = self._zmachine._mem
dynamic_len = (cmem._dynamic_end - cmem.dynamic_start) + 1 dynamic_len = (cmem._dynamic_end - cmem.dynamic_start) + 1
log(" Dynamic memory length is %d" % dynamic_len) log(f" Dynamic memory length is {dynamic_len}")
self._last_loaded_metadata["dynamic memory length"] = dynamic_len self._last_loaded_metadata["dynamic memory length"] = dynamic_len
savegame_mem = [ord(x) for x in data] savegame_mem = [ord(x) for x in data]
@ -176,7 +183,6 @@ class QuetzalParser:
cmem[cmem._dynamic_start : (cmem._dynamic_end + 1)] = savegame_mem cmem[cmem._dynamic_start : (cmem._dynamic_end + 1)] = savegame_mem
log(" Successfully installed new dynamic memory.") log(" Successfully installed new dynamic memory.")
def _parse_stks(self, data): def _parse_stks(self, data):
"""Parse a chunk of type Stks.""" """Parse a chunk of type Stks."""
@ -195,34 +201,34 @@ class QuetzalParser:
ptr = 0 ptr = 0
# Read successive stack frames: # Read successive stack frames:
while (ptr < total_len): while ptr < total_len:
log(" Parsing stack frame...") log(" Parsing stack frame...")
return_pc = (bytes[ptr] << 16) + (bytes[ptr + 1] << 8) + bytes[ptr + 3] return_pc = (bytes[ptr] << 16) + (bytes[ptr + 1] << 8) + bytes[ptr + 3]
ptr += 3 ptr += 3
flags_bitfield = bitfield.BitField(bytes[ptr]) flags_bitfield = bitfield.BitField(bytes[ptr])
ptr += 1 ptr += 1
varnum = bytes[ptr] ### TODO: tells us which variable gets the result _varnum = bytes[ptr] ### TODO: tells us which variable gets the result
ptr += 1 ptr += 1
argflag = bytes[ptr] _argflag = bytes[ptr]
ptr += 1 ptr += 1
evalstack_size = (bytes[ptr] << 8) + bytes[ptr + 1] evalstack_size = (bytes[ptr] << 8) + bytes[ptr + 1]
ptr += 2 ptr += 2
# read anywhere from 0 to 15 local vars # read anywhere from 0 to 15 local vars
local_vars = [] local_vars = []
for i in range(flags_bitfield[0:3]): for _i in range(flags_bitfield[0:3]):
var = (bytes[ptr] << 8) + bytes[ptr + 1] var = (bytes[ptr] << 8) + bytes[ptr + 1]
ptr += 2 ptr += 2
local_vars.append(var) local_vars.append(var)
log(" Found %d local vars" % len(local_vars)) log(f" Found {len(local_vars)} local vars")
# least recent to most recent stack values: # least recent to most recent stack values:
stack_values = [] stack_values = []
for i in range(evalstack_size): for _i in range(evalstack_size):
val = (bytes[ptr] << 8) + bytes[ptr + 1] val = (bytes[ptr] << 8) + bytes[ptr + 1]
ptr += 2 ptr += 2
stack_values.append(val) stack_values.append(val)
log(" Found %d local stack values" % len(stack_values)) log(f" Found {len(stack_values)} local stack values")
### Interesting... the reconstructed stack frames have no 'start ### Interesting... the reconstructed stack frames have no 'start
### address'. I guess it doesn't matter, since we only need to ### address'. I guess it doesn't matter, since we only need to
@ -232,33 +238,32 @@ class QuetzalParser:
### TODO: I can exactly which of the 7 args is "supplied", but I ### TODO: I can exactly which of the 7 args is "supplied", but I
### don't understand where the args *are*?? ### don't understand where the args *are*??
routine = zstackmanager.ZRoutine(0, return_pc, self._zmachine._mem, routine = zstackmanager.ZRoutine(
[], local_vars, stack_values) 0, return_pc, self._zmachine._mem, [], local_vars, stack_values
)
stackmanager.push_routine(routine) stackmanager.push_routine(routine)
log(" Added new frame to stack.") log(" Added new frame to stack.")
if (ptr > total_len): if ptr > total_len:
raise QuetzalStackFrameOverflow raise QuetzalStackFrameOverflow
self._zmachine._stackmanager = stackmanager self._zmachine._stackmanager = stackmanager
log(" Successfully installed all stack frames.") log(" Successfully installed all stack frames.")
def _parse_intd(self, data): def _parse_intd(self, data):
"""Parse a chunk of type IntD, which is interpreter-dependent info.""" """Parse a chunk of type IntD, which is interpreter-dependent info."""
log(" Begin parsing of interpreter-dependent metadata") log(" Begin parsing of interpreter-dependent metadata")
bytes = [ord(x) for x in data] bytes = [ord(x) for x in data]
os_id = bytes[0:3] _os_id = bytes[0:3]
flags = bytes[4] _flags = bytes[4]
contents_id = bytes[5] _contents_id = bytes[5]
reserved = bytes[6:8] _reserved = bytes[6:8]
interpreter_id = bytes[8:12] _interpreter_id = bytes[8:12]
private_data = bytes[12:] _private_data = bytes[12:]
### TODO: finish this ### TODO: finish this
# The following 3 chunks are totally optional metadata, and are # The following 3 chunks are totally optional metadata, and are
# artifacts of the larger IFF standard. We're not required to do # artifacts of the larger IFF standard. We're not required to do
# anything when we see them, though maybe it would be nice to print # anything when we see them, though maybe it would be nice to print
@ -267,25 +272,23 @@ class QuetzalParser:
def _parse_auth(self, data): def _parse_auth(self, data):
"""Parse a chunk of type AUTH. Display the author.""" """Parse a chunk of type AUTH. Display the author."""
log("Author of file: %s" % data) log(f"Author of file: {data}")
self._last_loaded_metadata["author"] = data self._last_loaded_metadata["author"] = data
def _parse_copyright(self, data): def _parse_copyright(self, data):
"""Parse a chunk of type (c) . Display the copyright.""" """Parse a chunk of type (c) . Display the copyright."""
log("Copyright: (C) %s" % data) log(f"Copyright: (C) {data}")
self._last_loaded_metadata["copyright"] = data self._last_loaded_metadata["copyright"] = data
def _parse_anno(self, data): def _parse_anno(self, data):
"""Parse a chunk of type ANNO. Display any annotation""" """Parse a chunk of type ANNO. Display any annotation"""
log("Annotation: %s" % data) log(f"Annotation: {data}")
self._last_loaded_metadata["annotation"] = data self._last_loaded_metadata["annotation"] = data
# --------- Public APIs ----------- # --------- Public APIs -----------
def get_last_loaded(self): def get_last_loaded(self):
"""Return a list of metadata about the last loaded Quetzal file, for """Return a list of metadata about the last loaded Quetzal file, for
debugging and test verification.""" debugging and test verification."""
@ -300,8 +303,8 @@ class QuetzalParser:
if not os.path.isfile(savefile_path): if not os.path.isfile(savefile_path):
raise QuetzalNoSuchSavefile raise QuetzalNoSuchSavefile
log("Attempting to load saved game from '%s'" % savefile_path) log(f"Attempting to load saved game from '{savefile_path}'")
self._file = open(savefile_path, 'rb') self._file = open(savefile_path, "rb") # noqa: SIM115
# The python 'chunk' module is pretty dumb; it doesn't understand # The python 'chunk' module is pretty dumb; it doesn't understand
# the FORM chunk and the way it contains nested chunks. # the FORM chunk and the way it contains nested chunks.
@ -316,7 +319,7 @@ class QuetzalParser:
self._len += bytestring[1] << 16 self._len += bytestring[1] << 16
self._len += bytestring[2] << 8 self._len += bytestring[2] << 8
self._len += bytestring[3] self._len += bytestring[3]
log("Total length of FORM data is %d" % self._len) log(f"Total length of FORM data is {self._len}")
self._last_loaded_metadata["total length"] = self._len self._last_loaded_metadata["total length"] = self._len
type = self._file.read(4) type = self._file.read(4)
@ -329,7 +332,7 @@ class QuetzalParser:
chunkname = c.getname() chunkname = c.getname()
chunksize = c.getsize() chunksize = c.getsize()
data = c.read(chunksize) data = c.read(chunksize)
log("** Found chunk ID %s: length %d" % (chunkname, chunksize)) log(f"** Found chunk ID {chunkname}: length {chunksize}")
self._last_loaded_metadata[chunkname] = chunksize self._last_loaded_metadata[chunkname] = chunksize
if chunkname == b"IFhd": if chunkname == b"IFhd":
@ -359,7 +362,6 @@ class QuetzalParser:
log("Finished parsing Quetzal file.") log("Finished parsing Quetzal file.")
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -385,7 +387,6 @@ class QuetzalWriter:
return "0" return "0"
def _generate_cmem_chunk(self): def _generate_cmem_chunk(self):
"""Return a compressed chunk of data representing the compressed """Return a compressed chunk of data representing the compressed
image of the zmachine's main memory.""" image of the zmachine's main memory."""
@ -396,9 +397,10 @@ class QuetzalWriter:
# XOR the original game image with the current one # XOR the original game image with the current one
diffarray = list(self._zmachine._pristine_mem) diffarray = list(self._zmachine._pristine_mem)
for index in range(len(self._zmachine._pristine_mem._total_size)): for index in range(len(self._zmachine._pristine_mem._total_size)):
diffarray[index] = self._zmachine._pristine_mem[index] \ diffarray[index] = (
^ self._zmachine._mem[index] self._zmachine._pristine_mem[index] ^ self._zmachine._mem[index]
log("XOR array is %s" % diffarray) )
log(f"XOR array is {diffarray}")
# Run-length encode the resulting list of 0's and 1's. # Run-length encode the resulting list of 0's and 1's.
result = [] result = []
@ -415,7 +417,6 @@ class QuetzalWriter:
result.append(diffarray[index]) result.append(diffarray[index])
return result return result
def _generate_stks_chunk(self): def _generate_stks_chunk(self):
"""Return a stacks chunk, describing the stack state of the """Return a stacks chunk, describing the stack state of the
zmachine at this moment.""" zmachine at this moment."""
@ -423,7 +424,6 @@ class QuetzalWriter:
### TODO: write this ### TODO: write this
return "0" return "0"
def _generate_anno_chunk(self): def _generate_anno_chunk(self):
"""Return an annotation chunk, containing metadata about the ZVM """Return an annotation chunk, containing metadata about the ZVM
interpreter which created the savefile.""" interpreter which created the savefile."""
@ -431,24 +431,23 @@ class QuetzalWriter:
### TODO: write this ### TODO: write this
return "0" return "0"
# --------- Public APIs ----------- # --------- Public APIs -----------
def write(self, savefile_path): def write(self, savefile_path):
"""Write the current zmachine state to a new Quetzal-file at """Write the current zmachine state to a new Quetzal-file at
SAVEFILE_PATH.""" SAVEFILE_PATH."""
log("Attempting to write game-state to '%s'" % savefile_path) log(f"Attempting to write game-state to '{savefile_path}'")
self._file = open(savefile_path, 'w') self._file = open(savefile_path, "w") # noqa: SIM115
ifhd_chunk = self._generate_ifhd_chunk() ifhd_chunk = self._generate_ifhd_chunk()
cmem_chunk = self._generate_cmem_chunk() cmem_chunk = self._generate_cmem_chunk()
stks_chunk = self._generate_stks_chunk() stks_chunk = self._generate_stks_chunk()
anno_chunk = self._generate_anno_chunk() anno_chunk = self._generate_anno_chunk()
total_chunk_size = len(ifhd_chunk) + len(cmem_chunk) \ _total_chunk_size = (
+ len(stks_chunk) + len(anno_chunk) len(ifhd_chunk) + len(cmem_chunk) + len(stks_chunk) + len(anno_chunk)
)
# Write main FORM chunk to hold other chunks # Write main FORM chunk to hold other chunks
self._file.write("FORM") self._file.write("FORM")
@ -456,8 +455,8 @@ class QuetzalWriter:
self._file.write("IFZS") self._file.write("IFZS")
# Write nested chunks. # Write nested chunks.
for chunk in (ifhd_chunk, cmem_chunk, stks_chunk, anno_chunk): for chunk_data in (ifhd_chunk, cmem_chunk, stks_chunk, anno_chunk):
self._file.write(chunk) self._file.write(chunk_data)
log("Wrote a chunk.") log("Wrote a chunk.")
self._file.close() self._file.close()
log("Done writing game-state to savefile.") log("Done writing game-state to savefile.")

View file

@ -34,7 +34,8 @@ class TrivialAudio(zaudio.ZAudio):
elif bleep_type == zaudio.BLEEP_LOW: elif bleep_type == zaudio.BLEEP_LOW:
sys.stdout.write("AUDIO: low-pitched bleep\n") sys.stdout.write("AUDIO: low-pitched bleep\n")
else: else:
raise AssertionError("Invalid bleep_type: %s" % str(bleep_type)) raise AssertionError(f"Invalid bleep_type: {str(bleep_type)}")
class TrivialScreen(zscreen.ZScreen): class TrivialScreen(zscreen.ZScreen):
def __init__(self): def __init__(self):
@ -49,17 +50,16 @@ class TrivialScreen(zscreen.ZScreen):
self.__rows_since_last_input = 0 self.__rows_since_last_input = 0
def split_window(self, height): def split_window(self, height):
log("TODO: split window here to height %d" % height) log(f"TODO: split window here to height {height}")
def select_window(self, window_num): def select_window(self, window):
log("TODO: select window %d here" % window_num) log(f"TODO: select window {window} here")
def set_cursor_position(self, x, y): def set_cursor_position(self, x, y):
log("TODO: set cursor position to (%d,%d) here" % (x,y)) log(f"TODO: set cursor position to ({x},{y}) here")
def erase_window(self, window=zscreen.WINDOW_LOWER, def erase_window(self, window=zscreen.WINDOW_LOWER, color=zscreen.COLOR_CURRENT):
color=zscreen.COLOR_CURRENT): for _row in range(self._rows):
for row in range(self._rows):
sys.stdout.write("\n") sys.stdout.write("\n")
self.__curr_column = 0 self.__curr_column = 0
self.__rows_since_last_input = 0 self.__rows_since_last_input = 0
@ -89,14 +89,13 @@ class TrivialScreen(zscreen.ZScreen):
then erase the [MORE] prompt, leaving the cursor at the same then erase the [MORE] prompt, leaving the cursor at the same
position that it was at before the call was made.""" position that it was at before the call was made."""
assert self.__curr_column == 0, \ assert self.__curr_column == 0, "Precondition: current column must be zero."
"Precondition: current column must be zero."
MORE_STRING = "[MORE]" MORE_STRING = "[MORE]"
sys.stdout.write(MORE_STRING) sys.stdout.write(MORE_STRING)
_read_char() _read_char()
# Erase the [MORE] prompt and reset the cursor position. # Erase the [MORE] prompt and reset the cursor position.
sys.stdout.write("\r%s\r" % (" " * len(MORE_STRING))) sys.stdout.write(f"\r{' ' * len(MORE_STRING)}\r")
self.__rows_since_last_input = 0 self.__rows_since_last_input = 0
def on_input_occurred(self, newline_occurred=False): def on_input_occurred(self, newline_occurred=False):
@ -129,8 +128,10 @@ class TrivialScreen(zscreen.ZScreen):
if newline_printed: if newline_printed:
self.__rows_since_last_input += 1 self.__rows_since_last_input += 1
self.__curr_column = 0 self.__curr_column = 0
if (self.__rows_since_last_input == self._rows and if (
self._rows != zscreen.INFINITE_ROWS): self.__rows_since_last_input == self._rows
and self._rows != zscreen.INFINITE_ROWS
):
self.__show_more_prompt() self.__show_more_prompt()
def write(self, string): def write(self, string):
@ -154,6 +155,7 @@ class TrivialScreen(zscreen.ZScreen):
self.__unbuffered_write(string) self.__unbuffered_write(string)
class TrivialKeyboardInputStream(zstream.ZInputStream): class TrivialKeyboardInputStream(zstream.ZInputStream):
def __init__(self, screen): def __init__(self, screen):
zstream.ZInputStream.__init__(self) zstream.ZInputStream.__init__(self)
@ -162,9 +164,14 @@ class TrivialKeyboardInputStream(zstream.ZInputStream):
"has_timed_input": False, "has_timed_input": False,
} }
def read_line(self, original_text=None, max_length=0, def read_line(
self,
original_text=None,
max_length=0,
terminating_characters=None, terminating_characters=None,
timed_input_routine=None, timed_input_interval=0): timed_input_routine=None,
timed_input_interval=0,
):
result = _read_line(original_text, terminating_characters) result = _read_line(original_text, terminating_characters)
if max_length > 0: if max_length > 0:
result = result[:max_length] result = result[:max_length]
@ -176,27 +183,25 @@ class TrivialKeyboardInputStream(zstream.ZInputStream):
return str(result) return str(result)
def read_char(self, timed_input_routine=None, def read_char(self, timed_input_routine=None, timed_input_interval=0):
timed_input_interval=0):
result = _read_char() result = _read_char()
self.__screen.on_input_occurred() self.__screen.on_input_occurred()
return ord(result) return ord(result)
class TrivialFilesystem(zfilesystem.ZFilesystem): class TrivialFilesystem(zfilesystem.ZFilesystem):
def __report_io_error(self, exception): def __report_io_error(self, exception):
sys.stdout.write("FILESYSTEM: An error occurred: %s\n" % exception) sys.stdout.write(f"FILESYSTEM: An error occurred: {exception}\n")
def save_game(self, data, suggested_filename=None): def save_game(self, data, suggested_filename=None):
success = False success = False
sys.stdout.write("Enter a name for the saved game " \ sys.stdout.write("Enter a name for the saved game (hit enter to cancel): ")
"(hit enter to cancel): ")
filename = _read_line(suggested_filename) filename = _read_line(suggested_filename)
if filename: if filename:
try: try:
file_obj = open(filename, "wb") with open(filename, "wb") as file_obj:
file_obj.write(data) file_obj.write(data)
file_obj.close()
success = True success = True
except OSError as e: except OSError as e:
self.__report_io_error(e) self.__report_io_error(e)
@ -206,14 +211,14 @@ class TrivialFilesystem(zfilesystem.ZFilesystem):
def restore_game(self): def restore_game(self):
data = None data = None
sys.stdout.write("Enter the name of the saved game to restore " \ sys.stdout.write(
"(hit enter to cancel): ") "Enter the name of the saved game to restore (hit enter to cancel): "
)
filename = _read_line() filename = _read_line()
if filename: if filename:
try: try:
file_obj = open(filename, "rb") with open(filename, "rb") as file_obj:
data = file_obj.read() data = file_obj.read()
file_obj.close()
except OSError as e: except OSError as e:
self.__report_io_error(e) self.__report_io_error(e)
@ -222,12 +227,11 @@ class TrivialFilesystem(zfilesystem.ZFilesystem):
def open_transcript_file_for_writing(self): def open_transcript_file_for_writing(self):
file_obj = None file_obj = None
sys.stdout.write("Enter a name for the transcript file " \ sys.stdout.write("Enter a name for the transcript file (hit enter to cancel): ")
"(hit enter to cancel): ")
filename = _read_line() filename = _read_line()
if filename: if filename:
try: try:
file_obj = open(filename, "w") file_obj = open(filename, "w") # noqa: SIM115
except OSError as e: except OSError as e:
self.__report_io_error(e) self.__report_io_error(e)
@ -236,17 +240,19 @@ class TrivialFilesystem(zfilesystem.ZFilesystem):
def open_transcript_file_for_reading(self): def open_transcript_file_for_reading(self):
file_obj = None file_obj = None
sys.stdout.write("Enter the name of the transcript file to read " \ sys.stdout.write(
"(hit enter to cancel): ") "Enter the name of the transcript file to read (hit enter to cancel): "
)
filename = _read_line() filename = _read_line()
if filename: if filename:
try: try:
file_obj = open(filename) file_obj = open(filename) # noqa: SIM115
except OSError as e: except OSError as e:
self.__report_io_error(e) self.__report_io_error(e)
return file_obj return file_obj
def create_zui(): def create_zui():
"""Creates and returns a ZUI instance representing a trivial user """Creates and returns a ZUI instance representing a trivial user
interface.""" interface."""
@ -256,12 +262,8 @@ def create_zui():
keyboard_input = TrivialKeyboardInputStream(screen) keyboard_input = TrivialKeyboardInputStream(screen)
filesystem = TrivialFilesystem() filesystem = TrivialFilesystem()
return zui.ZUI( return zui.ZUI(audio, screen, keyboard_input, filesystem)
audio,
screen,
keyboard_input,
filesystem
)
# Keyboard input functions # Keyboard input functions
@ -269,13 +271,15 @@ _INTERRUPT_CHAR = chr(3)
_BACKSPACE_CHAR = chr(8) _BACKSPACE_CHAR = chr(8)
_DELETE_CHAR = chr(127) _DELETE_CHAR = chr(127)
def _win32_read_char(): def _win32_read_char():
"""Win32-specific function that reads a character of input from the """Win32-specific function that reads a character of input from the
keyboard and returns it without printing it to the screen.""" keyboard and returns it without printing it to the screen."""
import msvcrt import msvcrt
return str(msvcrt.getch()) return str(msvcrt.getch()) # type: ignore[possibly-missing-attribute]
def _unix_read_char(): def _unix_read_char():
"""Unix-specific function that reads a character of input from the """Unix-specific function that reads a character of input from the
@ -296,6 +300,7 @@ def _unix_read_char():
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return str(ch) return str(ch)
def _read_char(): def _read_char():
"""Reads a character of input from the keyboard and returns it """Reads a character of input from the keyboard and returns it
without printing it to the screen.""" without printing it to the screen."""
@ -312,6 +317,7 @@ def _read_char():
else: else:
return char return char
def _read_line(original_text=None, terminating_characters=None): def _read_line(original_text=None, terminating_characters=None):
"""Reads a line of input with the given unicode string of original """Reads a line of input with the given unicode string of original
text, which is editable, and the given unicode string of terminating text, which is editable, and the given unicode string of terminating
@ -319,7 +325,7 @@ def _read_line(original_text=None, terminating_characters=None):
terminating_characters is a string containing the carriage return terminating_characters is a string containing the carriage return
character ('\r').""" character ('\r')."""
if original_text == None: if original_text is None:
original_text = "" original_text = ""
if not terminating_characters: if not terminating_characters:
terminating_characters = "\r" terminating_characters = "\r"
@ -349,15 +355,17 @@ def _read_line(original_text=None, terminating_characters=None):
if char == "\r": if char == "\r":
char_to_print = "\n" char_to_print = "\n"
elif char == _BACKSPACE_CHAR: elif char == _BACKSPACE_CHAR:
char_to_print = "%s %s" % (_BACKSPACE_CHAR, _BACKSPACE_CHAR) char_to_print = f"{_BACKSPACE_CHAR} {_BACKSPACE_CHAR}"
else: else:
char_to_print = char char_to_print = char
sys.stdout.write(char_to_print) sys.stdout.write(char_to_print)
return string return string
# Word wrapping helper function # Word wrapping helper function
def _word_wrap(text, width): def _word_wrap(text, width):
""" """
A word-wrap function that preserves existing line breaks A word-wrap function that preserves existing line breaks
@ -368,11 +376,16 @@ def _word_wrap(text, width):
# This code was taken from: # This code was taken from:
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061
return reduce(lambda line, word, width=width: '%s%s%s' % return reduce(
(line, lambda line, word, width=width: "{}{}{}".format(
' \n'[(len(line)-line.rfind('\n')-1 line,
+ len(word.split('\n',1)[0] " \n"[
) >= width)], (
word), len(line) - line.rfind("\n") - 1 + len(word.split("\n", 1)[0])
text.split(' ') >= width
)
],
word,
),
text.split(" "),
) )

View file

@ -22,6 +22,7 @@ EFFECT_START = 2
EFFECT_STOP = 3 EFFECT_STOP = 3
EFFECT_FINISH = 4 EFFECT_FINISH = 4
class ZAudio: class ZAudio:
def __init__(self): def __init__(self):
"""Constructor of the audio system.""" """Constructor of the audio system."""
@ -42,8 +43,7 @@ class ZAudio:
raise NotImplementedError() raise NotImplementedError()
def play_sound_effect(self, id, effect, volume, repeats, def play_sound_effect(self, id, effect, volume, repeats, routine=None):
routine=None):
"""The given effect happens to the given sound number. The id """The given effect happens to the given sound number. The id
must be 3 or above is supplied by the ZAudio object for the must be 3 or above is supplied by the ZAudio object for the
particular game in question. particular game in question.

View file

@ -10,6 +10,7 @@
# root directory of this distribution. # root directory of this distribution.
# #
class ZFilesystem: class ZFilesystem:
"""Encapsulates the interactions that the end-user has with the """Encapsulates the interactions that the end-user has with the
filesystem.""" filesystem."""
@ -27,7 +28,6 @@ class ZFilesystem:
raise NotImplementedError() raise NotImplementedError()
def restore_game(self): def restore_game(self):
"""Prompt for a filename, and return file's contents. (Presumably """Prompt for a filename, and return file's contents. (Presumably
the interpreter will attempt to use those contents to restore a the interpreter will attempt to use those contents to restore a
@ -42,7 +42,6 @@ class ZFilesystem:
raise NotImplementedError() raise NotImplementedError()
def open_transcript_file_for_writing(self): def open_transcript_file_for_writing(self):
"""Prompt for a filename in which to save either a full game """Prompt for a filename in which to save either a full game
transcript or just a list of the user's commands. Return standard transcript or just a list of the user's commands. Return standard
@ -53,7 +52,6 @@ class ZFilesystem:
raise NotImplementedError() raise NotImplementedError()
def open_transcript_file_for_reading(self): def open_transcript_file_for_reading(self):
"""Prompt for a filename contain user commands, which can be used """Prompt for a filename contain user commands, which can be used
to drive the interpreter. Return standard python file object that to drive the interpreter. Return standard python file object that

View file

@ -14,6 +14,7 @@ from .zstring import ZsciiTranslator, ZStringFactory
class ZLexerError(Exception): class ZLexerError(Exception):
"General exception for ZLexer class" "General exception for ZLexer class"
# Note that the specification describes tokenisation as a process # Note that the specification describes tokenisation as a process
# whereby the user's input is divided into words, each word converted # whereby the user's input is divided into words, each word converted
# to a z-string, then searched for in the 'standard' dictionary. This # to a z-string, then searched for in the 'standard' dictionary. This
@ -26,21 +27,20 @@ class ZLexerError(Exception):
# Note that the main API here (tokenise_input()) can work with any # Note that the main API here (tokenise_input()) can work with any
# dictionary, not just the standard one. # dictionary, not just the standard one.
class ZLexer: class ZLexer:
def __init__(self, mem): def __init__(self, mem):
self._memory = mem self._memory = mem
self._stringfactory = ZStringFactory(self._memory) self._stringfactory = ZStringFactory(self._memory)
self._zsciitranslator = ZsciiTranslator(self._memory) self._zsciitranslator = ZsciiTranslator(self._memory)
# Load and parse game's 'standard' dictionary from static memory. # Load and parse game's 'standard' dictionary from static memory.
dict_addr = self._memory.read_word(0x08) dict_addr = self._memory.read_word(0x08)
self._num_entries, self._entry_length, self._separators, entries_addr = \ self._num_entries, self._entry_length, self._separators, entries_addr = (
self._parse_dict_header(dict_addr) self._parse_dict_header(dict_addr)
)
self._dict = self.get_dictionary(dict_addr) self._dict = self.get_dictionary(dict_addr)
def _parse_dict_header(self, address): def _parse_dict_header(self, address):
"""Parse the header of the dictionary at ADDRESS. Return the """Parse the header of the dictionary at ADDRESS. Return the
number of entries, the length of each entry, a list of zscii number of entries, the length of each entry, a list of zscii
@ -49,7 +49,7 @@ class ZLexer:
addr = address addr = address
num_separators = self._memory[addr] num_separators = self._memory[addr]
separators = self._memory[(addr + 1) : (addr + num_separators)] separators = self._memory[(addr + 1) : (addr + num_separators)]
addr += (1 + num_separators) addr += 1 + num_separators
entry_length = self._memory[addr] entry_length = self._memory[addr]
addr += 1 addr += 1
num_entries = self._memory.read_word(addr) num_entries = self._memory.read_word(addr)
@ -57,7 +57,6 @@ class ZLexer:
return num_entries, entry_length, separators, addr return num_entries, entry_length, separators, addr
def _tokenise_string(self, string, separators): def _tokenise_string(self, string, separators):
"""Split unicode STRING into a list of words, and return the list. """Split unicode STRING into a list of words, and return the list.
Whitespace always counts as a word separator, but so do any Whitespace always counts as a word separator, but so do any
@ -69,17 +68,12 @@ class ZLexer:
sep_string = "" sep_string = ""
for sep in separators: for sep in separators:
sep_string += sep sep_string += sep
if sep_string == "": regex = r"\w+" if sep_string == "" else rf"[{sep_string}]|\w+"
regex = r"\w+"
else:
regex = r"[%s]|\w+" % sep_string
return re.findall(regex, string) return re.findall(regex, string)
# --------- Public APIs ----------- # --------- Public APIs -----------
def get_dictionary(self, address): def get_dictionary(self, address):
"""Load a z-machine-format dictionary at ADDRESS -- which maps """Load a z-machine-format dictionary at ADDRESS -- which maps
zstrings to bytestrings -- into a python dictionary which maps zstrings to bytestrings -- into a python dictionary which maps
@ -88,17 +82,15 @@ class ZLexer:
dict = {} dict = {}
num_entries, entry_length, separators, addr = \ num_entries, entry_length, separators, addr = self._parse_dict_header(address)
self._parse_dict_header(address)
for i in range(0, num_entries): for _i in range(0, num_entries):
text_key = self._stringfactory.get(addr) text_key = self._stringfactory.get(addr)
dict[text_key] = addr dict[text_key] = addr
addr += entry_length addr += entry_length
return dict return dict
def parse_input(self, string, dict_addr=None): def parse_input(self, string, dict_addr=None):
"""Given a unicode string, parse it into words based on a dictionary. """Given a unicode string, parse it into words based on a dictionary.
@ -119,8 +111,9 @@ class ZLexer:
zseparators = self._separators zseparators = self._separators
dict = self._dict dict = self._dict
else: else:
num_entries, entry_length, zseparators, addr = \ num_entries, entry_length, zseparators, addr = self._parse_dict_header(
self._parse_dict_header(dict_addr) dict_addr
)
dict = self.get_dictionary(dict_addr) dict = self.get_dictionary(dict_addr)
# Our list of word separators are actually zscii codes that must # Our list of word separators are actually zscii codes that must
@ -133,10 +126,7 @@ class ZLexer:
final_list = [] final_list = []
for word in token_list: for word in token_list:
if word in dict: byte_addr = dict.get(word, 0)
byte_addr = dict[word]
else:
byte_addr = 0
final_list.append([word, byte_addr]) final_list.append([word, byte_addr])
return final_list return final_list

View file

@ -12,22 +12,23 @@ logging.getLogger().setLevel(logging.DEBUG)
# Create the logging objects regardless. If debugmode is False, then # Create the logging objects regardless. If debugmode is False, then
# they won't actually do anything when used. # they won't actually do anything when used.
mainlog = logging.FileHandler('debug.log', 'a') mainlog_handler = logging.FileHandler("debug.log", "a")
mainlog.setLevel(logging.DEBUG) mainlog_handler.setLevel(logging.DEBUG)
mainlog.setFormatter(logging.Formatter('%(asctime)s: %(message)s')) mainlog_handler.setFormatter(logging.Formatter("%(asctime)s: %(message)s"))
logging.getLogger('mainlog').addHandler(mainlog) logging.getLogger("mainlog").addHandler(mainlog_handler)
# We'll store the disassembly in a separate file, for better # We'll store the disassembly in a separate file, for better
# readability. # readability.
disasm = logging.FileHandler('disasm.log', 'a') disasm_handler = logging.FileHandler("disasm.log", "a")
disasm.setLevel(logging.DEBUG) disasm_handler.setLevel(logging.DEBUG)
disasm.setFormatter(logging.Formatter('%(message)s')) disasm_handler.setFormatter(logging.Formatter("%(message)s"))
logging.getLogger('disasm').addHandler(disasm) logging.getLogger("disasm").addHandler(disasm_handler)
mainlog = logging.getLogger("mainlog")
mainlog.info("*** Log reopened ***")
disasm = logging.getLogger("disasm")
disasm.info("*** Log reopened ***")
mainlog = logging.getLogger('mainlog')
mainlog.info('*** Log reopened ***')
disasm = logging.getLogger('disasm')
disasm.info('*** Log reopened ***')
# Pubilc routines used by other modules # Pubilc routines used by other modules
def set_debug(state): def set_debug(state):
@ -36,9 +37,10 @@ def set_debug(state):
else: else:
logging.getLogger().setLevel(logging.CRITICAL) logging.getLogger().setLevel(logging.CRITICAL)
def log(msg): def log(msg):
mainlog.debug(msg) mainlog.debug(msg)
def log_disasm(pc, opcode_type, opcode_num, opcode_name, args): def log_disasm(pc, opcode_type, opcode_num, opcode_name, args):
disasm.debug("%06x %s:%02x %s %s" % (pc, opcode_type, opcode_num, disasm.debug(f"{pc:06x} {opcode_type}:{opcode_num:02x} {opcode_name} {args}")
opcode_name, args))

View file

@ -18,6 +18,7 @@ from .zstring import ZStringFactory
class ZMachineError(Exception): class ZMachineError(Exception):
"""General exception for ZMachine class""" """General exception for ZMachine class"""
class ZMachine: class ZMachine:
"""The Z-Machine black box.""" """The Z-Machine black box."""
@ -32,9 +33,15 @@ class ZMachine:
self._opdecoder.program_counter = self._mem.read_word(0x06) self._opdecoder.program_counter = self._mem.read_word(0x06)
self._ui = ui self._ui = ui
self._stream_manager = ZStreamManager(self._mem, self._ui) self._stream_manager = ZStreamManager(self._mem, self._ui)
self._cpu = ZCpu(self._mem, self._opdecoder, self._stackmanager, self._cpu = ZCpu(
self._objectparser, self._stringfactory, self._mem,
self._stream_manager, self._ui) self._opdecoder,
self._stackmanager,
self._objectparser,
self._stringfactory,
self._stream_manager,
self._ui,
)
# --------- Public APIs ----------- # --------- Public APIs -----------

View file

@ -18,37 +18,48 @@ from .zlogging import log
class ZMemoryError(Exception): class ZMemoryError(Exception):
"General exception for ZMemory class" "General exception for ZMemory class"
pass pass
class ZMemoryIllegalWrite(ZMemoryError): class ZMemoryIllegalWrite(ZMemoryError):
"Tried to write to a read-only part of memory" "Tried to write to a read-only part of memory"
def __init__(self, address): def __init__(self, address):
super().__init__( super().__init__(f"Illegal write to address {address}")
"Illegal write to address %d" % address)
class ZMemoryBadInitialization(ZMemoryError): class ZMemoryBadInitialization(ZMemoryError):
"Failure to initialize ZMemory class" "Failure to initialize ZMemory class"
pass pass
class ZMemoryOutOfBounds(ZMemoryError): class ZMemoryOutOfBounds(ZMemoryError):
"Accessed an address beyond the bounds of memory." "Accessed an address beyond the bounds of memory."
pass pass
class ZMemoryBadMemoryLayout(ZMemoryError): class ZMemoryBadMemoryLayout(ZMemoryError):
"Static plus dynamic memory exceeds 64k" "Static plus dynamic memory exceeds 64k"
pass pass
class ZMemoryBadStoryfileSize(ZMemoryError): class ZMemoryBadStoryfileSize(ZMemoryError):
"Story is too large for Z-machine version." "Story is too large for Z-machine version."
pass pass
class ZMemoryUnsupportedVersion(ZMemoryError): class ZMemoryUnsupportedVersion(ZMemoryError):
"Unsupported version of Z-story file." "Unsupported version of Z-story file."
pass pass
class ZMemory: class ZMemory:
# A list of 64 tuples describing who's allowed to tweak header-bytes. # A list of 64 tuples describing who's allowed to tweak header-bytes.
# Index into the list is the header-byte being tweaked. # Index into the list is the header-byte being tweaked.
# List value is a tuple of the form # List value is a tuple of the form
@ -59,22 +70,72 @@ class ZMemory:
# enforcing authorization by *bit*, not by byte. Maybe do this # enforcing authorization by *bit*, not by byte. Maybe do this
# someday. # someday.
HEADER_PERMS = ([1,0,0], [3,0,1], None, None, HEADER_PERMS = (
[1,0,0], None, [1,0,0], None, [1, 0, 0],
[1,0,0], None, [1,0,0], None, [3, 0, 1],
[1,0,0], None, [1,0,0], None, None,
[1,1,1], [1,1,1], None, None, None,
None, None, None, None, [1, 0, 0],
[2,0,0], None, [3,0,0], None, None,
[3,0,0], None, [4,1,1], [4,1,1], [1, 0, 0],
[4,0,1], [4,0,1], [5,0,1], None, None,
[5,0,1], None, [5,0,1], [5,0,1], [1, 0, 0],
[6,0,0], None, [6,0,0], None, None,
[5,0,1], [5,0,1], [5,0,0], None, [1, 0, 0],
[6,0,1], None, [1,0,1], None, None,
[5,0,0], None, [5,0,0], None, [1, 0, 0],
None, None, None, None, None,
None, None, None, None) [1, 0, 0],
None,
[1, 1, 1],
[1, 1, 1],
None,
None,
None,
None,
None,
None,
[2, 0, 0],
None,
[3, 0, 0],
None,
[3, 0, 0],
None,
[4, 1, 1],
[4, 1, 1],
[4, 0, 1],
[4, 0, 1],
[5, 0, 1],
None,
[5, 0, 1],
None,
[5, 0, 1],
[5, 0, 1],
[6, 0, 0],
None,
[6, 0, 0],
None,
[5, 0, 1],
[5, 0, 1],
[5, 0, 0],
None,
[6, 0, 1],
None,
[1, 0, 1],
None,
[5, 0, 0],
None,
[5, 0, 0],
None,
None,
None,
None,
None,
None,
None,
None,
None,
)
def __init__(self, initial_string): def __init__(self, initial_string):
"""Construct class based on a string that represents an initial """Construct class based on a string that represents an initial
@ -87,17 +148,18 @@ class ZMemory:
self._memory = bytearray(initial_string) self._memory = bytearray(initial_string)
# Figure out the different sections of memory # Figure out the different sections of memory
self._static_start = self.read_word(0x0e) self._static_start = self.read_word(0x0E)
self._static_end = min(0x0ffff, self._total_size) self._static_end = min(0x0FFFF, self._total_size)
self._dynamic_start = 0 self._dynamic_start = 0
self._dynamic_end = self._static_start - 1 self._dynamic_end = self._static_start - 1
self._high_start = self.read_word(0x04) self._high_start = self.read_word(0x04)
self._high_end = self._total_size self._high_end = self._total_size
self._global_variable_start = self.read_word(0x0c) self._global_variable_start = self.read_word(0x0C)
# Dynamic + static must not exceed 64k # Dynamic + static must not exceed 64k
dynamic_plus_static = ((self._dynamic_end - self._dynamic_start) dynamic_plus_static = (self._dynamic_end - self._dynamic_start) + (
+ (self._static_end - self._static_start)) self._static_end - self._static_start
)
if dynamic_plus_static > 65534: if dynamic_plus_static > 65534:
raise ZMemoryBadMemoryLayout raise ZMemoryBadMemoryLayout
@ -115,10 +177,10 @@ class ZMemory:
raise ZMemoryUnsupportedVersion raise ZMemoryUnsupportedVersion
log("Memory system initialized, map follows") log("Memory system initialized, map follows")
log(" Dynamic memory: %x - %x" % (self._dynamic_start, self._dynamic_end)) log(f" Dynamic memory: {self._dynamic_start:x} - {self._dynamic_end:x}")
log(" Static memory: %x - %x" % (self._static_start, self._static_end)) log(f" Static memory: {self._static_start:x} - {self._static_end:x}")
log(" High memory: %x - %x" % (self._high_start, self._high_end)) log(f" High memory: {self._high_start:x} - {self._high_end:x}")
log(" Global variable start: %x" % self._global_variable_start) log(f" Global variable start: {self._global_variable_start:x}")
def _check_bounds(self, index): def _check_bounds(self, index):
if isinstance(index, slice): if isinstance(index, slice):
@ -258,7 +320,7 @@ class ZMemory:
raise ZMemoryOutOfBounds raise ZMemoryOutOfBounds
if not (0x00 <= value <= 0xFFFF): if not (0x00 <= value <= 0xFFFF):
raise ZMemoryIllegalWrite(value) raise ZMemoryIllegalWrite(value)
log("Write %d to global variable %d" % (value, varnum)) log(f"Write {value} to global variable {varnum}")
actual_address = self._global_variable_start + ((varnum - 0x10) * 2) actual_address = self._global_variable_start + ((varnum - 0x10) * 2)
bf = bitfield.BitField(value) bf = bitfield.BitField(value)
self._memory[actual_address] = bf[8:15] self._memory[actual_address] = bf[8:15]
@ -275,4 +337,4 @@ class ZMemory:
while count < self._total_size: while count < self._total_size:
total += self._memory[count] total += self._memory[count]
count += 1 count += 1
return (total % 0x10000) return total % 0x10000

View file

@ -20,45 +20,59 @@ from .zstring import ZStringFactory
class ZObjectError(Exception): class ZObjectError(Exception):
"General exception for ZObject class" "General exception for ZObject class"
pass pass
class ZObjectIllegalObjectNumber(ZObjectError): class ZObjectIllegalObjectNumber(ZObjectError):
"Illegal object number given." "Illegal object number given."
pass pass
class ZObjectIllegalAttributeNumber(ZObjectError): class ZObjectIllegalAttributeNumber(ZObjectError):
"Illegal attribute number given." "Illegal attribute number given."
pass pass
class ZObjectIllegalPropertyNumber(ZObjectError): class ZObjectIllegalPropertyNumber(ZObjectError):
"Illegal property number given." "Illegal property number given."
pass pass
class ZObjectIllegalPropertySet(ZObjectError): class ZObjectIllegalPropertySet(ZObjectError):
"Illegal set of a property whose size is not 1 or 2." "Illegal set of a property whose size is not 1 or 2."
pass pass
class ZObjectIllegalVersion(ZObjectError): class ZObjectIllegalVersion(ZObjectError):
"Unsupported z-machine version." "Unsupported z-machine version."
pass pass
class ZObjectIllegalPropLength(ZObjectError): class ZObjectIllegalPropLength(ZObjectError):
"Illegal property length." "Illegal property length."
pass pass
class ZObjectMalformedTree(ZObjectError): class ZObjectMalformedTree(ZObjectError):
"Object tree is malformed." "Object tree is malformed."
pass pass
# The interpreter should only need exactly one instance of this class. # The interpreter should only need exactly one instance of this class.
class ZObjectParser: class ZObjectParser:
def __init__(self, zmem): def __init__(self, zmem):
self._memory = zmem self._memory = zmem
self._propdefaults_addr = zmem.read_word(0x0a) self._propdefaults_addr = zmem.read_word(0x0A)
self._stringfactory = ZStringFactory(self._memory) self._stringfactory = ZStringFactory(self._memory)
if 1 <= self._memory.version <= 3: if 1 <= self._memory.version <= 3:
@ -68,7 +82,6 @@ class ZObjectParser:
else: else:
raise ZObjectIllegalVersion raise ZObjectIllegalVersion
def _get_object_addr(self, objectnum): def _get_object_addr(self, objectnum):
"""Return address of object number OBJECTNUM.""" """Return address of object number OBJECTNUM."""
@ -79,16 +92,15 @@ class ZObjectParser:
result = self._objecttree_addr + (9 * (objectnum - 1)) result = self._objecttree_addr + (9 * (objectnum - 1))
elif 4 <= self._memory.version <= 5: elif 4 <= self._memory.version <= 5:
if not (1 <= objectnum <= 65535): if not (1 <= objectnum <= 65535):
log("error: there is no object %d" % objectnum) log(f"error: there is no object {objectnum}")
raise ZObjectIllegalObjectNumber raise ZObjectIllegalObjectNumber
result = self._objecttree_addr + (14 * (objectnum - 1)) result = self._objecttree_addr + (14 * (objectnum - 1))
else: else:
raise ZObjectIllegalVersion raise ZObjectIllegalVersion
log("address of object %d is %d" % (objectnum, result)) log(f"address of object {objectnum} is {result}")
return result return result
def _get_parent_sibling_child(self, objectnum): def _get_parent_sibling_child(self, objectnum):
"""Return [parent, sibling, child] object numbers of object OBJECTNUM.""" """Return [parent, sibling, child] object numbers of object OBJECTNUM."""
@ -101,17 +113,20 @@ class ZObjectParser:
elif 4 <= self._memory.version <= 5: elif 4 <= self._memory.version <= 5:
addr += 6 # skip past attributes addr += 6 # skip past attributes
result = [self._memory.read_word(addr), result = [
self._memory.read_word(addr),
self._memory.read_word(addr + 2), self._memory.read_word(addr + 2),
self._memory.read_word(addr + 4)] self._memory.read_word(addr + 4),
]
else: else:
raise ZObjectIllegalVersion raise ZObjectIllegalVersion
log ("parent/sibling/child of object %d is %d, %d, %d" % log(
(objectnum, result[0], result[1], result[2])) f"parent/sibling/child of object {objectnum} is "
f"{result[0]}, {result[1]}, {result[2]}"
)
return result return result
def _get_proptable_addr(self, objectnum): def _get_proptable_addr(self, objectnum):
"""Return address of property table of object OBJECTNUM.""" """Return address of property table of object OBJECTNUM."""
@ -141,8 +156,7 @@ class ZObjectParser:
else: else:
raise ZObjectIllegalVersion raise ZObjectIllegalVersion
return (addr + (2 * (propnum - 1))) return addr + (2 * (propnum - 1))
# --------- Public APIs ----------- # --------- Public APIs -----------
@ -167,7 +181,6 @@ class ZObjectParser:
return bf[7 - (attrnum % 8)] return bf[7 - (attrnum % 8)]
def get_all_attributes(self, objectnum): def get_all_attributes(self, objectnum):
"""Return a list of all attribute numbers that are set on object """Return a list of all attribute numbers that are set on object
OBJECTNUM""" OBJECTNUM"""
@ -186,28 +199,24 @@ class ZObjectParser:
attrs.append(i) attrs.append(i)
return attrs return attrs
def get_parent(self, objectnum): def get_parent(self, objectnum):
"""Return object number of parent of object number OBJECTNUM.""" """Return object number of parent of object number OBJECTNUM."""
[parent, sibling, child] = self._get_parent_sibling_child(objectnum) [parent, sibling, child] = self._get_parent_sibling_child(objectnum)
return parent return parent
def get_child(self, objectnum): def get_child(self, objectnum):
"""Return object number of child of object number OBJECTNUM.""" """Return object number of child of object number OBJECTNUM."""
[parent, sibling, child] = self._get_parent_sibling_child(objectnum) [parent, sibling, child] = self._get_parent_sibling_child(objectnum)
return child return child
def get_sibling(self, objectnum): def get_sibling(self, objectnum):
"""Return object number of sibling of object number OBJECTNUM.""" """Return object number of sibling of object number OBJECTNUM."""
[parent, sibling, child] = self._get_parent_sibling_child(objectnum) [parent, sibling, child] = self._get_parent_sibling_child(objectnum)
return sibling return sibling
def set_parent(self, objectnum, new_parent_num): def set_parent(self, objectnum, new_parent_num):
"""Make OBJECTNUM's parent pointer point to NEW_PARENT_NUM.""" """Make OBJECTNUM's parent pointer point to NEW_PARENT_NUM."""
@ -219,7 +228,6 @@ class ZObjectParser:
else: else:
raise ZObjectIllegalVersion raise ZObjectIllegalVersion
def set_child(self, objectnum, new_child_num): def set_child(self, objectnum, new_child_num):
"""Make OBJECTNUM's child pointer point to NEW_PARENT_NUM.""" """Make OBJECTNUM's child pointer point to NEW_PARENT_NUM."""
@ -231,7 +239,6 @@ class ZObjectParser:
else: else:
raise ZObjectIllegalVersion raise ZObjectIllegalVersion
def set_sibling(self, objectnum, new_sibling_num): def set_sibling(self, objectnum, new_sibling_num):
"""Make OBJECTNUM's sibling pointer point to NEW_PARENT_NUM.""" """Make OBJECTNUM's sibling pointer point to NEW_PARENT_NUM."""
@ -243,7 +250,6 @@ class ZObjectParser:
else: else:
raise ZObjectIllegalVersion raise ZObjectIllegalVersion
def insert_object(self, parent_object, new_child): def insert_object(self, parent_object, new_child):
"""Prepend object NEW_CHILD to the list of PARENT_OBJECT's children.""" """Prepend object NEW_CHILD to the list of PARENT_OBJECT's children."""
@ -277,14 +283,12 @@ class ZObjectParser:
# we reached the end of the list, never got a match # we reached the end of the list, never got a match
raise ZObjectMalformedTree raise ZObjectMalformedTree
def get_shortname(self, objectnum): def get_shortname(self, objectnum):
"""Return 'short name' of object number OBJECTNUM as ascii string.""" """Return 'short name' of object number OBJECTNUM as ascii string."""
addr = self._get_proptable_addr(objectnum) addr = self._get_proptable_addr(objectnum)
return self._stringfactory.get(addr + 1) return self._stringfactory.get(addr + 1)
def get_prop(self, objectnum, propnum): def get_prop(self, objectnum, propnum):
"""Return either a byte or word value of property PROPNUM of """Return either a byte or word value of property PROPNUM of
object OBJECTNUM.""" object OBJECTNUM."""
@ -296,7 +300,6 @@ class ZObjectParser:
else: else:
raise ZObjectIllegalPropLength raise ZObjectIllegalPropLength
def get_prop_addr_len(self, objectnum, propnum): def get_prop_addr_len(self, objectnum, propnum):
"""Return address & length of value for property number PROPNUM of """Return address & length of value for property number PROPNUM of
object number OBJECTNUM. If object has no such property, then object number OBJECTNUM. If object has no such property, then
@ -305,11 +308,10 @@ class ZObjectParser:
# start at the beginning of the object's proptable # start at the beginning of the object's proptable
addr = self._get_proptable_addr(objectnum) addr = self._get_proptable_addr(objectnum)
# skip past the shortname of the object # skip past the shortname of the object
addr += (2 * self._memory[addr]) addr += 2 * self._memory[addr]
pnum = 0 pnum = 0
if 1 <= self._memory.version <= 3: if 1 <= self._memory.version <= 3:
while self._memory[addr] != 0: while self._memory[addr] != 0:
bf = BitField(self._memory[addr]) bf = BitField(self._memory[addr])
addr += 1 addr += 1
@ -320,7 +322,6 @@ class ZObjectParser:
addr += size addr += size
elif 4 <= self._memory.version <= 5: elif 4 <= self._memory.version <= 5:
while self._memory[addr] != 0: while self._memory[addr] != 0:
bf = BitField(self._memory[addr]) bf = BitField(self._memory[addr])
addr += 1 addr += 1
@ -330,10 +331,7 @@ class ZObjectParser:
addr += 1 addr += 1
size = bf2[5:0] size = bf2[5:0]
else: else:
if bf[6]: size = 2 if bf[6] else 1
size = 2
else:
size = 1
if pnum == propnum: if pnum == propnum:
return (addr, size) return (addr, size)
addr += size addr += size
@ -345,7 +343,6 @@ class ZObjectParser:
default_value_addr = self._get_default_property_addr(propnum) default_value_addr = self._get_default_property_addr(propnum)
return (default_value_addr, 2) return (default_value_addr, 2)
def get_all_properties(self, objectnum): def get_all_properties(self, objectnum):
"""Return a dictionary of all properties listed in the property """Return a dictionary of all properties listed in the property
table of object OBJECTNUM. (Obviously, this discounts 'default' table of object OBJECTNUM. (Obviously, this discounts 'default'
@ -359,7 +356,7 @@ class ZObjectParser:
# skip past the shortname of the object # skip past the shortname of the object
shortname_length = self._memory[addr] shortname_length = self._memory[addr]
addr += 1 addr += 1
addr += (2*shortname_length) addr += 2 * shortname_length
if 1 <= self._memory.version <= 3: if 1 <= self._memory.version <= 3:
while self._memory[addr] != 0: while self._memory[addr] != 0:
@ -382,10 +379,7 @@ class ZObjectParser:
if size == 0: if size == 0:
size = 64 size = 64
else: else:
if bf[6]: size = 2 if bf[6] else 1
size = 2
else:
size = 1
proplist[pnum] = (addr, size) proplist[pnum] = (addr, size)
addr += size addr += size
@ -394,7 +388,6 @@ class ZObjectParser:
return proplist return proplist
def set_property(self, objectnum, propnum, value): def set_property(self, objectnum, propnum, value):
"""Set a property on an object.""" """Set a property on an object."""
proplist = self.get_all_properties(objectnum) proplist = self.get_all_properties(objectnum)
@ -403,21 +396,20 @@ class ZObjectParser:
addr, size = proplist[propnum] addr, size = proplist[propnum]
if size == 1: if size == 1:
self._memory[addr] = (value & 0xFF) self._memory[addr] = value & 0xFF
elif size == 2: elif size == 2:
self._memory.write_word(addr, value) self._memory.write_word(addr, value)
else: else:
raise ZObjectIllegalPropertySet raise ZObjectIllegalPropertySet
def describe_object(self, objectnum): def describe_object(self, objectnum):
"""For debugging purposes, pretty-print everything known about """For debugging purposes, pretty-print everything known about
object OBJECTNUM.""" object OBJECTNUM."""
print("Object number:", objectnum) print("Object number:", objectnum)
print(" Short name:", self.get_shortname(objectnum)) print(" Short name:", self.get_shortname(objectnum))
print(" Parent:", self.get_parent(objectnum), end=' ') print(" Parent:", self.get_parent(objectnum), end=" ")
print(" Sibling:", self.get_sibling(objectnum), end=' ') print(" Sibling:", self.get_sibling(objectnum), end=" ")
print(" Child:", self.get_child(objectnum)) print(" Child:", self.get_child(objectnum))
print(" Attributes:", self.get_all_attributes(objectnum)) print(" Attributes:", self.get_all_attributes(objectnum))
print(" Properties:") print(" Properties:")
@ -425,8 +417,7 @@ class ZObjectParser:
proplist = self.get_all_properties(objectnum) proplist = self.get_all_properties(objectnum)
for key in list(proplist.keys()): for key in list(proplist.keys()):
(addr, len) = proplist[key] (addr, len) = proplist[key]
print(" [%2d] :" % key, end=' ') print(f" [{key:2d}] :", end=" ")
for i in range(0, len): for i in range(0, len):
print("%02X" % self._memory[addr+i], end=' ') print(f"{self._memory[addr + i]:02X}", end=" ")
print() print()

View file

@ -12,8 +12,10 @@ from .zlogging import log
class ZOperationError(Exception): class ZOperationError(Exception):
"General exception for ZOperation class" "General exception for ZOperation class"
pass pass
# Constants defining the known instruction types. These types are # Constants defining the known instruction types. These types are
# related to the number of operands the opcode has: for each operand # related to the number of operands the opcode has: for each operand
# count, there is a separate opcode table, and the actual opcode # count, there is a separate opcode table, and the actual opcode
@ -27,11 +29,11 @@ OPCODE_EXT = 4
# Mapping of those constants to strings describing the opcode # Mapping of those constants to strings describing the opcode
# classes. Used for pretty-printing only. # classes. Used for pretty-printing only.
OPCODE_STRINGS = { OPCODE_STRINGS = {
OPCODE_0OP: '0OP', OPCODE_0OP: "0OP",
OPCODE_1OP: '1OP', OPCODE_1OP: "1OP",
OPCODE_2OP: '2OP', OPCODE_2OP: "2OP",
OPCODE_VAR: 'VAR', OPCODE_VAR: "VAR",
OPCODE_EXT: 'EXT', OPCODE_EXT: "EXT",
} }
# Constants defining the possible operand types. # Constants defining the possible operand types.
@ -40,6 +42,7 @@ SMALL_CONSTANT = 0x1
VARIABLE = 0x2 VARIABLE = 0x2
ABSENT = 0x3 ABSENT = 0x3
class ZOpDecoder: class ZOpDecoder:
def __init__(self, zmem, zstack): def __init__(self, zmem, zstack):
"" ""
@ -64,7 +67,7 @@ class ZOpDecoder:
opcode = self._get_pc() opcode = self._get_pc()
log("Decode opcode %x" % opcode) log(f"Decode opcode {opcode:x}")
# Determine the opcode type, and hand off further parsing. # Determine the opcode type, and hand off further parsing.
if self._memory.version == 5 and opcode == 0xBE: if self._memory.version == 5 and opcode == 0xBE:
@ -88,8 +91,10 @@ class ZOpDecoder:
# encoded in bits 5 and 6 of the opcode. # encoded in bits 5 and 6 of the opcode.
log("Opcode is long") log("Opcode is long")
LONG_OPERAND_TYPES = [SMALL_CONSTANT, VARIABLE] LONG_OPERAND_TYPES = [SMALL_CONSTANT, VARIABLE]
operands = [self._parse_operand(LONG_OPERAND_TYPES[opcode[6]]), operands = [
self._parse_operand(LONG_OPERAND_TYPES[opcode[5]])] self._parse_operand(LONG_OPERAND_TYPES[opcode[6]]),
self._parse_operand(LONG_OPERAND_TYPES[opcode[5]]),
]
return (OPCODE_2OP, opcode[0:5], operands) return (OPCODE_2OP, opcode[0:5], operands)
def _parse_opcode_short(self, opcode): def _parse_opcode_short(self, opcode):
@ -127,6 +132,10 @@ class ZOpDecoder:
return (opcode_type, opcode_num, operands) return (opcode_type, opcode_num, operands)
def _parse_opcode_extended(self):
"""Parse an extended opcode (v5+ feature)."""
raise NotImplementedError("Extended opcodes (v5+) not yet implemented")
def _parse_operand(self, operand_type): def _parse_operand(self, operand_type):
"""Read and return an operand of the given type. """Read and return an operand of the given type.
@ -143,7 +152,7 @@ class ZOpDecoder:
operand = self._get_pc() operand = self._get_pc()
elif operand_type == VARIABLE: elif operand_type == VARIABLE:
variable_number = self._get_pc() variable_number = self._get_pc()
log("Operand is variable %d" % variable_number) log(f"Operand is variable {variable_number}")
if variable_number == 0: if variable_number == 0:
log("Operand value comes from stack") log("Operand value comes from stack")
operand = self._stack.pop_stack() # TODO: make sure this is right. operand = self._stack.pop_stack() # TODO: make sure this is right.
@ -157,7 +166,7 @@ class ZOpDecoder:
log("Operand is absent") log("Operand is absent")
operand = None operand = None
if operand is not None: if operand is not None:
log("Operand value: %d" % operand) log(f"Operand value: {operand}")
return operand return operand
@ -167,8 +176,12 @@ class ZOpDecoder:
""" """
operand_byte = BitField(self._get_pc()) operand_byte = BitField(self._get_pc())
operands = [] operands = []
for operand_type in [operand_byte[6:8], operand_byte[4:6], for operand_type in [
operand_byte[2:4], operand_byte[0:2]]: operand_byte[6:8],
operand_byte[4:6],
operand_byte[2:4],
operand_byte[0:2],
]:
operand = self._parse_operand(operand_type) operand = self._parse_operand(operand_type)
if operand is None: if operand is None:
break break
@ -176,7 +189,6 @@ class ZOpDecoder:
return operands return operands
# Public funcs that the ZPU may also need to call, depending on the # Public funcs that the ZPU may also need to call, depending on the
# opcode being executed: # opcode being executed:
@ -195,14 +207,12 @@ class ZOpDecoder:
return start_addr return start_addr
def get_store_address(self): def get_store_address(self):
"""For store opcodes, read byte pointed to by PC and return the """For store opcodes, read byte pointed to by PC and return the
variable number in which the operation result should be stored. variable number in which the operation result should be stored.
Increment the PC as necessary.""" Increment the PC as necessary."""
return self._get_pc() return self._get_pc()
def get_branch_offset(self): def get_branch_offset(self):
"""For branching opcodes, examine address pointed to by PC, and """For branching opcodes, examine address pointed to by PC, and
return two values: first, either True or False (indicating whether return two values: first, either True or False (indicating whether
@ -230,5 +240,5 @@ class ZOpDecoder:
if bf[5]: if bf[5]:
branch_offset -= 8192 branch_offset -= 8192
log('Branch if %s to offset %+d' % (branch_if_true, branch_offset)) log(f"Branch if {branch_if_true} to offset {branch_offset:+d}")
return branch_if_true, branch_offset return branch_if_true, branch_offset

View file

@ -131,7 +131,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
return [self._rows, self._columns] return [self._rows, self._columns]
def select_window(self, window): def select_window(self, window):
"""Select a window to be the 'active' window, and move that """Select a window to be the 'active' window, and move that
window's cursor to the upper left. window's cursor to the upper left.
@ -143,7 +142,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError() raise NotImplementedError()
def split_window(self, height): def split_window(self, height):
"""Make the upper window appear and be HEIGHT lines tall. To """Make the upper window appear and be HEIGHT lines tall. To
'unsplit' a window, call with a height of 0 lines. 'unsplit' a window, call with a height of 0 lines.
@ -153,7 +151,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError() raise NotImplementedError()
def set_cursor_position(self, x, y): def set_cursor_position(self, x, y):
"""Set the cursor to (row, column) coordinates (X,Y) in the """Set the cursor to (row, column) coordinates (X,Y) in the
current window, where (1,1) is the upper-left corner. current window, where (1,1) is the upper-left corner.
@ -168,9 +165,7 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError() raise NotImplementedError()
def erase_window(self, window=WINDOW_LOWER, color=COLOR_CURRENT):
def erase_window(self, window=WINDOW_LOWER,
color=COLOR_CURRENT):
"""Erase WINDOW to background COLOR. """Erase WINDOW to background COLOR.
WINDOW should be one of WINDOW_UPPER or WINDOW_LOWER. WINDOW should be one of WINDOW_UPPER or WINDOW_LOWER.
@ -185,7 +180,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError() raise NotImplementedError()
def erase_line(self): def erase_line(self):
"""Erase from the current cursor position to the end of its line """Erase from the current cursor position to the end of its line
in the current window. in the current window.
@ -196,7 +190,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError() raise NotImplementedError()
# Status Line # Status Line
# #
# These routines are only called if the has_status_line capability # These routines are only called if the has_status_line capability
@ -216,7 +209,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError() raise NotImplementedError()
def print_status_time(self, hours, minutes): def print_status_time(self, hours, minutes):
"""Print a status line in the upper window, as follows: """Print a status line in the upper window, as follows:
@ -229,7 +221,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError() raise NotImplementedError()
# Text Appearances # Text Appearances
# #
@ -238,7 +229,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
return [self._fontwidth, self._fontheight] return [self._fontwidth, self._fontheight]
def set_font(self, font_number): def set_font(self, font_number):
"""Set the current window's font to one of """Set the current window's font to one of
@ -256,7 +246,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError() raise NotImplementedError()
def set_text_style(self, style): def set_text_style(self, style):
"""Set the current text style to the given text style. """Set the current text style to the given text style.
@ -281,7 +270,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError() raise NotImplementedError()
def set_text_color(self, foreground_color, background_color): def set_text_color(self, foreground_color, background_color):
"""Set current text foreground and background color. Each color """Set current text foreground and background color. Each color
should correspond to one of the COLOR_* constants. should correspond to one of the COLOR_* constants.
@ -292,7 +280,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError() raise NotImplementedError()
# Standard output # Standard output
def write(self, string): def write(self, string):

View file

@ -13,30 +13,40 @@ from .zlogging import log
class ZStackError(Exception): class ZStackError(Exception):
"General exception for stack or routine-related errors" "General exception for stack or routine-related errors"
pass pass
class ZStackUnsupportedVersion(ZStackError): class ZStackUnsupportedVersion(ZStackError):
"Unsupported version of Z-story file." "Unsupported version of Z-story file."
pass pass
class ZStackNoRoutine(ZStackError): class ZStackNoRoutine(ZStackError):
"No routine is being executed." "No routine is being executed."
pass pass
class ZStackNoSuchVariable(ZStackError): class ZStackNoSuchVariable(ZStackError):
"Trying to access non-existent local variable." "Trying to access non-existent local variable."
pass pass
class ZStackPopError(ZStackError): class ZStackPopError(ZStackError):
"Nothing to pop from stack!" "Nothing to pop from stack!"
pass pass
# Helper class used by ZStackManager; a 'routine' object which # Helper class used by ZStackManager; a 'routine' object which
# includes its own private stack of data. # includes its own private stack of data.
class ZRoutine: class ZRoutine:
def __init__(
def __init__(self, start_addr, return_addr, zmem, args, self, start_addr, return_addr, zmem, args, local_vars=None, stack=None
local_vars=None, stack=None): ):
"""Initialize a routine object beginning at START_ADDR in ZMEM, """Initialize a routine object beginning at START_ADDR in ZMEM,
with initial argument values in list ARGS. If LOCAL_VARS is None, with initial argument values in list ARGS. If LOCAL_VARS is None,
then parse them from START_ADDR.""" then parse them from START_ADDR."""
@ -55,7 +65,7 @@ class ZRoutine:
else: else:
num_local_vars = zmem[self.start_addr] num_local_vars = zmem[self.start_addr]
if not (0 <= num_local_vars <= 15): if not (0 <= num_local_vars <= 15):
log("num local vars is %d" % num_local_vars) log(f"num local vars is {num_local_vars}")
raise ZStackError raise ZStackError
self.start_addr += 1 self.start_addr += 1
@ -74,31 +84,26 @@ class ZRoutine:
for i in range(0, len(args)): for i in range(0, len(args)):
self.local_vars[i] = args[i] self.local_vars[i] = args[i]
def pretty_print(self): def pretty_print(self):
"Display a ZRoutine nicely, for debugging purposes." "Display a ZRoutine nicely, for debugging purposes."
log("ZRoutine: start address: %d" % self.start_addr) log(f"ZRoutine: start address: {self.start_addr}")
log("ZRoutine: return value address: %d" % self.return_addr) log(f"ZRoutine: return value address: {self.return_addr}")
log("ZRoutine: program counter: %d" % self.program_counter) log(f"ZRoutine: program counter: {self.program_counter}")
log("ZRoutine: local variables: %d" % self.local_vars) log(f"ZRoutine: local variables: {self.local_vars}")
class ZStackBottom: class ZStackBottom:
def __init__(self): def __init__(self):
self.program_counter = 0 # used as a cache only self.program_counter = 0 # used as a cache only
class ZStackManager: class ZStackManager:
def __init__(self, zmem): def __init__(self, zmem):
self._memory = zmem self._memory = zmem
self._stackbottom = ZStackBottom() self._stackbottom = ZStackBottom()
self._call_stack = [self._stackbottom] self._call_stack = [self._stackbottom]
def get_local_variable(self, varnum): def get_local_variable(self, varnum):
"""Return value of local variable VARNUM from currently-running """Return value of local variable VARNUM from currently-running
routine. VARNUM must be a value between 0 and 15, and must routine. VARNUM must be a value between 0 and 15, and must
@ -112,8 +117,7 @@ class ZStackManager:
current_routine = self._call_stack[-1] current_routine = self._call_stack[-1]
return current_routine.local_vars[varnum] return current_routine.local_vars[varnum] # type: ignore[possibly-missing-attribute]
def set_local_variable(self, varnum, value): def set_local_variable(self, varnum, value):
"""Set value of local variable VARNUM to VALUE in """Set value of local variable VARNUM to VALUE in
@ -128,29 +132,25 @@ class ZStackManager:
current_routine = self._call_stack[-1] current_routine = self._call_stack[-1]
current_routine.local_vars[varnum] = value current_routine.local_vars[varnum] = value # type: ignore[possibly-missing-attribute]
def push_stack(self, value): def push_stack(self, value):
"Push VALUE onto the top of the current routine's data stack." "Push VALUE onto the top of the current routine's data stack."
current_routine = self._call_stack[-1] current_routine = self._call_stack[-1]
current_routine.stack.append(value) current_routine.stack.append(value) # type: ignore[possibly-missing-attribute]
def pop_stack(self): def pop_stack(self):
"Remove and return value from the top of the data stack." "Remove and return value from the top of the data stack."
current_routine = self._call_stack[-1] current_routine = self._call_stack[-1]
return current_routine.stack.pop() return current_routine.stack.pop() # type: ignore[possibly-missing-attribute]
def get_stack_frame_index(self): def get_stack_frame_index(self):
"Return current stack frame number. For use by 'catch' opcode." "Return current stack frame number. For use by 'catch' opcode."
return len(self._call_stack) - 1 return len(self._call_stack) - 1
# Used by quetzal save-file parser to reconstruct stack-frames. # Used by quetzal save-file parser to reconstruct stack-frames.
def push_routine(self, routine): def push_routine(self, routine):
"""Blindly push a ZRoutine object to the call stack. """Blindly push a ZRoutine object to the call stack.
@ -160,24 +160,20 @@ class ZStackManager:
self._call_stack.append(routine) self._call_stack.append(routine)
# ZPU should call this whenever it decides to call a new routine. # ZPU should call this whenever it decides to call a new routine.
def start_routine(self, routine_addr, return_addr, def start_routine(self, routine_addr, return_addr, program_counter, args):
program_counter, args):
"""Save the state of the currenly running routine (by examining """Save the state of the currenly running routine (by examining
the current value of the PROGRAM_COUNTER), and prepare for the current value of the PROGRAM_COUNTER), and prepare for
execution of a new routine at ROUTINE_ADDR with list of initial execution of a new routine at ROUTINE_ADDR with list of initial
arguments ARGS.""" arguments ARGS."""
new_routine = ZRoutine(routine_addr, return_addr, new_routine = ZRoutine(routine_addr, return_addr, self._memory, args)
self._memory, args)
current_routine = self._call_stack[-1] current_routine = self._call_stack[-1]
current_routine.program_counter = program_counter current_routine.program_counter = program_counter
self._call_stack.append(new_routine) self._call_stack.append(new_routine)
return new_routine.start_addr return new_routine.start_addr
# ZPU should call this whenever it decides to return from current # ZPU should call this whenever it decides to return from current
# routine. # routine.
def finish_routine(self, return_value): def finish_routine(self, return_value):
@ -190,15 +186,15 @@ class ZStackManager:
current_routine = self._call_stack[-1] current_routine = self._call_stack[-1]
# Depending on many things, return stuff. # Depending on many things, return stuff.
if exiting_routine.return_addr != None: if exiting_routine.return_addr is not None: # type: ignore[possibly-missing-attribute]
if exiting_routine.return_addr == 0: # Push to stack if exiting_routine.return_addr == 0: # type: ignore[possibly-missing-attribute]
# Push to stack
self.push_stack(return_value) self.push_stack(return_value)
elif 0 < exiting_routine.return_addr < 10: # Store in local var elif 0 < exiting_routine.return_addr < 10: # type: ignore[possibly-missing-attribute]
self.set_local_variable(exiting_routine.return_addr, # Store in local var
return_value) self.set_local_variable(exiting_routine.return_addr, return_value) # type: ignore[possibly-missing-attribute]
else: # Store in global var else:
self._memory.write_global(exiting_routine.return_addr, # Store in global var
return_value) self._memory.write_global(exiting_routine.return_addr, return_value) # type: ignore[possibly-missing-attribute]
return current_routine.program_counter return current_routine.program_counter

View file

@ -5,6 +5,7 @@
# root directory of this distribution. # root directory of this distribution.
# #
class ZOutputStream: class ZOutputStream:
"""Abstract class representing an output stream for a z-machine.""" """Abstract class representing an output stream for a z-machine."""
@ -38,9 +39,14 @@ class ZInputStream:
"has_timed_input": False, "has_timed_input": False,
} }
def read_line(self, original_text=None, max_length=0, def read_line(
self,
original_text=None,
max_length=0,
terminating_characters=None, terminating_characters=None,
timed_input_routine=None, timed_input_interval=0): timed_input_routine=None,
timed_input_interval=0,
):
"""Reads from the input stream and returns a unicode string """Reads from the input stream and returns a unicode string
representing the characters the end-user entered. The characters representing the characters the end-user entered. The characters
are displayed to the screen as the user types them. are displayed to the screen as the user types them.
@ -83,8 +89,7 @@ class ZInputStream:
raise NotImplementedError() raise NotImplementedError()
def read_char(self, timed_input_routine=None, def read_char(self, timed_input_routine=None, timed_input_interval=0):
timed_input_interval=0):
"""Reads a single character from the stream and returns it as a """Reads a single character from the stream and returns it as a
unicode character. unicode character.

View file

@ -20,6 +20,7 @@ OUTPUT_PLAYER_INPUT = 4 # contains *only* the player's typed commands
INPUT_KEYBOARD = 0 INPUT_KEYBOARD = 0
INPUT_FILE = 1 INPUT_FILE = 1
class ZOutputStreamManager: class ZOutputStreamManager:
"""Manages output streams for a Z-Machine.""" """Manages output streams for a Z-Machine."""
@ -66,6 +67,7 @@ class ZOutputStreamManager:
for stream in self._selectedStreams: for stream in self._selectedStreams:
self._streams[stream].write(string) self._streams[stream].write(string)
class ZInputStreamManager: class ZInputStreamManager:
"""Manages input streams for a Z-Machine.""" """Manages input streams for a Z-Machine."""
@ -91,6 +93,7 @@ class ZInputStreamManager:
return self._streams[self._selectedStream] return self._streams[self._selectedStream]
class ZStreamManager: class ZStreamManager:
def __init__(self, zmem, zui): def __init__(self, zmem, zui):
self.input = ZInputStreamManager(zui) self.input = ZInputStreamManager(zui)

View file

@ -11,6 +11,7 @@ import itertools
class ZStringEndOfString(Exception): class ZStringEndOfString(Exception):
"""No more data left in string.""" """No more data left in string."""
class ZStringIllegalAbbrevInString(Exception): class ZStringIllegalAbbrevInString(Exception):
"""String abbreviation encountered within a string in a context """String abbreviation encountered within a string in a context
where it is not allowed.""" where it is not allowed."""
@ -22,6 +23,7 @@ class ZStringTranslator:
def get(self, addr): def get(self, addr):
from .bitfield import BitField from .bitfield import BitField
pos = (addr, BitField(self._mem.read_word(addr)), 0) pos = (addr, BitField(self._mem.read_word(addr)), 0)
s = [] s = []
@ -50,16 +52,13 @@ class ZStringTranslator:
# Kill processing. # Kill processing.
raise ZStringEndOfString raise ZStringEndOfString
# Get and return the next block. # Get and return the next block.
return (pos[0] + 2, return (pos[0] + 2, BitField(self._mem.read_word(pos[0] + 2)), 0)
BitField(self._mem.read_word(pos[0] + 2)),
0)
# Just increment the intra-block counter. # Just increment the intra-block counter.
return (pos[0], pos[1], offset) return (pos[0], pos[1], offset)
class ZCharTranslator: class ZCharTranslator:
# The default alphabet tables for ZChar translation. # The default alphabet tables for ZChar translation.
# As the codes 0-5 are special, alphabets start with code 0x6. # As the codes 0-5 are special, alphabets start with code 0x6.
DEFAULT_A0 = [ord(x) for x in "abcdefghijklmnopqrstuvwxyz"] DEFAULT_A0 = [ord(x) for x in "abcdefghijklmnopqrstuvwxyz"]
@ -109,8 +108,7 @@ class ZCharTranslator:
xlator = ZStringTranslator(self._mem) xlator = ZStringTranslator(self._mem)
def _load_subtable(num, base): def _load_subtable(num, base):
for i,zoff in [(i,base+(num*64)+(i*2)) for i, zoff in [(i, base + (num * 64) + (i * 2)) for i in range(0, 32)]:
for i in range(0, 32)]:
zaddr = self._mem.read_word(zoff) zaddr = self._mem.read_word(zoff)
zstr = xlator.get(self._mem.word_address(zaddr)) zstr = xlator.get(self._mem.word_address(zaddr))
zchr = self.get(zstr, allow_abbreviations=False) zchr = self.get(zstr, allow_abbreviations=False)
@ -128,11 +126,12 @@ class ZCharTranslator:
"""Load the special character code handlers for the current """Load the special character code handlers for the current
machine version. machine version.
""" """
# The following three functions define the three possible # The following three functions define the three possible
# special character code handlers. # special character code handlers.
def newline(state): def newline(state):
"""Append ZSCII 13 (newline) to the output.""" """Append ZSCII 13 (newline) to the output."""
state['zscii'].append(13) state["zscii"].append(13)
def shift_alphabet(state, direction, lock): def shift_alphabet(state, direction, lock):
"""Shift the current alphaber up or down. If lock is """Shift the current alphaber up or down. If lock is
@ -140,9 +139,9 @@ class ZCharTranslator:
after outputting 1 character. Else, the alphabet will after outputting 1 character. Else, the alphabet will
remain unchanged until the next shift. remain unchanged until the next shift.
""" """
state['curr_alpha'] = (state['curr_alpha'] + direction) % 3 state["curr_alpha"] = (state["curr_alpha"] + direction) % 3
if lock: if lock:
state['prev_alpha'] = state['curr_alpha'] state["prev_alpha"] = state["curr_alpha"]
def abbreviation(state, abbrev): def abbreviation(state, abbrev):
"""Insert the given abbreviation from the given table into """Insert the given abbreviation from the given table into
@ -152,18 +151,18 @@ class ZCharTranslator:
character will be the offset within that table of the character will be the offset within that table of the
abbreviation. Set up a state handler to intercept the next abbreviation. Set up a state handler to intercept the next
character and output the right abbreviation.""" character and output the right abbreviation."""
def write_abbreviation(state, c, subtable): def write_abbreviation(state, c, subtable):
state['zscii'] += self._abbrevs[(subtable, c)] state["zscii"] += self._abbrevs[(subtable, c)]
del state['state_handler'] del state["state_handler"]
# If we're parsing an abbreviation, there should be no # If we're parsing an abbreviation, there should be no
# nested abbreviations. So this is just a sanity check for # nested abbreviations. So this is just a sanity check for
# people feeding us bad stories. # people feeding us bad stories.
if not state['allow_abbreviations']: if not state["allow_abbreviations"]:
raise ZStringIllegalAbbrevInString raise ZStringIllegalAbbrevInString
state['state_handler'] = lambda s,c: write_abbreviation(s, c, state["state_handler"] = lambda s, c: write_abbreviation(s, c, abbrev)
abbrev)
# Register the specials handlers depending on machine version. # Register the specials handlers depending on machine version.
if self._mem.version == 1: if self._mem.version == 1:
@ -192,33 +191,33 @@ class ZCharTranslator:
} }
def _special_zscii(self, state, char): def _special_zscii(self, state, char):
if 'zscii_char' not in list(state.keys()): if "zscii_char" not in list(state.keys()):
state['zscii_char'] = char state["zscii_char"] = char
else: else:
zchar = (state['zscii_char'] << 5) + char zchar = (state["zscii_char"] << 5) + char
state['zscii'].append(zchar) state["zscii"].append(zchar)
del state['zscii_char'] del state["zscii_char"]
del state['state_handler'] del state["state_handler"]
def get(self, zstr, allow_abbreviations=True): def get(self, zstr, allow_abbreviations=True):
state = { state = {
'curr_alpha': 0, "curr_alpha": 0,
'prev_alpha': 0, "prev_alpha": 0,
'zscii': [], "zscii": [],
'allow_abbreviations': allow_abbreviations, "allow_abbreviations": allow_abbreviations,
} }
for c in zstr: for c in zstr:
if 'state_handler' in list(state.keys()): if "state_handler" in list(state.keys()):
# If a special handler has registered itself, then hand # If a special handler has registered itself, then hand
# processing over to it. # processing over to it.
state['state_handler'](state, c) state["state_handler"](state, c) # type: ignore[call-non-callable]
elif c in list(self._specials.keys()): elif c in list(self._specials.keys()):
# Hand off per-ZM version special char handling. # Hand off per-ZM version special char handling.
self._specials[c](state) self._specials[c](state)
elif state['curr_alpha'] == 2 and c == 6: elif state["curr_alpha"] == 2 and c == 6:
# Handle the strange A2/6 character # Handle the strange A2/6 character
state['state_handler'] = self._special_zscii state["state_handler"] = self._special_zscii
else: else:
# Do the usual Thing: append a zscii code to the # Do the usual Thing: append a zscii code to the
# decoded sequence and revert to the "previous" # decoded sequence and revert to the "previous"
@ -227,36 +226,97 @@ class ZCharTranslator:
if c == 0: if c == 0:
# Append a space. # Append a space.
z = 32 z = 32
elif state['curr_alpha'] == 2: elif state["curr_alpha"] == 2:
# The symbol alphabet table only has 25 chars # The symbol alphabet table only has 25 chars
# because of the A2/6 special char, so we need to # because of the A2/6 special char, so we need to
# adjust differently. # adjust differently.
z = self._alphabet[state['curr_alpha']][c-7] z = self._alphabet[state["curr_alpha"]][c - 7]
else: else:
z = self._alphabet[state['curr_alpha']][c-6] z = self._alphabet[state["curr_alpha"]][c - 6]
state['zscii'].append(z) state["zscii"].append(z)
state['curr_alpha'] = state['prev_alpha'] state["curr_alpha"] = state["prev_alpha"]
return state['zscii'] return state["zscii"]
class ZsciiTranslator: class ZsciiTranslator:
# The default Unicode Translation Table that maps to ZSCII codes # The default Unicode Translation Table that maps to ZSCII codes
# 155-251. The codes are unicode codepoints for a host of strange # 155-251. The codes are unicode codepoints for a host of strange
# characters. # characters.
DEFAULT_UTT = [chr(x) for x in DEFAULT_UTT = [
(0xe4, 0xf6, 0xfc, 0xc4, 0xd6, 0xdc, chr(x)
0xdf, 0xbb, 0xab, 0xeb, 0xef, 0xff, for x in (
0xcb, 0xcf, 0xe1, 0xe9, 0xed, 0xf3, 0xE4,
0xfa, 0xfd, 0xc1, 0xc9, 0xcd, 0xd3, 0xF6,
0xda, 0xdd, 0xe0, 0xe8, 0xec, 0xf2, 0xFC,
0xf9, 0xc0, 0xc8, 0xcc, 0xd2, 0xd9, 0xC4,
0xe2, 0xea, 0xee, 0xf4, 0xfb, 0xc2, 0xD6,
0xca, 0xce, 0xd4, 0xdb, 0xe5, 0xc5, 0xDC,
0xf8, 0xd8, 0xe3, 0xf1, 0xf5, 0xc3, 0xDF,
0xd1, 0xd5, 0xe6, 0xc6, 0xe7, 0xc7, 0xBB,
0xfe, 0xf0, 0xde, 0xd0, 0xa3, 0x153, 0xAB,
0x152, 0xa1, 0xbf)] 0xEB,
0xEF,
0xFF,
0xCB,
0xCF,
0xE1,
0xE9,
0xED,
0xF3,
0xFA,
0xFD,
0xC1,
0xC9,
0xCD,
0xD3,
0xDA,
0xDD,
0xE0,
0xE8,
0xEC,
0xF2,
0xF9,
0xC0,
0xC8,
0xCC,
0xD2,
0xD9,
0xE2,
0xEA,
0xEE,
0xF4,
0xFB,
0xC2,
0xCA,
0xCE,
0xD4,
0xDB,
0xE5,
0xC5,
0xF8,
0xD8,
0xE3,
0xF1,
0xF5,
0xC3,
0xD1,
0xD5,
0xE6,
0xC6,
0xE7,
0xC7,
0xFE,
0xF0,
0xDE,
0xD0,
0xA3,
0x153,
0x152,
0xA1,
0xBF,
)
]
# And here is the offset at which the Unicode Translation Table # And here is the offset at which the Unicode Translation Table
# starts. # starts.
UTT_OFFSET = 155 UTT_OFFSET = 155
@ -299,13 +359,8 @@ class ZsciiTranslator:
def __init__(self, zmem): def __init__(self, zmem):
self._mem = zmem self._mem = zmem
self._output_table = { self._output_table = {0: "", 10: "\n"}
0 : "", self._input_table = {"\n": 10}
10: "\n"
}
self._input_table = {
"\n": 10
}
self._load_unicode_table() self._load_unicode_table()
@ -324,8 +379,11 @@ class ZsciiTranslator:
# Oh and we also pull the items from the subclass into this # Oh and we also pull the items from the subclass into this
# instance, so as to make reference to these special codes # instance, so as to make reference to these special codes
# easier. # easier.
for name,code in [(c,v) for c,v in list(self.Input.__dict__.items()) for name, code in [
if not c.startswith('__')]: (c, v)
for c, v in list(self.Input.__dict__.items())
if not c.startswith("__")
]:
self._input_table[code] = code self._input_table[code] = code
setattr(self, name, code) setattr(self, name, code)
@ -350,18 +408,22 @@ class ZsciiTranslator:
# #
# Then there is a unicode translation table other than the # Then there is a unicode translation table other than the
# default that needs loading. # default that needs loading.
if (ext_table_addr != 0 and if (
self._mem.read_word(ext_table_addr) >= 3 and ext_table_addr != 0
self._mem.read_word(ext_table_addr+6) != 0): and self._mem.read_word(ext_table_addr) >= 3
and self._mem.read_word(ext_table_addr + 6) != 0
):
# Get the unicode translation table address
utt_addr = self._mem.read_word(ext_table_addr + 6)
# The first byte is the number of unicode characters # The first byte is the number of unicode characters
# in the table. # in the table.
utt_len = self._mem[ext_table_addr] utt_len = self._mem[utt_addr]
# Build the range of addresses to load from, and build # Build the range of addresses to load from, and build
# the unicode translation table as a list of unicode # the unicode translation table as a list of unicode
# chars. # chars.
utt_range = range(ext_table+1, ext_table+1+(utt_len*2), 2) utt_range = range(utt_addr + 1, utt_addr + 1 + (utt_len * 2), 2)
utt = [chr(self._mem.read_word(i)) for i in utt_range] utt = [chr(self._mem.read_word(i)) for i in utt_range]
else: else:
utt = self.DEFAULT_UTT utt = self.DEFAULT_UTT
@ -380,7 +442,7 @@ class ZsciiTranslator:
try: try:
return self._output_table[index] return self._output_table[index]
except KeyError: except KeyError:
raise IndexError("No such ZSCII character") raise IndexError("No such ZSCII character") from None
def utoz(self, char): def utoz(self, char):
"""Translate the given Unicode code into the corresponding """Translate the given Unicode code into the corresponding
@ -389,10 +451,10 @@ class ZsciiTranslator:
try: try:
return self._input_table[char] return self._input_table[char]
except KeyError: except KeyError:
raise IndexError("No such input character") raise IndexError("No such input character") from None
def get(self, zscii): def get(self, zscii):
return ''.join([self.ztou(c) for c in zscii]) return "".join([self.ztou(c) for c in zscii])
class ZStringFactory: class ZStringFactory: