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.
@ -31,7 +32,7 @@ class BitField:
start, stop = index.start, index.stop start, stop = index.start, index.stop
if start > stop: if start > stop:
(start, stop) = (stop, start) (start, stop) = (stop, start)
mask = (1<<(stop - start)) -1 mask = (1 << (stop - start)) - 1
return (self._d >> start) & mask return (self._d >> start) & mask
else: else:
return (self._d >> index) & 1 return (self._d >> index) & 1
@ -42,7 +43,7 @@ class BitField:
value = ord(value) value = ord(value)
if isinstance(index, slice): if isinstance(index, slice):
start, stop = index.start, index.stop start, stop = index.start, index.stop
mask = (1<<(stop - start)) -1 mask = (1 << (stop - start)) - 1
value = (value & mask) << start value = (value & mask) << start
mask = mask << start mask = mask << start
self._d = (self._d & ~mask) | value self._d = (self._d & ~mask) | value
@ -50,7 +51,7 @@ class BitField:
else: else:
value = (value) << index value = (value) << index
mask = (1) << index mask = (1) << index
self._d = (self._d & ~mask) | value self._d = (self._d & ~mask) | value
def __int__(self): def __int__(self):
"""Return the whole bitfield as an integer.""" """Return the whole bitfield as an integer."""
@ -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,38 +80,37 @@ 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_MAXVAL = 28 keycode_Func9 = 0xFFFFFFE7
keycode_Func10 = 0xFFFFFFE6
keycode_Func11 = 0xFFFFFFE5
keycode_Func12 = 0xFFFFFFE4
keycode_MAXVAL = 28
style_Normal = 0 style_Normal = 0
style_Emphasized = 1 style_Emphasized = 1
@ -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
@ -137,23 +137,23 @@ wintype_TextBuffer = 3
wintype_TextGrid = 4 wintype_TextGrid = 4
wintype_Graphics = 5 wintype_Graphics = 5
winmethod_Left = 0x00 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
filemode_Write = 0x01 filemode_Write = 0x01
@ -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), (
ctypes.POINTER(glui32), None,
ctypes.POINTER(winid_t))), "glk_window_get_arrangement",
(
winid_t,
ctypes.POINTER(glui32),
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,14 +257,13 @@ 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))),
(None, "glk_cancel_char_event", (winid_t,)), (None, "glk_cancel_char_event", (winid_t,)),
(None, "glk_cancel_mouse_event", (winid_t,)), (None, "glk_cancel_mouse_event", (winid_t,)),
] ]
# Function prototypes for the optional Unicode extension of the Glk # Function prototypes for the optional Unicode extension of the Glk
# API. # API.
@ -269,20 +273,24 @@ 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
@ -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,435 +29,434 @@ 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."
class QuetzalParser: class QuetzalParser:
"""A class to read a Quetzal save-file and modify a z-machine.""" """A class to read a Quetzal save-file and modify a z-machine."""
def __init__(self, zmachine): def __init__(self, zmachine):
log("Creating new instance of QuetzalParser") log("Creating new instance of QuetzalParser")
self._zmachine = zmachine self._zmachine = zmachine
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):
"""Parse a chunk of type IFhd, and check that the quetzal file
really belongs to the current story (by comparing release number,
serial number, and checksum.)"""
# Spec says that this chunk *must* come before memory or stack chunks.
if self._seen_mem_or_stks:
raise QuetzalIllegalChunkOrder
bytes = data
if len(bytes) != 13:
raise QuetzalMalformedChunk
chunk_release = (data[0] << 8) + data[1]
chunk_serial = data[2:8]
chunk_checksum = (data[8] << 8) + data[9]
chunk_pc = (data[10] << 16) + (data[11] << 8) + data[12]
self._zmachine._opdecoder.program_counter = chunk_pc
log(f" Found release number {chunk_release}")
log(f" Found serial number {int(chunk_serial)}")
log(f" Found checksum {chunk_checksum}")
log(f" Initial program counter value is {chunk_pc}")
self._last_loaded_metadata["release number"] = chunk_release
self._last_loaded_metadata["serial number"] = chunk_serial
self._last_loaded_metadata["checksum"] = chunk_checksum
self._last_loaded_metadata["program counter"] = chunk_pc
# Verify the save-file params against the current z-story header
mem = self._zmachine._mem
if mem.read_word(2) != chunk_release:
raise QuetzalMismatchedFile
serial_bytes = chunk_serial
if serial_bytes != mem[0x12:0x18]:
raise QuetzalMismatchedFile
mem_checksum = mem.read_word(0x1C)
if mem_checksum != 0 and (mem_checksum != chunk_checksum):
raise QuetzalMismatchedFile
log(" Quetzal file correctly verifies against original story.")
def _parse_cmem(self, data):
"""Parse a chunk of type Cmem. Decompress an image of dynamic
memory, and place it into the ZMachine."""
log(" Decompressing dynamic memory image")
self._seen_mem_or_stks = True
# Just duplicate the dynamic memory block of the pristine story image,
# and then make tweaks to it as we decode the runlength-encoding.
pmem = self._zmachine._pristine_mem
cmem = self._zmachine._mem
savegame_mem = list(pmem[pmem._dynamic_start : (pmem._dynamic_end + 1)])
memlen = len(savegame_mem)
memcounter = 0
log(f" Dynamic memory length is {memlen}")
self._last_loaded_metadata["memory length"] = memlen
runlength_bytes = data
bytelen = len(runlength_bytes)
bytecounter = 0
log(" Decompressing dynamic memory image")
while bytecounter < bytelen:
byte = runlength_bytes[bytecounter]
if byte != 0:
savegame_mem[memcounter] = byte ^ pmem[memcounter]
memcounter += 1
bytecounter += 1
log(f" Set byte {memcounter}:{savegame_mem[memcounter]}")
else:
bytecounter += 1
num_extra_zeros = runlength_bytes[bytecounter]
memcounter += 1 + num_extra_zeros
bytecounter += 1
log(f" Skipped {1 + num_extra_zeros} unchanged bytes")
if memcounter >= memlen:
raise QuetzalMemoryOutOfBounds
# If memcounter finishes less then memlen, that's totally fine, it
# just means there are no more diffs to apply.
cmem[cmem._dynamic_start : (cmem._dynamic_end + 1)] = savegame_mem
log(" Successfully installed new dynamic memory.")
def _parse_umem(self, data):
"""Parse a chunk of type Umem. Suck a raw image of dynamic memory
and place it into the ZMachine."""
### TODO: test this by either finding an interpreter that ouptuts
## this type of chunk, or by having own QuetzalWriter class
## (optionally) do it.
log(" Loading uncompressed dynamic memory image")
self._seen_mem_or_stks = True
cmem = self._zmachine._mem
dynamic_len = (cmem._dynamic_end - cmem.dynamic_start) + 1
log(f" Dynamic memory length is {dynamic_len}")
self._last_loaded_metadata["dynamic memory length"] = dynamic_len
savegame_mem = [ord(x) for x in data]
if len(savegame_mem) != dynamic_len:
raise QuetzalMemoryMismatch
cmem[cmem._dynamic_start : (cmem._dynamic_end + 1)] = savegame_mem
log(" Successfully installed new dynamic memory.")
def _parse_stks(self, data):
"""Parse a chunk of type Stks."""
log(" Begin parsing of stack frames")
# Our strategy here is simply to create an entirely new
# ZStackManager object and populate it with a series of ZRoutine
# stack-frames parses from the quetzal file. We then attach this
# new ZStackManager to our z-machine, and allow the old one to be
# garbage collected.
stackmanager = zstackmanager.ZStackManager(self._zmachine._mem)
self._seen_mem_or_stks = True
bytes = data
total_len = len(bytes)
ptr = 0
# Read successive stack frames:
while ptr < total_len:
log(" Parsing stack frame...")
return_pc = (bytes[ptr] << 16) + (bytes[ptr + 1] << 8) + bytes[ptr + 3]
ptr += 3
flags_bitfield = bitfield.BitField(bytes[ptr])
ptr += 1
_varnum = bytes[ptr] ### TODO: tells us which variable gets the result
ptr += 1
_argflag = bytes[ptr]
ptr += 1
evalstack_size = (bytes[ptr] << 8) + bytes[ptr + 1]
ptr += 2
# read anywhere from 0 to 15 local vars
local_vars = []
for _i in range(flags_bitfield[0:3]):
var = (bytes[ptr] << 8) + bytes[ptr + 1]
ptr += 2
local_vars.append(var)
log(f" Found {len(local_vars)} local vars")
# least recent to most recent stack values:
stack_values = []
for _i in range(evalstack_size):
val = (bytes[ptr] << 8) + bytes[ptr + 1]
ptr += 2
stack_values.append(val)
log(f" Found {len(stack_values)} local stack values")
### Interesting... the reconstructed stack frames have no 'start
### address'. I guess it doesn't matter, since we only need to
### pop back to particular return addresses to resume each
### routine.
### TODO: I can exactly which of the 7 args is "supplied", but I
### don't understand where the args *are*??
routine = zstackmanager.ZRoutine(
0, return_pc, self._zmachine._mem, [], local_vars, stack_values
)
stackmanager.push_routine(routine)
log(" Added new frame to stack.")
if ptr > total_len:
raise QuetzalStackFrameOverflow
self._zmachine._stackmanager = stackmanager
log(" Successfully installed all stack frames.")
def _parse_intd(self, data):
"""Parse a chunk of type IntD, which is interpreter-dependent info."""
log(" Begin parsing of interpreter-dependent metadata")
bytes = [ord(x) for x in data]
_os_id = bytes[0:3]
_flags = bytes[4]
_contents_id = bytes[5]
_reserved = bytes[6:8]
_interpreter_id = bytes[8:12]
_private_data = bytes[12:]
### TODO: finish this
# The following 3 chunks are totally optional metadata, and are
# 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
# them to the user?
def _parse_auth(self, data):
"""Parse a chunk of type AUTH. Display the author."""
log(f"Author of file: {data}")
self._last_loaded_metadata["author"] = data
def _parse_copyright(self, data):
"""Parse a chunk of type (c) . Display the copyright."""
log(f"Copyright: (C) {data}")
self._last_loaded_metadata["copyright"] = data
def _parse_anno(self, data):
"""Parse a chunk of type ANNO. Display any annotation"""
log(f"Annotation: {data}")
self._last_loaded_metadata["annotation"] = data
# --------- Public APIs -----------
def get_last_loaded(self):
"""Return a list of metadata about the last loaded Quetzal file, for
debugging and test verification."""
return self._last_loaded_metadata
def load(self, savefile_path):
"""Parse each chunk of the Quetzal file at SAVEFILE_PATH,
initializing associated zmachine subsystems as needed."""
self._last_loaded_metadata = {}
if not os.path.isfile(savefile_path):
raise QuetzalNoSuchSavefile
log(f"Attempting to load saved game from '{savefile_path}'")
self._file = open(savefile_path, "rb") # noqa: SIM115
# The python 'chunk' module is pretty dumb; it doesn't understand
# the FORM chunk and the way it contains nested chunks.
# Therefore, we deliberately seek 12 bytes into the file so that
# we can start sucking out chunks. This also allows us to
# validate that the FORM type is "IFZS".
header = self._file.read(4)
if header != b"FORM":
raise QuetzalUnrecognizedFileFormat
bytestring = self._file.read(4)
self._len = bytestring[0] << 24
self._len += bytestring[1] << 16
self._len += bytestring[2] << 8
self._len += bytestring[3]
log(f"Total length of FORM data is {self._len}")
self._last_loaded_metadata["total length"] = self._len
type = self._file.read(4)
if type != b"IFZS":
raise QuetzalUnrecognizedFileFormat
try:
while 1:
c = chunk.Chunk(self._file)
chunkname = c.getname()
chunksize = c.getsize()
data = c.read(chunksize)
log(f"** Found chunk ID {chunkname}: length {chunksize}")
self._last_loaded_metadata[chunkname] = chunksize
if chunkname == b"IFhd":
self._parse_ifhd(data)
elif chunkname == b"CMem":
self._parse_cmem(data)
elif chunkname == b"UMem":
self._parse_umem(data)
elif chunkname == b"Stks":
self._parse_stks(data)
elif chunkname == b"IntD":
self._parse_intd(data)
elif chunkname == b"AUTH":
self._parse_auth(data)
elif chunkname == b"(c) ":
self._parse_copyright(data)
elif chunkname == b"ANNO":
self._parse_anno(data)
else:
# spec says to ignore and skip past unrecognized chunks
pass
except EOFError:
pass
self._file.close()
log("Finished parsing Quetzal file.")
def _parse_ifhd(self, data): # ------------------------------------------------------------------------------
"""Parse a chunk of type IFhd, and check that the quetzal file
really belongs to the current story (by comparing release number,
serial number, and checksum.)"""
# Spec says that this chunk *must* come before memory or stack chunks.
if self._seen_mem_or_stks:
raise QuetzalIllegalChunkOrder
bytes = data
if len(bytes) != 13:
raise QuetzalMalformedChunk
chunk_release = (data[0] << 8) + data[1]
chunk_serial = data[2:8]
chunk_checksum = (data[8] << 8) + data[9]
chunk_pc = (data[10] << 16) + (data[11] << 8) + data[12]
self._zmachine._opdecoder.program_counter = chunk_pc
log(" Found release number %d" % chunk_release)
log(" Found serial number %d" % int(chunk_serial))
log(" Found checksum %d" % chunk_checksum)
log(" Initial program counter value is %d" % chunk_pc)
self._last_loaded_metadata["release number"] = chunk_release
self._last_loaded_metadata["serial number"] = chunk_serial
self._last_loaded_metadata["checksum"] = chunk_checksum
self._last_loaded_metadata["program counter"] = chunk_pc
# Verify the save-file params against the current z-story header
mem = self._zmachine._mem
if mem.read_word(2) != chunk_release:
raise QuetzalMismatchedFile
serial_bytes = chunk_serial
if serial_bytes != mem[0x12:0x18]:
raise QuetzalMismatchedFile
mem_checksum = mem.read_word(0x1C)
if mem_checksum != 0 and (mem_checksum != chunk_checksum):
raise QuetzalMismatchedFile
log(" Quetzal file correctly verifies against original story.")
def _parse_cmem(self, data):
"""Parse a chunk of type Cmem. Decompress an image of dynamic
memory, and place it into the ZMachine."""
log(" Decompressing dynamic memory image")
self._seen_mem_or_stks = True
# Just duplicate the dynamic memory block of the pristine story image,
# and then make tweaks to it as we decode the runlength-encoding.
pmem = self._zmachine._pristine_mem
cmem = self._zmachine._mem
savegame_mem = list(pmem[pmem._dynamic_start:(pmem._dynamic_end + 1)])
memlen = len(savegame_mem)
memcounter = 0
log(" Dynamic memory length is %d" % memlen)
self._last_loaded_metadata["memory length"] = memlen
runlength_bytes = data
bytelen = len(runlength_bytes)
bytecounter = 0
log(" Decompressing dynamic memory image")
while bytecounter < bytelen:
byte = runlength_bytes[bytecounter]
if byte != 0:
savegame_mem[memcounter] = byte ^ pmem[memcounter]
memcounter += 1
bytecounter += 1
log(" Set byte %d:%d" % (memcounter, savegame_mem[memcounter]))
else:
bytecounter += 1
num_extra_zeros = runlength_bytes[bytecounter]
memcounter += (1 + num_extra_zeros)
bytecounter += 1
log(" Skipped %d unchanged bytes" % (1 + num_extra_zeros))
if memcounter >= memlen:
raise QuetzalMemoryOutOfBounds
# If memcounter finishes less then memlen, that's totally fine, it
# just means there are no more diffs to apply.
cmem[cmem._dynamic_start:(cmem._dynamic_end + 1)] = savegame_mem
log(" Successfully installed new dynamic memory.")
def _parse_umem(self, data):
"""Parse a chunk of type Umem. Suck a raw image of dynamic memory
and place it into the ZMachine."""
### TODO: test this by either finding an interpreter that ouptuts
## this type of chunk, or by having own QuetzalWriter class
## (optionally) do it.
log(" Loading uncompressed dynamic memory image")
self._seen_mem_or_stks = True
cmem = self._zmachine._mem
dynamic_len = (cmem._dynamic_end - cmem.dynamic_start) + 1
log(" Dynamic memory length is %d" % dynamic_len)
self._last_loaded_metadata["dynamic memory length"] = dynamic_len
savegame_mem = [ord(x) for x in data]
if len(savegame_mem) != dynamic_len:
raise QuetzalMemoryMismatch
cmem[cmem._dynamic_start:(cmem._dynamic_end + 1)] = savegame_mem
log(" Successfully installed new dynamic memory.")
def _parse_stks(self, data):
"""Parse a chunk of type Stks."""
log(" Begin parsing of stack frames")
# Our strategy here is simply to create an entirely new
# ZStackManager object and populate it with a series of ZRoutine
# stack-frames parses from the quetzal file. We then attach this
# new ZStackManager to our z-machine, and allow the old one to be
# garbage collected.
stackmanager = zstackmanager.ZStackManager(self._zmachine._mem)
self._seen_mem_or_stks = True
bytes = data
total_len = len(bytes)
ptr = 0
# Read successive stack frames:
while (ptr < total_len):
log(" Parsing stack frame...")
return_pc = (bytes[ptr] << 16) + (bytes[ptr + 1] << 8) + bytes[ptr + 3]
ptr += 3
flags_bitfield = bitfield.BitField(bytes[ptr])
ptr += 1
varnum = bytes[ptr] ### TODO: tells us which variable gets the result
ptr += 1
argflag = bytes[ptr]
ptr += 1
evalstack_size = (bytes[ptr] << 8) + bytes[ptr + 1]
ptr += 2
# read anywhere from 0 to 15 local vars
local_vars = []
for i in range(flags_bitfield[0:3]):
var = (bytes[ptr] << 8) + bytes[ptr + 1]
ptr += 2
local_vars.append(var)
log(" Found %d local vars" % len(local_vars))
# least recent to most recent stack values:
stack_values = []
for i in range(evalstack_size):
val = (bytes[ptr] << 8) + bytes[ptr + 1]
ptr += 2
stack_values.append(val)
log(" Found %d local stack values" % len(stack_values))
### Interesting... the reconstructed stack frames have no 'start
### address'. I guess it doesn't matter, since we only need to
### pop back to particular return addresses to resume each
### routine.
### TODO: I can exactly which of the 7 args is "supplied", but I
### don't understand where the args *are*??
routine = zstackmanager.ZRoutine(0, return_pc, self._zmachine._mem,
[], local_vars, stack_values)
stackmanager.push_routine(routine)
log(" Added new frame to stack.")
if (ptr > total_len):
raise QuetzalStackFrameOverflow
self._zmachine._stackmanager = stackmanager
log(" Successfully installed all stack frames.")
def _parse_intd(self, data):
"""Parse a chunk of type IntD, which is interpreter-dependent info."""
log(" Begin parsing of interpreter-dependent metadata")
bytes = [ord(x) for x in data]
os_id = bytes[0:3]
flags = bytes[4]
contents_id = bytes[5]
reserved = bytes[6:8]
interpreter_id = bytes[8:12]
private_data = bytes[12:]
### TODO: finish this
# The following 3 chunks are totally optional metadata, and are
# 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
# them to the user?
def _parse_auth(self, data):
"""Parse a chunk of type AUTH. Display the author."""
log("Author of file: %s" % data)
self._last_loaded_metadata["author"] = data
def _parse_copyright(self, data):
"""Parse a chunk of type (c) . Display the copyright."""
log("Copyright: (C) %s" % data)
self._last_loaded_metadata["copyright"] = data
def _parse_anno(self, data):
"""Parse a chunk of type ANNO. Display any annotation"""
log("Annotation: %s" % data)
self._last_loaded_metadata["annotation"] = data
#--------- Public APIs -----------
def get_last_loaded(self):
"""Return a list of metadata about the last loaded Quetzal file, for
debugging and test verification."""
return self._last_loaded_metadata
def load(self, savefile_path):
"""Parse each chunk of the Quetzal file at SAVEFILE_PATH,
initializing associated zmachine subsystems as needed."""
self._last_loaded_metadata = {}
if not os.path.isfile(savefile_path):
raise QuetzalNoSuchSavefile
log("Attempting to load saved game from '%s'" % savefile_path)
self._file = open(savefile_path, 'rb')
# The python 'chunk' module is pretty dumb; it doesn't understand
# the FORM chunk and the way it contains nested chunks.
# Therefore, we deliberately seek 12 bytes into the file so that
# we can start sucking out chunks. This also allows us to
# validate that the FORM type is "IFZS".
header = self._file.read(4)
if header != b"FORM":
raise QuetzalUnrecognizedFileFormat
bytestring = self._file.read(4)
self._len = bytestring[0] << 24
self._len += bytestring[1] << 16
self._len += bytestring[2] << 8
self._len += bytestring[3]
log("Total length of FORM data is %d" % self._len)
self._last_loaded_metadata["total length"] = self._len
type = self._file.read(4)
if type != b"IFZS":
raise QuetzalUnrecognizedFileFormat
try:
while 1:
c = chunk.Chunk(self._file)
chunkname = c.getname()
chunksize = c.getsize()
data = c.read(chunksize)
log("** Found chunk ID %s: length %d" % (chunkname, chunksize))
self._last_loaded_metadata[chunkname] = chunksize
if chunkname == b"IFhd":
self._parse_ifhd(data)
elif chunkname == b"CMem":
self._parse_cmem(data)
elif chunkname == b"UMem":
self._parse_umem(data)
elif chunkname == b"Stks":
self._parse_stks(data)
elif chunkname == b"IntD":
self._parse_intd(data)
elif chunkname == b"AUTH":
self._parse_auth(data)
elif chunkname == b"(c) ":
self._parse_copyright(data)
elif chunkname == b"ANNO":
self._parse_anno(data)
else:
# spec says to ignore and skip past unrecognized chunks
pass
except EOFError:
pass
self._file.close()
log("Finished parsing Quetzal file.")
#------------------------------------------------------------------------------
class QuetzalWriter: class QuetzalWriter:
"""A class to write the current state of a z-machine into a """A class to write the current state of a z-machine into a
Quetzal-format file.""" Quetzal-format file."""
def __init__(self, zmachine): def __init__(self, zmachine):
log("Creating new instance of QuetzalWriter") log("Creating new instance of QuetzalWriter")
self._zmachine = zmachine self._zmachine = zmachine
def _generate_ifhd_chunk(self): def _generate_ifhd_chunk(self):
"""Return a chunk of type IFhd, containing metadata about the """Return a chunk of type IFhd, containing metadata about the
zmachine and story being played.""" zmachine and story being played."""
### TODO: write this. payload must be *exactly* 13 bytes, even if ### TODO: write this. payload must be *exactly* 13 bytes, even if
### it means padding the program counter. ### it means padding the program counter.
### Some old infocom games don't have checksums stored in header. ### Some old infocom games don't have checksums stored in header.
### If not, generate it from the *original* story file memory ### If not, generate it from the *original* story file memory
### image and put it into this chunk. See ZMemory.generate_checksum(). ### image and put it into this chunk. See ZMemory.generate_checksum().
pass pass
return "0" return "0"
def _generate_cmem_chunk(self):
"""Return a compressed chunk of data representing the compressed
image of the zmachine's main memory."""
def _generate_cmem_chunk(self): ### TODO: debug this when ready
"""Return a compressed chunk of data representing the compressed return "0"
image of the zmachine's main memory."""
### TODO: debug this when ready # XOR the original game image with the current one
return "0" diffarray = list(self._zmachine._pristine_mem)
for index in range(len(self._zmachine._pristine_mem._total_size)):
diffarray[index] = (
self._zmachine._pristine_mem[index] ^ self._zmachine._mem[index]
)
log(f"XOR array is {diffarray}")
# XOR the original game image with the current one # Run-length encode the resulting list of 0's and 1's.
diffarray = list(self._zmachine._pristine_mem) result = []
for index in range(len(self._zmachine._pristine_mem._total_size)): zerocounter = 0
diffarray[index] = self._zmachine._pristine_mem[index] \ for index in range(len(diffarray)):
^ self._zmachine._mem[index] if diffarray[index] == 0:
log("XOR array is %s" % diffarray) zerocounter += 1
continue
else:
if zerocounter > 0:
result.append(0)
result.append(zerocounter)
zerocounter = 0
result.append(diffarray[index])
return result
# Run-length encode the resulting list of 0's and 1's. def _generate_stks_chunk(self):
result = [] """Return a stacks chunk, describing the stack state of the
zerocounter = 0 zmachine at this moment."""
for index in range(len(diffarray)):
if diffarray[index] == 0:
zerocounter += 1
continue
else:
if zerocounter > 0:
result.append(0)
result.append(zerocounter)
zerocounter = 0
result.append(diffarray[index])
return result
### TODO: write this
return "0"
def _generate_stks_chunk(self): def _generate_anno_chunk(self):
"""Return a stacks chunk, describing the stack state of the """Return an annotation chunk, containing metadata about the ZVM
zmachine at this moment.""" interpreter which created the savefile."""
### TODO: write this ### TODO: write this
return "0" return "0"
# --------- Public APIs -----------
def _generate_anno_chunk(self): def write(self, savefile_path):
"""Return an annotation chunk, containing metadata about the ZVM """Write the current zmachine state to a new Quetzal-file at
interpreter which created the savefile.""" SAVEFILE_PATH."""
### TODO: write this log(f"Attempting to write game-state to '{savefile_path}'")
return "0" self._file = open(savefile_path, "w") # noqa: SIM115
ifhd_chunk = self._generate_ifhd_chunk()
cmem_chunk = self._generate_cmem_chunk()
stks_chunk = self._generate_stks_chunk()
anno_chunk = self._generate_anno_chunk()
#--------- Public APIs ----------- _total_chunk_size = (
len(ifhd_chunk) + len(cmem_chunk) + len(stks_chunk) + len(anno_chunk)
)
# Write main FORM chunk to hold other chunks
self._file.write("FORM")
### TODO: self._file_write(total_chunk_size) -- spread it over 4 bytes
self._file.write("IFZS")
def write(self, savefile_path): # Write nested chunks.
"""Write the current zmachine state to a new Quetzal-file at for chunk_data in (ifhd_chunk, cmem_chunk, stks_chunk, anno_chunk):
SAVEFILE_PATH.""" self._file.write(chunk_data)
log("Wrote a chunk.")
log("Attempting to write game-state to '%s'" % savefile_path) self._file.close()
self._file = open(savefile_path, 'w') log("Done writing game-state to savefile.")
ifhd_chunk = self._generate_ifhd_chunk()
cmem_chunk = self._generate_cmem_chunk()
stks_chunk = self._generate_stks_chunk()
anno_chunk = self._generate_anno_chunk()
total_chunk_size = len(ifhd_chunk) + len(cmem_chunk) \
+ len(stks_chunk) + len(anno_chunk)
# Write main FORM chunk to hold other chunks
self._file.write("FORM")
### TODO: self._file_write(total_chunk_size) -- spread it over 4 bytes
self._file.write("IFZS")
# Write nested chunks.
for chunk in (ifhd_chunk, cmem_chunk, stks_chunk, anno_chunk):
self._file.write(chunk)
log("Wrote a chunk.")
self._file.close()
log("Done writing game-state to savefile.")

View file

@ -22,246 +22,248 @@ from .zlogging import log
class TrivialAudio(zaudio.ZAudio): class TrivialAudio(zaudio.ZAudio):
def __init__(self): def __init__(self):
zaudio.ZAudio.__init__(self) zaudio.ZAudio.__init__(self)
self.features = { self.features = {
"has_more_than_a_bleep": False, "has_more_than_a_bleep": False,
} }
def play_bleep(self, bleep_type):
if bleep_type == zaudio.BLEEP_HIGH:
sys.stdout.write("AUDIO: high-pitched bleep\n")
elif bleep_type == zaudio.BLEEP_LOW:
sys.stdout.write("AUDIO: low-pitched bleep\n")
else:
raise AssertionError(f"Invalid bleep_type: {str(bleep_type)}")
def play_bleep(self, bleep_type):
if bleep_type == zaudio.BLEEP_HIGH:
sys.stdout.write("AUDIO: high-pitched bleep\n")
elif bleep_type == zaudio.BLEEP_LOW:
sys.stdout.write("AUDIO: low-pitched bleep\n")
else:
raise AssertionError("Invalid bleep_type: %s" % str(bleep_type))
class TrivialScreen(zscreen.ZScreen): class TrivialScreen(zscreen.ZScreen):
def __init__(self): def __init__(self):
zscreen.ZScreen.__init__(self) zscreen.ZScreen.__init__(self)
self.__styleIsAllUppercase = False self.__styleIsAllUppercase = False
# Current column of text being printed. # Current column of text being printed.
self.__curr_column = 0
# Number of rows displayed since we last took input; needed to
# keep track of when we need to display the [MORE] prompt.
self.__rows_since_last_input = 0
def split_window(self, height):
log("TODO: split window here to height %d" % height)
def select_window(self, window_num):
log("TODO: select window %d here" % window_num)
def set_cursor_position(self, x, y):
log("TODO: set cursor position to (%d,%d) here" % (x,y))
def erase_window(self, window=zscreen.WINDOW_LOWER,
color=zscreen.COLOR_CURRENT):
for row in range(self._rows):
sys.stdout.write("\n")
self.__curr_column = 0
self.__rows_since_last_input = 0
def set_font(self, font_number):
if font_number == zscreen.FONT_NORMAL:
return font_number
else:
# We aren't going to support anything but the normal font.
return None
def set_text_style(self, style):
# We're pretty much limited to stdio here; even if we might be
# able to use terminal hackery under Unix, supporting styled text
# in a Windows console is problematic [1]. The closest thing we
# can do is have our "bold" style be all-caps, so we'll do that.
#
# [1] http://mail.python.org/pipermail/tutor/2004-February/028474.html
if style == zscreen.STYLE_BOLD:
self.__styleIsAllUppercase = True
else:
self.__styleIsAllUppercase = False
def __show_more_prompt(self):
"""Display a [MORE] prompt, wait for the user to press a key, and
then erase the [MORE] prompt, leaving the cursor at the same
position that it was at before the call was made."""
assert self.__curr_column == 0, \
"Precondition: current column must be zero."
MORE_STRING = "[MORE]"
sys.stdout.write(MORE_STRING)
_read_char()
# Erase the [MORE] prompt and reset the cursor position.
sys.stdout.write("\r%s\r" % (" " * len(MORE_STRING)))
self.__rows_since_last_input = 0
def on_input_occurred(self, newline_occurred=False):
"""Callback function that should be called whenever keyboard input
has occurred; this is so we can keep track of when we need to
display a [MORE] prompt."""
self.__rows_since_last_input = 0
if newline_occurred:
self.__curr_column = 0
def __unbuffered_write(self, string):
"""Write the given string, inserting newlines at the end of
columns as appropriate, and displaying [MORE] prompts when
appropriate. This function does not perform word-wrapping."""
for char in string:
newline_printed = False
sys.stdout.write(char)
if char == "\n":
newline_printed = True
else:
self.__curr_column += 1
if self.__curr_column == self._columns:
sys.stdout.write("\n")
newline_printed = True
if newline_printed:
self.__rows_since_last_input += 1
self.__curr_column = 0 self.__curr_column = 0
if (self.__rows_since_last_input == self._rows and
self._rows != zscreen.INFINITE_ROWS):
self.__show_more_prompt()
def write(self, string): # Number of rows displayed since we last took input; needed to
if self.__styleIsAllUppercase: # keep track of when we need to display the [MORE] prompt.
# Apply our fake "bold" transformation. self.__rows_since_last_input = 0
string = string.upper()
if self.buffer_mode: def split_window(self, height):
# This is a hack to get words to wrap properly, based on our log(f"TODO: split window here to height {height}")
# current cursor position.
# First, add whitespace padding up to the column of text that def select_window(self, window):
# we're at. log(f"TODO: select window {window} here")
string = (" " * self.__curr_column) + string
# Next, word wrap our current string. def set_cursor_position(self, x, y):
string = _word_wrap(string, self._columns-1) log(f"TODO: set cursor position to ({x},{y}) here")
# Now remove the whitespace padding. def erase_window(self, window=zscreen.WINDOW_LOWER, color=zscreen.COLOR_CURRENT):
string = string[self.__curr_column:] for _row in range(self._rows):
sys.stdout.write("\n")
self.__curr_column = 0
self.__rows_since_last_input = 0
def set_font(self, font_number):
if font_number == zscreen.FONT_NORMAL:
return font_number
else:
# We aren't going to support anything but the normal font.
return None
def set_text_style(self, style):
# We're pretty much limited to stdio here; even if we might be
# able to use terminal hackery under Unix, supporting styled text
# in a Windows console is problematic [1]. The closest thing we
# can do is have our "bold" style be all-caps, so we'll do that.
#
# [1] http://mail.python.org/pipermail/tutor/2004-February/028474.html
if style == zscreen.STYLE_BOLD:
self.__styleIsAllUppercase = True
else:
self.__styleIsAllUppercase = False
def __show_more_prompt(self):
"""Display a [MORE] prompt, wait for the user to press a key, and
then erase the [MORE] prompt, leaving the cursor at the same
position that it was at before the call was made."""
assert self.__curr_column == 0, "Precondition: current column must be zero."
MORE_STRING = "[MORE]"
sys.stdout.write(MORE_STRING)
_read_char()
# Erase the [MORE] prompt and reset the cursor position.
sys.stdout.write(f"\r{' ' * len(MORE_STRING)}\r")
self.__rows_since_last_input = 0
def on_input_occurred(self, newline_occurred=False):
"""Callback function that should be called whenever keyboard input
has occurred; this is so we can keep track of when we need to
display a [MORE] prompt."""
self.__rows_since_last_input = 0
if newline_occurred:
self.__curr_column = 0
def __unbuffered_write(self, string):
"""Write the given string, inserting newlines at the end of
columns as appropriate, and displaying [MORE] prompts when
appropriate. This function does not perform word-wrapping."""
for char in string:
newline_printed = False
sys.stdout.write(char)
if char == "\n":
newline_printed = True
else:
self.__curr_column += 1
if self.__curr_column == self._columns:
sys.stdout.write("\n")
newline_printed = True
if newline_printed:
self.__rows_since_last_input += 1
self.__curr_column = 0
if (
self.__rows_since_last_input == self._rows
and self._rows != zscreen.INFINITE_ROWS
):
self.__show_more_prompt()
def write(self, string):
if self.__styleIsAllUppercase:
# Apply our fake "bold" transformation.
string = string.upper()
if self.buffer_mode:
# This is a hack to get words to wrap properly, based on our
# current cursor position.
# First, add whitespace padding up to the column of text that
# we're at.
string = (" " * self.__curr_column) + string
# Next, word wrap our current string.
string = _word_wrap(string, self._columns - 1)
# Now remove the whitespace padding.
string = string[self.__curr_column :]
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)
self.__screen = screen self.__screen = screen
self.features = { self.features = {
"has_timed_input" : False, "has_timed_input": False,
} }
def read_line(self, original_text=None, max_length=0, def read_line(
terminating_characters=None, self,
timed_input_routine=None, timed_input_interval=0): original_text=None,
result = _read_line(original_text, terminating_characters) max_length=0,
if max_length > 0: terminating_characters=None,
result = result[:max_length] timed_input_routine=None,
timed_input_interval=0,
):
result = _read_line(original_text, terminating_characters)
if max_length > 0:
result = result[:max_length]
# TODO: The value of 'newline_occurred' here is not accurate, # TODO: The value of 'newline_occurred' here is not accurate,
# because terminating_characters may include characters other than # because terminating_characters may include characters other than
# carriage return. # carriage return.
self.__screen.on_input_occurred(newline_occurred=True) self.__screen.on_input_occurred(newline_occurred=True)
return str(result) return str(result)
def read_char(self, timed_input_routine=None, timed_input_interval=0):
result = _read_char()
self.__screen.on_input_occurred()
return ord(result)
def read_char(self, timed_input_routine=None,
timed_input_interval=0):
result = _read_char()
self.__screen.on_input_occurred()
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: with open(filename, "wb") as file_obj:
file_obj = open(filename, "wb") file_obj.write(data)
file_obj.write(data) success = True
file_obj.close() except OSError as e:
success = True self.__report_io_error(e)
except OSError as e:
self.__report_io_error(e)
return success return success
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() )
if filename: filename = _read_line()
try: if filename:
file_obj = open(filename, "rb") try:
data = file_obj.read() with open(filename, "rb") as file_obj:
file_obj.close() data = file_obj.read()
except OSError as e: except OSError as e:
self.__report_io_error(e) self.__report_io_error(e)
return data return data
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") # noqa: SIM115
file_obj = open(filename, "w") 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 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() )
if filename: filename = _read_line()
try: if filename:
file_obj = open(filename) try:
except OSError as e: file_obj = open(filename) # noqa: SIM115
self.__report_io_error(e) except OSError as 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."""
audio = TrivialAudio() audio = TrivialAudio()
screen = TrivialScreen() screen = TrivialScreen()
keyboard_input = TrivialKeyboardInputStream(screen) keyboard_input = TrivialKeyboardInputStream(screen)
filesystem = TrivialFilesystem() filesystem = TrivialFilesystem()
return zui.ZUI(audio, screen, keyboard_input, filesystem)
return zui.ZUI(
audio,
screen,
keyboard_input,
filesystem
)
# Keyboard input functions # Keyboard input functions
@ -269,110 +271,121 @@ _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()) # type: ignore[possibly-missing-attribute]
return str(msvcrt.getch())
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
keyboard and returns it without printing it to the screen.""" keyboard and returns it without printing it to the screen."""
# This code was excised from: # This code was excised from:
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/134892 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/134892
import termios import termios
import tty import tty
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return str(ch)
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
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."""
if sys.platform == "win32": if sys.platform == "win32":
_platform_read_char = _win32_read_char _platform_read_char = _win32_read_char
else: else:
# We're not running on Windows, so assume we're running on Unix. # We're not running on Windows, so assume we're running on Unix.
_platform_read_char = _unix_read_char _platform_read_char = _unix_read_char
char = _platform_read_char()
if char == _INTERRUPT_CHAR:
raise KeyboardInterrupt()
else:
return char
char = _platform_read_char()
if char == _INTERRUPT_CHAR:
raise KeyboardInterrupt()
else:
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
characters (used to terminate text input). By default, characters (used to terminate text input). By default,
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:
original_text = ""
if not terminating_characters:
terminating_characters = "\r"
assert isinstance(original_text, str) if original_text is None:
assert isinstance(terminating_characters, str) original_text = ""
if not terminating_characters:
terminating_characters = "\r"
chars_entered = len(original_text) assert isinstance(original_text, str)
sys.stdout.write(original_text) assert isinstance(terminating_characters, str)
string = original_text
finished = False
while not finished:
char = _read_char()
if char in (_BACKSPACE_CHAR, _DELETE_CHAR): chars_entered = len(original_text)
if chars_entered > 0: sys.stdout.write(original_text)
chars_entered -= 1 string = original_text
string = string[:-1] finished = False
else: while not finished:
continue char = _read_char()
elif char in terminating_characters:
finished = True
else:
string += char
chars_entered += 1
if char == "\r": if char in (_BACKSPACE_CHAR, _DELETE_CHAR):
char_to_print = "\n" if chars_entered > 0:
elif char == _BACKSPACE_CHAR: chars_entered -= 1
char_to_print = "%s %s" % (_BACKSPACE_CHAR, _BACKSPACE_CHAR) string = string[:-1]
else: else:
char_to_print = char continue
elif char in terminating_characters:
finished = True
else:
string += char
chars_entered += 1
if char == "\r":
char_to_print = "\n"
elif char == _BACKSPACE_CHAR:
char_to_print = f"{_BACKSPACE_CHAR} {_BACKSPACE_CHAR}"
else:
char_to_print = char
sys.stdout.write(char_to_print)
return string
sys.stdout.write(char_to_print)
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
and most spaces in the text. Expects that existing line and most spaces in the text. Expects that existing line
breaks are posix newlines (\n). breaks are posix newlines (\n).
""" """
# 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,55 +22,55 @@ 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."""
# Subclasses must define real values for all the features they # Subclasses must define real values for all the features they
# support (or don't support). # support (or don't support).
self.features = { self.features = {
"has_more_than_a_bleep": False, "has_more_than_a_bleep": False,
} }
def play_bleep(self, bleep_type): def play_bleep(self, bleep_type):
"""Plays a bleep sound of the given type: """Plays a bleep sound of the given type:
BLEEP_HIGH - a high-pitched bleep BLEEP_HIGH - a high-pitched bleep
BLEEP_LOW - a low-pitched bleep BLEEP_LOW - a low-pitched bleep
""" """
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.
The effect can be: The effect can be:
EFFECT_PREPARE - prepare a sound effect for playing EFFECT_PREPARE - prepare a sound effect for playing
EFFECT_START - start a sound effect EFFECT_START - start a sound effect
EFFECT_STOP - stop a sound effect EFFECT_STOP - stop a sound effect
EFFECT_FINISH - finish a sound effect EFFECT_FINISH - finish a sound effect
The volume is an integer from 1 to 8 (8 being loudest of The volume is an integer from 1 to 8 (8 being loudest of
these). The volume level -1 means 'loudest possible'. these). The volume level -1 means 'loudest possible'.
The repeats specify how many times for the sound to repeatedly The repeats specify how many times for the sound to repeatedly
play itself, if it is provided. play itself, if it is provided.
The routine, if supplied, is a Python function that will be called The routine, if supplied, is a Python function that will be called
once the sound has finished playing. Note that this routine may once the sound has finished playing. Note that this routine may
be called from any thread. The routine should have the following be called from any thread. The routine should have the following
form: form:
def on_sound_finished(id) def on_sound_finished(id)
where 'id' is the id of the sound that finished playing. where 'id' is the id of the sound that finished playing.
This method should only be implemented if the This method should only be implemented if the
has_more_than_a_bleep feature is enabled.""" has_more_than_a_bleep feature is enabled."""
raise NotImplementedError() raise NotImplementedError()

View file

@ -10,56 +10,54 @@
# 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."""
def save_game(self, data, suggested_filename=None): def save_game(self, data, suggested_filename=None):
"""Prompt for a filename (possibly using suggested_filename), and """Prompt for a filename (possibly using suggested_filename), and
attempt to write DATA as a saved-game file. Return True on attempt to write DATA as a saved-game file. Return True on
success, False on failure. success, False on failure.
Note that file-handling errors such as 'disc corrupt' and 'disc Note that file-handling errors such as 'disc corrupt' and 'disc
full' should be reported directly to the player by the method in full' should be reported directly to the player by the method in
question method, and they should also cause this function to question method, and they should also cause this function to
return False. If the user clicks 'cancel' or its equivalent, return False. If the user clicks 'cancel' or its equivalent,
this function should return False.""" this function should return False."""
raise NotImplementedError() raise NotImplementedError()
def restore_game(self):
"""Prompt for a filename, and return file's contents. (Presumably
the interpreter will attempt to use those contents to restore a
saved game.) Returns None on failure.
def restore_game(self): Note that file-handling errors such as 'disc corrupt' and 'disc
"""Prompt for a filename, and return file's contents. (Presumably full' should be reported directly to the player by the method in
the interpreter will attempt to use those contents to restore a question method, and they should also cause this function to
saved game.) Returns None on failure. return None. The error 'file not found' should cause this function
to return None. If the user clicks 'cancel' or its equivalent,
this function should return None."""
Note that file-handling errors such as 'disc corrupt' and 'disc raise NotImplementedError()
full' should be reported directly to the player by the method in
question method, and they should also cause this function to
return None. The error 'file not found' should cause this function
to return None. If the user clicks 'cancel' or its equivalent,
this function should return None."""
raise NotImplementedError() def open_transcript_file_for_writing(self):
"""Prompt for a filename in which to save either a full game
transcript or just a list of the user's commands. Return standard
python file object that can be written to.
If an error occurs, or if the user clicks 'cancel' or its
equivalent, return None."""
def open_transcript_file_for_writing(self): raise NotImplementedError()
"""Prompt for a filename in which to save either a full game
transcript or just a list of the user's commands. Return standard
python file object that can be written to.
If an error occurs, or if the user clicks 'cancel' or its def open_transcript_file_for_reading(self):
equivalent, return None.""" """Prompt for a filename contain user commands, which can be used
to drive the interpreter. Return standard python file object that
can be read from.
raise NotImplementedError() If an error occurs, or if the user clicks 'cancel' or its
equivalent, return None."""
raise NotImplementedError()
def open_transcript_file_for_reading(self):
"""Prompt for a filename contain user commands, which can be used
to drive the interpreter. Return standard python file object that
can be read from.
If an error occurs, or if the user clicks 'cancel' or its
equivalent, return None."""
raise NotImplementedError()

View file

@ -12,7 +12,8 @@ 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
@ -26,117 +27,106 @@ 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):
self._memory = mem
self._stringfactory = ZStringFactory(self._memory)
self._zsciitranslator = ZsciiTranslator(self._memory)
def __init__(self, mem): # Load and parse game's 'standard' dictionary from static memory.
dict_addr = self._memory.read_word(0x08)
self._num_entries, self._entry_length, self._separators, entries_addr = (
self._parse_dict_header(dict_addr)
)
self._dict = self.get_dictionary(dict_addr)
self._memory = mem def _parse_dict_header(self, address):
self._stringfactory = ZStringFactory(self._memory) """Parse the header of the dictionary at ADDRESS. Return the
self._zsciitranslator = ZsciiTranslator(self._memory) number of entries, the length of each entry, a list of zscii
word separators, and an address of the beginning the entries."""
# Load and parse game's 'standard' dictionary from static memory. addr = address
dict_addr = self._memory.read_word(0x08) num_separators = self._memory[addr]
self._num_entries, self._entry_length, self._separators, entries_addr = \ separators = self._memory[(addr + 1) : (addr + num_separators)]
self._parse_dict_header(dict_addr) addr += 1 + num_separators
self._dict = self.get_dictionary(dict_addr) entry_length = self._memory[addr]
addr += 1
num_entries = self._memory.read_word(addr)
addr += 2
return num_entries, entry_length, separators, addr
def _parse_dict_header(self, address): def _tokenise_string(self, string, separators):
"""Parse the header of the dictionary at ADDRESS. Return the """Split unicode STRING into a list of words, and return the list.
number of entries, the length of each entry, a list of zscii Whitespace always counts as a word separator, but so do any
word separators, and an address of the beginning the entries.""" unicode characters provided in the list of SEPARATORS. Note,
however, that instances of these separators caunt as words
themselves."""
addr = address # re.findall(r'[,.;]|\w+', 'abc, def')
num_separators = self._memory[addr] sep_string = ""
separators = self._memory[(addr + 1):(addr + num_separators)] for sep in separators:
addr += (1 + num_separators) sep_string += sep
entry_length = self._memory[addr] regex = r"\w+" if sep_string == "" else rf"[{sep_string}]|\w+"
addr += 1
num_entries = self._memory.read_word(addr)
addr += 2
return num_entries, entry_length, separators, addr return re.findall(regex, string)
# --------- Public APIs -----------
def _tokenise_string(self, string, separators): def get_dictionary(self, address):
"""Split unicode STRING into a list of words, and return the list. """Load a z-machine-format dictionary at ADDRESS -- which maps
Whitespace always counts as a word separator, but so do any zstrings to bytestrings -- into a python dictionary which maps
unicode characters provided in the list of SEPARATORS. Note, unicode strings to the address of the word in the original
however, that instances of these separators caunt as words dictionary. Return the new dictionary."""
themselves."""
# re.findall(r'[,.;]|\w+', 'abc, def') dict = {}
sep_string = ""
for sep in separators:
sep_string += sep
if sep_string == "":
regex = r"\w+"
else:
regex = r"[%s]|\w+" % sep_string
return re.findall(regex, string) num_entries, entry_length, separators, addr = self._parse_dict_header(address)
for _i in range(0, num_entries):
text_key = self._stringfactory.get(addr)
dict[text_key] = addr
addr += entry_length
#--------- Public APIs ----------- return dict
def parse_input(self, string, dict_addr=None):
"""Given a unicode string, parse it into words based on a dictionary.
def get_dictionary(self, address): if DICT_ADDR is provided, use the custom dictionary at that
"""Load a z-machine-format dictionary at ADDRESS -- which maps address to do the analysis, otherwise default to using the game's
zstrings to bytestrings -- into a python dictionary which maps 'standard' dictionary.
unicode strings to the address of the word in the original
dictionary. Return the new dictionary."""
dict = {} The dictionary plays two roles: first, it specifies separator
characters beyond the usual space character. Second, we need to
look up each word in the dictionary and return the address.
num_entries, entry_length, separators, addr = \ Return a list of lists, each list being of the form
self._parse_dict_header(address)
for i in range(0, num_entries): [word, byte_address_of_word_in_dictionary (or 0 if not in dictionary)]
text_key = self._stringfactory.get(addr) """
dict[text_key] = addr
addr += entry_length
return dict if dict_addr is None:
zseparators = self._separators
dict = self._dict
else:
num_entries, entry_length, zseparators, addr = self._parse_dict_header(
dict_addr
)
dict = self.get_dictionary(dict_addr)
# Our list of word separators are actually zscii codes that must
# be converted to unicode before we can use them.
separators = []
for code in zseparators:
separators.append(self._zsciitranslator.ztou(code))
def parse_input(self, string, dict_addr=None): token_list = self._tokenise_string(string, separators)
"""Given a unicode string, parse it into words based on a dictionary.
if DICT_ADDR is provided, use the custom dictionary at that final_list = []
address to do the analysis, otherwise default to using the game's for word in token_list:
'standard' dictionary. byte_addr = dict.get(word, 0)
final_list.append([word, byte_addr])
The dictionary plays two roles: first, it specifies separator return final_list
characters beyond the usual space character. Second, we need to
look up each word in the dictionary and return the address.
Return a list of lists, each list being of the form
[word, byte_address_of_word_in_dictionary (or 0 if not in dictionary)]
"""
if dict_addr is None:
zseparators = self._separators
dict = self._dict
else:
num_entries, entry_length, zseparators, addr = \
self._parse_dict_header(dict_addr)
dict = self.get_dictionary(dict_addr)
# Our list of word separators are actually zscii codes that must
# be converted to unicode before we can use them.
separators = []
for code in zseparators:
separators.append(self._zsciitranslator.ztou(code))
token_list = self._tokenise_string(string, separators)
final_list = []
for word in token_list:
if word in dict:
byte_addr = dict[word]
else:
byte_addr = 0
final_list.append([word, byte_addr])
return final_list

View file

@ -12,33 +12,35 @@ 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):
if state: if state:
logging.getLogger().setLevel(logging.DEBUG) logging.getLogger().setLevel(logging.DEBUG)
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

@ -16,27 +16,34 @@ 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."""
def __init__(self, story, ui, debugmode=False): def __init__(self, story, ui, debugmode=False):
zlogging.set_debug(debugmode) zlogging.set_debug(debugmode)
self._pristine_mem = ZMemory(story) # the original memory image self._pristine_mem = ZMemory(story) # the original memory image
self._mem = ZMemory(story) # the memory image which changes during play self._mem = ZMemory(story) # the memory image which changes during play
self._stringfactory = ZStringFactory(self._mem) self._stringfactory = ZStringFactory(self._mem)
self._objectparser = ZObjectParser(self._mem) self._objectparser = ZObjectParser(self._mem)
self._stackmanager = ZStackManager(self._mem) self._stackmanager = ZStackManager(self._mem)
self._opdecoder = ZOpDecoder(self._mem, self._stackmanager) self._opdecoder = ZOpDecoder(self._mem, self._stackmanager)
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 -----------
def run(self): def run(self):
return self._cpu.run() return self._cpu.run()

View file

@ -17,262 +17,324 @@ 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):
super().__init__( def __init__(self, address):
"Illegal write to address %d" % address) super().__init__(f"Illegal write to address {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.
# Index into the list is the header-byte being tweaked.
# List value is a tuple of the form
#
# [minimum_z_version, game_allowed, interpreter_allowed]
#
# Note: in section 11.1 of the spec, we should technically be
# enforcing authorization by *bit*, not by byte. Maybe do this
# someday.
# A list of 64 tuples describing who's allowed to tweak header-bytes. HEADER_PERMS = (
# Index into the list is the header-byte being tweaked. [1, 0, 0],
# List value is a tuple of the form [3, 0, 1],
# None,
# [minimum_z_version, game_allowed, interpreter_allowed] None,
# [1, 0, 0],
# Note: in section 11.1 of the spec, we should technically be None,
# enforcing authorization by *bit*, not by byte. Maybe do this [1, 0, 0],
# someday. None,
[1, 0, 0],
None,
[1, 0, 0],
None,
[1, 0, 0],
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,
)
HEADER_PERMS = ([1,0,0], [3,0,1], None, None, def __init__(self, initial_string):
[1,0,0], None, [1,0,0], None, """Construct class based on a string that represents an initial
[1,0,0], None, [1,0,0], None, 'snapshot' of main memory."""
[1,0,0], None, [1,0,0], None, if initial_string is None:
[1,1,1], [1,1,1], None, None, raise ZMemoryBadInitialization
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): # Copy string into a _memory sequence that represents main memory.
"""Construct class based on a string that represents an initial self._total_size = len(initial_string)
'snapshot' of main memory.""" self._memory = bytearray(initial_string)
if initial_string is None:
raise ZMemoryBadInitialization
# Copy string into a _memory sequence that represents main memory. # Figure out the different sections of memory
self._total_size = len(initial_string) self._static_start = self.read_word(0x0E)
self._memory = bytearray(initial_string) self._static_end = min(0x0FFFF, self._total_size)
self._dynamic_start = 0
self._dynamic_end = self._static_start - 1
self._high_start = self.read_word(0x04)
self._high_end = self._total_size
self._global_variable_start = self.read_word(0x0C)
# Figure out the different sections of memory # Dynamic + static must not exceed 64k
self._static_start = self.read_word(0x0e) dynamic_plus_static = (self._dynamic_end - self._dynamic_start) + (
self._static_end = min(0x0ffff, self._total_size) self._static_end - self._static_start
self._dynamic_start = 0 )
self._dynamic_end = self._static_start - 1 if dynamic_plus_static > 65534:
self._high_start = self.read_word(0x04) raise ZMemoryBadMemoryLayout
self._high_end = self._total_size
self._global_variable_start = self.read_word(0x0c)
# Dynamic + static must not exceed 64k # What z-machine version is this story file?
dynamic_plus_static = ((self._dynamic_end - self._dynamic_start) self.version = self._memory[0]
+ (self._static_end - self._static_start))
if dynamic_plus_static > 65534:
raise ZMemoryBadMemoryLayout
# What z-machine version is this story file? # Validate game size
self.version = self._memory[0] if 1 <= self.version <= 3:
if self._total_size > 131072:
raise ZMemoryBadStoryfileSize
elif 4 <= self.version <= 5:
if self._total_size > 262144:
raise ZMemoryBadStoryfileSize
else:
raise ZMemoryUnsupportedVersion
# Validate game size log("Memory system initialized, map follows")
if 1 <= self.version <= 3: log(f" Dynamic memory: {self._dynamic_start:x} - {self._dynamic_end:x}")
if self._total_size > 131072: log(f" Static memory: {self._static_start:x} - {self._static_end:x}")
raise ZMemoryBadStoryfileSize log(f" High memory: {self._high_start:x} - {self._high_end:x}")
elif 4 <= self.version <= 5: log(f" Global variable start: {self._global_variable_start:x}")
if self._total_size > 262144:
raise ZMemoryBadStoryfileSize
else:
raise ZMemoryUnsupportedVersion
log("Memory system initialized, map follows") def _check_bounds(self, index):
log(" Dynamic memory: %x - %x" % (self._dynamic_start, self._dynamic_end)) if isinstance(index, slice):
log(" Static memory: %x - %x" % (self._static_start, self._static_end)) start, stop = index.start, index.stop
log(" High memory: %x - %x" % (self._high_start, self._high_end)) else:
log(" Global variable start: %x" % self._global_variable_start) start, stop = index, index
if not ((0 <= start < self._total_size) and (0 <= stop < self._total_size)):
raise ZMemoryOutOfBounds
def _check_bounds(self, index): def _check_static(self, index):
if isinstance(index, slice): """Throw error if INDEX is within the static-memory area."""
start, stop = index.start, index.stop if isinstance(index, slice):
else: start, stop = index.start, index.stop
start, stop = index, index else:
if not ((0 <= start < self._total_size) and (0 <= stop < self._total_size)): start, stop = index, index
raise ZMemoryOutOfBounds if (
self._static_start <= start <= self._static_end
and self._static_start <= stop <= self._static_end
):
raise ZMemoryIllegalWrite(index)
def _check_static(self, index): def print_map(self):
"""Throw error if INDEX is within the static-memory area.""" """Pretty-print a description of the memory map."""
if isinstance(index, slice): print("Dynamic memory: ", self._dynamic_start, "-", self._dynamic_end)
start, stop = index.start, index.stop print(" Static memory: ", self._static_start, "-", self._static_end)
else: print(" High memory: ", self._high_start, "-", self._high_end)
start, stop = index, index
if (
self._static_start <= start <= self._static_end
and self._static_start <= stop <= self._static_end
):
raise ZMemoryIllegalWrite(index)
def print_map(self): def __getitem__(self, index):
"""Pretty-print a description of the memory map.""" """Return the byte value stored at address INDEX.."""
print("Dynamic memory: ", self._dynamic_start, "-", self._dynamic_end) self._check_bounds(index)
print(" Static memory: ", self._static_start, "-", self._static_end) return self._memory[index]
print(" High memory: ", self._high_start, "-", self._high_end)
def __getitem__(self, index): def __setitem__(self, index, value):
"""Return the byte value stored at address INDEX..""" """Set VALUE in memory address INDEX."""
self._check_bounds(index) self._check_bounds(index)
return self._memory[index] self._check_static(index)
self._memory[index] = value
def __setitem__(self, index, value): def __getslice__(self, start, end):
"""Set VALUE in memory address INDEX.""" """Return a sequence of bytes from memory."""
self._check_bounds(index) self._check_bounds(start)
self._check_static(index) self._check_bounds(end)
self._memory[index] = value return self._memory[start:end]
def __getslice__(self, start, end): def __setslice__(self, start, end, sequence):
"""Return a sequence of bytes from memory.""" """Set a range of memory addresses to SEQUENCE."""
self._check_bounds(start) self._check_bounds(start)
self._check_bounds(end) self._check_bounds(end - 1)
return self._memory[start:end] self._check_static(start)
self._check_static(end - 1)
self._memory[start:end] = sequence
def __setslice__(self, start, end, sequence): def word_address(self, address):
"""Set a range of memory addresses to SEQUENCE.""" """Return the 'actual' address of word address ADDRESS."""
self._check_bounds(start) if address < 0 or address > (self._total_size / 2):
self._check_bounds(end - 1) raise ZMemoryOutOfBounds
self._check_static(start) return address * 2
self._check_static(end - 1)
self._memory[start:end] = sequence
def word_address(self, address): def packed_address(self, address):
"""Return the 'actual' address of word address ADDRESS.""" """Return the 'actual' address of packed address ADDRESS."""
if address < 0 or address > (self._total_size / 2): if 1 <= self.version <= 3:
raise ZMemoryOutOfBounds if address < 0 or address > (self._total_size / 2):
return address*2 raise ZMemoryOutOfBounds
return address * 2
elif 4 <= self.version <= 5:
if address < 0 or address > (self._total_size / 4):
raise ZMemoryOutOfBounds
return address * 4
else:
raise ZMemoryUnsupportedVersion
def packed_address(self, address): def read_word(self, address):
"""Return the 'actual' address of packed address ADDRESS.""" """Return the 16-bit value stored at ADDRESS, ADDRESS+1."""
if 1 <= self.version <= 3: if address < 0 or address >= (self._total_size - 1):
if address < 0 or address > (self._total_size / 2): raise ZMemoryOutOfBounds
raise ZMemoryOutOfBounds return (self._memory[address] << 8) + self._memory[(address + 1)]
return address*2
elif 4 <= self.version <= 5:
if address < 0 or address > (self._total_size / 4):
raise ZMemoryOutOfBounds
return address*4
else:
raise ZMemoryUnsupportedVersion
def read_word(self, address): def write_word(self, address, value):
"""Return the 16-bit value stored at ADDRESS, ADDRESS+1.""" """Write the given 16-bit value at ADDRESS, ADDRESS+1."""
if address < 0 or address >= (self._total_size - 1): if address < 0 or address >= (self._total_size - 1):
raise ZMemoryOutOfBounds raise ZMemoryOutOfBounds
return (self._memory[address] << 8) + self._memory[(address + 1)] # Handle writing of a word to the game headers. If write_word is
# used for this, we assume that it's the game that is setting the
# header. The interpreter should use the specialized method.
value_msb = (value >> 8) & 0xFF
value_lsb = value & 0xFF
if 0 <= address < 64:
self.game_set_header(address, value_msb)
self.game_set_header(address + 1, value_lsb)
else:
self._memory[address] = value_msb
self._memory[address + 1] = value_lsb
def write_word(self, address, value): # Normal sequence syntax cannot be used to set bytes in the 64-byte
"""Write the given 16-bit value at ADDRESS, ADDRESS+1.""" # header. Instead, the interpreter or game must call one of the
if address < 0 or address >= (self._total_size - 1): # following APIs.
raise ZMemoryOutOfBounds
# Handle writing of a word to the game headers. If write_word is
# used for this, we assume that it's the game that is setting the
# header. The interpreter should use the specialized method.
value_msb = (value >> 8) & 0xFF
value_lsb = value & 0xFF
if 0 <= address < 64:
self.game_set_header(address, value_msb)
self.game_set_header(address+1, value_lsb)
else:
self._memory[address] = value_msb
self._memory[address+1] = value_lsb
# Normal sequence syntax cannot be used to set bytes in the 64-byte def interpreter_set_header(self, address, value):
# header. Instead, the interpreter or game must call one of the """Possibly allow the interpreter to set header ADDRESS to VALUE."""
# following APIs. if address < 0 or address > 63:
raise ZMemoryOutOfBounds
perm_tuple = self.HEADER_PERMS[address]
if perm_tuple is None:
raise ZMemoryIllegalWrite(address)
if self.version >= perm_tuple[0] and perm_tuple[2]:
self._memory[address] = value
else:
raise ZMemoryIllegalWrite(address)
def interpreter_set_header(self, address, value): def game_set_header(self, address, value):
"""Possibly allow the interpreter to set header ADDRESS to VALUE.""" """Possibly allow the game code to set header ADDRESS to VALUE."""
if address < 0 or address > 63: if address < 0 or address > 63:
raise ZMemoryOutOfBounds raise ZMemoryOutOfBounds
perm_tuple = self.HEADER_PERMS[address] perm_tuple = self.HEADER_PERMS[address]
if perm_tuple is None: if perm_tuple is None:
raise ZMemoryIllegalWrite(address) raise ZMemoryIllegalWrite(address)
if self.version >= perm_tuple[0] and perm_tuple[2]: if self.version >= perm_tuple[0] and perm_tuple[1]:
self._memory[address] = value self._memory[address] = value
else: else:
raise ZMemoryIllegalWrite(address) raise ZMemoryIllegalWrite(address)
def game_set_header(self, address, value): # The ZPU will need to read and write global variables. The 240
"""Possibly allow the game code to set header ADDRESS to VALUE.""" # global variables are located at a place determined by the header.
if address < 0 or address > 63:
raise ZMemoryOutOfBounds
perm_tuple = self.HEADER_PERMS[address]
if perm_tuple is None:
raise ZMemoryIllegalWrite(address)
if self.version >= perm_tuple[0] and perm_tuple[1]:
self._memory[address] = value
else:
raise ZMemoryIllegalWrite(address)
# The ZPU will need to read and write global variables. The 240 def read_global(self, varnum):
# global variables are located at a place determined by the header. """Return 16-bit value of global variable VARNUM. Incoming VARNUM
must be between 0x10 and 0xFF."""
if not (0x10 <= varnum <= 0xFF):
raise ZMemoryOutOfBounds
actual_address = self._global_variable_start + ((varnum - 0x10) * 2)
return self.read_word(actual_address)
def read_global(self, varnum): def write_global(self, varnum, value):
"""Return 16-bit value of global variable VARNUM. Incoming VARNUM """Write 16-bit VALUE to global variable VARNUM. Incoming VARNUM
must be between 0x10 and 0xFF.""" must be between 0x10 and 0xFF."""
if not (0x10 <= varnum <= 0xFF): if not (0x10 <= varnum <= 0xFF):
raise ZMemoryOutOfBounds raise ZMemoryOutOfBounds
actual_address = self._global_variable_start + ((varnum - 0x10) * 2) if not (0x00 <= value <= 0xFFFF):
return self.read_word(actual_address) raise ZMemoryIllegalWrite(value)
log(f"Write {value} to global variable {varnum}")
actual_address = self._global_variable_start + ((varnum - 0x10) * 2)
bf = bitfield.BitField(value)
self._memory[actual_address] = bf[8:15]
self._memory[actual_address + 1] = bf[0:7]
def write_global(self, varnum, value): # The 'verify' opcode and the QueztalWriter class both need to have
"""Write 16-bit VALUE to global variable VARNUM. Incoming VARNUM # a checksum of memory generated.
must be between 0x10 and 0xFF."""
if not (0x10 <= varnum <= 0xFF):
raise ZMemoryOutOfBounds
if not (0x00 <= value <= 0xFFFF):
raise ZMemoryIllegalWrite(value)
log("Write %d to global variable %d" % (value, varnum))
actual_address = self._global_variable_start + ((varnum - 0x10) * 2)
bf = bitfield.BitField(value)
self._memory[actual_address] = bf[8:15]
self._memory[actual_address + 1] = bf[0:7]
# The 'verify' opcode and the QueztalWriter class both need to have def generate_checksum(self):
# a checksum of memory generated. """Return a checksum value which represents all the bytes of
memory added from $0040 upwards, modulo $10000."""
def generate_checksum(self): count = 0x40
"""Return a checksum value which represents all the bytes of total = 0
memory added from $0040 upwards, modulo $10000.""" while count < self._total_size:
count = 0x40 total += self._memory[count]
total = 0 count += 1
while count < self._total_size: return total % 0x10000
total += self._memory[count]
count += 1
return (total % 0x10000)

View file

@ -19,414 +19,405 @@ 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):
self._memory = zmem
self._propdefaults_addr = zmem.read_word(0x0A)
self._stringfactory = ZStringFactory(self._memory)
def __init__(self, zmem): if 1 <= self._memory.version <= 3:
self._objecttree_addr = self._propdefaults_addr + 62
elif 4 <= self._memory.version <= 5:
self._objecttree_addr = self._propdefaults_addr + 126
else:
raise ZObjectIllegalVersion
self._memory = zmem def _get_object_addr(self, objectnum):
self._propdefaults_addr = zmem.read_word(0x0a) """Return address of object number OBJECTNUM."""
self._stringfactory = ZStringFactory(self._memory)
if 1 <= self._memory.version <= 3: result = 0
self._objecttree_addr = self._propdefaults_addr + 62 if 1 <= self._memory.version <= 3:
elif 4 <= self._memory.version <= 5: if not (1 <= objectnum <= 255):
self._objecttree_addr = self._propdefaults_addr + 126 raise ZObjectIllegalObjectNumber
else: result = self._objecttree_addr + (9 * (objectnum - 1))
raise ZObjectIllegalVersion elif 4 <= self._memory.version <= 5:
if not (1 <= objectnum <= 65535):
log(f"error: there is no object {objectnum}")
raise ZObjectIllegalObjectNumber
result = self._objecttree_addr + (14 * (objectnum - 1))
else:
raise ZObjectIllegalVersion
log(f"address of object {objectnum} is {result}")
return result
def _get_object_addr(self, objectnum): def _get_parent_sibling_child(self, objectnum):
"""Return address of object number OBJECTNUM.""" """Return [parent, sibling, child] object numbers of object OBJECTNUM."""
result = 0 addr = self._get_object_addr(objectnum)
if 1 <= self._memory.version <= 3:
if not (1 <= objectnum <= 255):
raise ZObjectIllegalObjectNumber
result = self._objecttree_addr + (9 * (objectnum - 1))
elif 4 <= self._memory.version <= 5:
if not (1 <= objectnum <= 65535):
log("error: there is no object %d" % objectnum)
raise ZObjectIllegalObjectNumber
result = self._objecttree_addr + (14 * (objectnum - 1))
else:
raise ZObjectIllegalVersion
log("address of object %d is %d" % (objectnum, result)) result = 0
return result if 1 <= self._memory.version <= 3:
addr += 4 # skip past attributes
result = self._memory[addr : addr + 3]
elif 4 <= self._memory.version <= 5:
def _get_parent_sibling_child(self, objectnum): addr += 6 # skip past attributes
"""Return [parent, sibling, child] object numbers of object OBJECTNUM.""" result = [
self._memory.read_word(addr),
addr = self._get_object_addr(objectnum)
result = 0
if 1 <= self._memory.version <= 3:
addr += 4 # skip past attributes
result = self._memory[addr:addr+3]
elif 4 <= self._memory.version <= 5:
addr += 6 # skip past attributes
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: ]
raise ZObjectIllegalVersion
log ("parent/sibling/child of object %d is %d, %d, %d" %
(objectnum, result[0], result[1], result[2]))
return result
def _get_proptable_addr(self, objectnum):
"""Return address of property table of object OBJECTNUM."""
addr = self._get_object_addr(objectnum)
# skip past attributes and relatives
if 1 <= self._memory.version <= 3:
addr += 7
elif 4 <= self._memory.version <= 5:
addr += 12
else:
raise ZObjectIllegalVersion
return self._memory.read_word(addr)
def _get_default_property_addr(self, propnum):
"""Return address of default value for property PROPNUM."""
addr = self._propdefaults_addr
if 1 <= self._memory.version <= 3:
if not (1 <= propnum <= 31):
raise ZObjectIllegalPropertyNumber
elif 4 <= self._memory.version <= 5:
if not (1 <= propnum <= 63):
raise ZObjectIllegalPropertyNumber
else:
raise ZObjectIllegalVersion
return (addr + (2 * (propnum - 1)))
#--------- Public APIs -----------
def get_attribute(self, objectnum, attrnum):
"""Return value (0 or 1) of attribute number ATTRNUM of object
number OBJECTNUM."""
object_addr = self._get_object_addr(objectnum)
if 1 <= self._memory.version <= 3:
if not (0 <= attrnum <= 31):
raise ZObjectIllegalAttributeNumber
bf = BitField(self._memory[object_addr + (attrnum / 8)])
elif 4 <= self._memory.version <= 5:
if not (0 <= attrnum <= 47):
raise ZObjectIllegalAttributeNumber
bf = BitField(self._memory[object_addr + (attrnum / 8)])
else:
raise ZObjectIllegalVersion
return bf[7 - (attrnum % 8)]
def get_all_attributes(self, objectnum):
"""Return a list of all attribute numbers that are set on object
OBJECTNUM"""
if 1 <= self._memory.version <= 3:
max = 32
elif 4 <= self._memory.version <= 5:
max = 48
else:
raise ZObjectIllegalVersion
# really inefficient, but who cares?
attrs = []
for i in range (0, max):
if self.get_attribute(objectnum, i):
attrs.append(i)
return attrs
def get_parent(self, objectnum):
"""Return object number of parent of object number OBJECTNUM."""
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
return parent
def get_child(self, objectnum):
"""Return object number of child of object number OBJECTNUM."""
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
return child
def get_sibling(self, objectnum):
"""Return object number of sibling of object number OBJECTNUM."""
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
return sibling
def set_parent(self, objectnum, new_parent_num):
"""Make OBJECTNUM's parent pointer point to NEW_PARENT_NUM."""
addr = self._get_object_addr(objectnum)
if 1 <= self._memory.version <= 3:
self._memory[addr + 4] = new_parent_num
elif 4 <= self._memory.version <= 5:
self._memory.write_word(addr + 6, new_parent_num)
else:
raise ZObjectIllegalVersion
def set_child(self, objectnum, new_child_num):
"""Make OBJECTNUM's child pointer point to NEW_PARENT_NUM."""
addr = self._get_object_addr(objectnum)
if 1 <= self._memory.version <= 3:
self._memory[addr + 6] = new_child_num
elif 4 <= self._memory.version <= 5:
self._memory.write_word(addr + 10, new_child_num)
else:
raise ZObjectIllegalVersion
def set_sibling(self, objectnum, new_sibling_num):
"""Make OBJECTNUM's sibling pointer point to NEW_PARENT_NUM."""
addr = self._get_object_addr(objectnum)
if 1 <= self._memory.version <= 3:
self._memory[addr + 5] = new_sibling_num
elif 4 <= self._memory.version <= 5:
self._memory.write_word(addr + 8, new_sibling_num)
else:
raise ZObjectIllegalVersion
def insert_object(self, parent_object, new_child):
"""Prepend object NEW_CHILD to the list of PARENT_OBJECT's children."""
# Remember all the original pointers within the new_child
[p, s, c] = self._get_parent_sibling_child(new_child)
# First insert new_child intto the parent_object
original_child = self.get_child(parent_object)
self.set_sibling(new_child, original_child)
self.set_parent(new_child, parent_object)
self.set_child(parent_object, new_child)
if p == 0: # no need to 'remove' new_child, since it wasn't in a tree
return
# Hunt down and remove the new_child from its old location
item = self.get_child(p)
if item == 0:
# new_object claimed to have parent p, but p has no children!?
raise ZObjectMalformedTree
elif item == new_child: # done! new_object was head of list
self.set_child(p, s) # note that s might be 0, that's fine.
else: # walk across list of sibling links
prev = item
current = self.get_sibling(item)
while current != 0:
if current == new_child:
self.set_sibling(prev, s) # s might be 0, that's fine.
break
else:
# we reached the end of the list, never got a match
raise ZObjectMalformedTree
def get_shortname(self, objectnum):
"""Return 'short name' of object number OBJECTNUM as ascii string."""
addr = self._get_proptable_addr(objectnum)
return self._stringfactory.get(addr+1)
def get_prop(self, objectnum, propnum):
"""Return either a byte or word value of property PROPNUM of
object OBJECTNUM."""
(addr, size) = self.get_prop_addr_len(objectnum, propnum)
if size == 1:
return self._memory[addr]
elif size == 2:
return self._memory.read_word(addr)
else:
raise ZObjectIllegalPropLength
def get_prop_addr_len(self, objectnum, propnum):
"""Return address & length of value for property number PROPNUM of
object number OBJECTNUM. If object has no such property, then
return the address & length of the 'default' value for the property."""
# start at the beginning of the object's proptable
addr = self._get_proptable_addr(objectnum)
# skip past the shortname of the object
addr += (2 * self._memory[addr])
pnum = 0
if 1 <= self._memory.version <= 3:
while self._memory[addr] != 0:
bf = BitField(self._memory[addr])
addr += 1
pnum = bf[4:0]
size = bf[7:5] + 1
if pnum == propnum:
return (addr, size)
addr += size
elif 4 <= self._memory.version <= 5:
while self._memory[addr] != 0:
bf = BitField(self._memory[addr])
addr += 1
pnum = bf[5:0]
if bf[7]:
bf2 = BitField(self._memory[addr])
addr += 1
size = bf2[5:0]
else: else:
if bf[6]: raise ZObjectIllegalVersion
size = 2
else:
size = 1
if pnum == propnum:
return (addr, size)
addr += size
else: log(
raise ZObjectIllegalVersion f"parent/sibling/child of object {objectnum} is "
f"{result[0]}, {result[1]}, {result[2]}"
)
return result
# property list ran out, so return default propval instead. def _get_proptable_addr(self, objectnum):
default_value_addr = self._get_default_property_addr(propnum) """Return address of property table of object OBJECTNUM."""
return (default_value_addr, 2)
addr = self._get_object_addr(objectnum)
def get_all_properties(self, objectnum): # skip past attributes and relatives
"""Return a dictionary of all properties listed in the property if 1 <= self._memory.version <= 3:
table of object OBJECTNUM. (Obviously, this discounts 'default' addr += 7
property values.). The dictionary maps property numbers to (addr, elif 4 <= self._memory.version <= 5:
len) propval tuples.""" addr += 12
proplist = {}
# start at the beginning of the object's proptable
addr = self._get_proptable_addr(objectnum)
# skip past the shortname of the object
shortname_length = self._memory[addr]
addr += 1
addr += (2*shortname_length)
if 1 <= self._memory.version <= 3:
while self._memory[addr] != 0:
bf = BitField(self._memory[addr])
addr += 1
pnum = bf[4:0]
size = bf[7:5] + 1
proplist[pnum] = (addr, size)
addr += size
elif 4 <= self._memory.version <= 5:
while self._memory[addr] != 0:
bf = BitField(self._memory[addr])
addr += 1
pnum = bf[0:6]
if bf[7]:
bf2 = BitField(self._memory[addr])
addr += 1
size = bf2[0:6]
if size == 0:
size = 64
else: else:
if bf[6]: raise ZObjectIllegalVersion
size = 2
else:
size = 1
proplist[pnum] = (addr, size)
addr += size
else: return self._memory.read_word(addr)
raise ZObjectIllegalVersion
return proplist def _get_default_property_addr(self, propnum):
"""Return address of default value for property PROPNUM."""
addr = self._propdefaults_addr
def set_property(self, objectnum, propnum, value): if 1 <= self._memory.version <= 3:
"""Set a property on an object.""" if not (1 <= propnum <= 31):
proplist = self.get_all_properties(objectnum) raise ZObjectIllegalPropertyNumber
if propnum not in proplist: elif 4 <= self._memory.version <= 5:
raise ZObjectIllegalPropertyNumber if not (1 <= propnum <= 63):
raise ZObjectIllegalPropertyNumber
else:
raise ZObjectIllegalVersion
addr, size = proplist[propnum] return addr + (2 * (propnum - 1))
if size == 1:
self._memory[addr] = (value & 0xFF)
elif size == 2:
self._memory.write_word(addr, value)
else:
raise ZObjectIllegalPropertySet
# --------- Public APIs -----------
def describe_object(self, objectnum): def get_attribute(self, objectnum, attrnum):
"""For debugging purposes, pretty-print everything known about """Return value (0 or 1) of attribute number ATTRNUM of object
object OBJECTNUM.""" number OBJECTNUM."""
print("Object number:", objectnum) object_addr = self._get_object_addr(objectnum)
print(" Short name:", self.get_shortname(objectnum))
print(" Parent:", self.get_parent(objectnum), end=' ')
print(" Sibling:", self.get_sibling(objectnum), end=' ')
print(" Child:", self.get_child(objectnum))
print(" Attributes:", self.get_all_attributes(objectnum))
print(" Properties:")
proplist = self.get_all_properties(objectnum) if 1 <= self._memory.version <= 3:
for key in list(proplist.keys()): if not (0 <= attrnum <= 31):
(addr, len) = proplist[key] raise ZObjectIllegalAttributeNumber
print(" [%2d] :" % key, end=' ') bf = BitField(self._memory[object_addr + (attrnum / 8)])
for i in range(0, len):
print("%02X" % self._memory[addr+i], end=' ')
print()
elif 4 <= self._memory.version <= 5:
if not (0 <= attrnum <= 47):
raise ZObjectIllegalAttributeNumber
bf = BitField(self._memory[object_addr + (attrnum / 8)])
else:
raise ZObjectIllegalVersion
return bf[7 - (attrnum % 8)]
def get_all_attributes(self, objectnum):
"""Return a list of all attribute numbers that are set on object
OBJECTNUM"""
if 1 <= self._memory.version <= 3:
max = 32
elif 4 <= self._memory.version <= 5:
max = 48
else:
raise ZObjectIllegalVersion
# really inefficient, but who cares?
attrs = []
for i in range(0, max):
if self.get_attribute(objectnum, i):
attrs.append(i)
return attrs
def get_parent(self, objectnum):
"""Return object number of parent of object number OBJECTNUM."""
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
return parent
def get_child(self, objectnum):
"""Return object number of child of object number OBJECTNUM."""
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
return child
def get_sibling(self, objectnum):
"""Return object number of sibling of object number OBJECTNUM."""
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
return sibling
def set_parent(self, objectnum, new_parent_num):
"""Make OBJECTNUM's parent pointer point to NEW_PARENT_NUM."""
addr = self._get_object_addr(objectnum)
if 1 <= self._memory.version <= 3:
self._memory[addr + 4] = new_parent_num
elif 4 <= self._memory.version <= 5:
self._memory.write_word(addr + 6, new_parent_num)
else:
raise ZObjectIllegalVersion
def set_child(self, objectnum, new_child_num):
"""Make OBJECTNUM's child pointer point to NEW_PARENT_NUM."""
addr = self._get_object_addr(objectnum)
if 1 <= self._memory.version <= 3:
self._memory[addr + 6] = new_child_num
elif 4 <= self._memory.version <= 5:
self._memory.write_word(addr + 10, new_child_num)
else:
raise ZObjectIllegalVersion
def set_sibling(self, objectnum, new_sibling_num):
"""Make OBJECTNUM's sibling pointer point to NEW_PARENT_NUM."""
addr = self._get_object_addr(objectnum)
if 1 <= self._memory.version <= 3:
self._memory[addr + 5] = new_sibling_num
elif 4 <= self._memory.version <= 5:
self._memory.write_word(addr + 8, new_sibling_num)
else:
raise ZObjectIllegalVersion
def insert_object(self, parent_object, new_child):
"""Prepend object NEW_CHILD to the list of PARENT_OBJECT's children."""
# Remember all the original pointers within the new_child
[p, s, c] = self._get_parent_sibling_child(new_child)
# First insert new_child intto the parent_object
original_child = self.get_child(parent_object)
self.set_sibling(new_child, original_child)
self.set_parent(new_child, parent_object)
self.set_child(parent_object, new_child)
if p == 0: # no need to 'remove' new_child, since it wasn't in a tree
return
# Hunt down and remove the new_child from its old location
item = self.get_child(p)
if item == 0:
# new_object claimed to have parent p, but p has no children!?
raise ZObjectMalformedTree
elif item == new_child: # done! new_object was head of list
self.set_child(p, s) # note that s might be 0, that's fine.
else: # walk across list of sibling links
prev = item
current = self.get_sibling(item)
while current != 0:
if current == new_child:
self.set_sibling(prev, s) # s might be 0, that's fine.
break
else:
# we reached the end of the list, never got a match
raise ZObjectMalformedTree
def get_shortname(self, objectnum):
"""Return 'short name' of object number OBJECTNUM as ascii string."""
addr = self._get_proptable_addr(objectnum)
return self._stringfactory.get(addr + 1)
def get_prop(self, objectnum, propnum):
"""Return either a byte or word value of property PROPNUM of
object OBJECTNUM."""
(addr, size) = self.get_prop_addr_len(objectnum, propnum)
if size == 1:
return self._memory[addr]
elif size == 2:
return self._memory.read_word(addr)
else:
raise ZObjectIllegalPropLength
def get_prop_addr_len(self, objectnum, propnum):
"""Return address & length of value for property number PROPNUM of
object number OBJECTNUM. If object has no such property, then
return the address & length of the 'default' value for the property."""
# start at the beginning of the object's proptable
addr = self._get_proptable_addr(objectnum)
# skip past the shortname of the object
addr += 2 * self._memory[addr]
pnum = 0
if 1 <= self._memory.version <= 3:
while self._memory[addr] != 0:
bf = BitField(self._memory[addr])
addr += 1
pnum = bf[4:0]
size = bf[7:5] + 1
if pnum == propnum:
return (addr, size)
addr += size
elif 4 <= self._memory.version <= 5:
while self._memory[addr] != 0:
bf = BitField(self._memory[addr])
addr += 1
pnum = bf[5:0]
if bf[7]:
bf2 = BitField(self._memory[addr])
addr += 1
size = bf2[5:0]
else:
size = 2 if bf[6] else 1
if pnum == propnum:
return (addr, size)
addr += size
else:
raise ZObjectIllegalVersion
# property list ran out, so return default propval instead.
default_value_addr = self._get_default_property_addr(propnum)
return (default_value_addr, 2)
def get_all_properties(self, objectnum):
"""Return a dictionary of all properties listed in the property
table of object OBJECTNUM. (Obviously, this discounts 'default'
property values.). The dictionary maps property numbers to (addr,
len) propval tuples."""
proplist = {}
# start at the beginning of the object's proptable
addr = self._get_proptable_addr(objectnum)
# skip past the shortname of the object
shortname_length = self._memory[addr]
addr += 1
addr += 2 * shortname_length
if 1 <= self._memory.version <= 3:
while self._memory[addr] != 0:
bf = BitField(self._memory[addr])
addr += 1
pnum = bf[4:0]
size = bf[7:5] + 1
proplist[pnum] = (addr, size)
addr += size
elif 4 <= self._memory.version <= 5:
while self._memory[addr] != 0:
bf = BitField(self._memory[addr])
addr += 1
pnum = bf[0:6]
if bf[7]:
bf2 = BitField(self._memory[addr])
addr += 1
size = bf2[0:6]
if size == 0:
size = 64
else:
size = 2 if bf[6] else 1
proplist[pnum] = (addr, size)
addr += size
else:
raise ZObjectIllegalVersion
return proplist
def set_property(self, objectnum, propnum, value):
"""Set a property on an object."""
proplist = self.get_all_properties(objectnum)
if propnum not in proplist:
raise ZObjectIllegalPropertyNumber
addr, size = proplist[propnum]
if size == 1:
self._memory[addr] = value & 0xFF
elif size == 2:
self._memory.write_word(addr, value)
else:
raise ZObjectIllegalPropertySet
def describe_object(self, objectnum):
"""For debugging purposes, pretty-print everything known about
object OBJECTNUM."""
print("Object number:", objectnum)
print(" Short name:", self.get_shortname(objectnum))
print(" Parent:", self.get_parent(objectnum), end=" ")
print(" Sibling:", self.get_sibling(objectnum), end=" ")
print(" Child:", self.get_child(objectnum))
print(" Attributes:", self.get_all_attributes(objectnum))
print(" Properties:")
proplist = self.get_all_properties(objectnum)
for key in list(proplist.keys()):
(addr, len) = proplist[key]
print(f" [{key:2d}] :", end=" ")
for i in range(0, len):
print(f"{self._memory[addr + i]:02X}", end=" ")
print()

View file

@ -11,8 +11,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
@ -27,12 +29,12 @@ 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.
LARGE_CONSTANT = 0x0 LARGE_CONSTANT = 0x0
@ -40,195 +42,203 @@ 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):
"" ""
self._memory = zmem self._memory = zmem
self._stack = zstack self._stack = zstack
self._parse_map = {} self._parse_map = {}
self.program_counter = self._memory.read_word(0x6) self.program_counter = self._memory.read_word(0x6)
def _get_pc(self): def _get_pc(self):
byte = self._memory[self.program_counter] byte = self._memory[self.program_counter]
self.program_counter += 1 self.program_counter += 1
return byte return byte
def get_next_instruction(self): def get_next_instruction(self):
"""Decode the opcode & operands currently pointed to by the """Decode the opcode & operands currently pointed to by the
program counter, and appropriately increment the program counter program counter, and appropriately increment the program counter
afterwards. A decoded operation is returned to the caller in the form: afterwards. A decoded operation is returned to the caller in the form:
[opcode-class, opcode-number, [operand, operand, operand, ...]] [opcode-class, opcode-number, [operand, operand, operand, ...]]
If the opcode has no operands, the operand list is present but empty.""" If the opcode has no operands, the operand list is present but empty."""
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:
# Extended opcode # Extended opcode
return self._parse_opcode_extended() return self._parse_opcode_extended()
opcode = BitField(opcode) opcode = BitField(opcode)
if opcode[7] == 0: if opcode[7] == 0:
# Long opcode # Long opcode
return self._parse_opcode_long(opcode) return self._parse_opcode_long(opcode)
elif opcode[6] == 0: elif opcode[6] == 0:
# Short opcode # Short opcode
return self._parse_opcode_short(opcode) return self._parse_opcode_short(opcode)
else: else:
# Variable opcode # Variable opcode
return self._parse_opcode_variable(opcode) return self._parse_opcode_variable(opcode)
def _parse_opcode_long(self, opcode): def _parse_opcode_long(self, opcode):
"""Parse an opcode of the long form.""" """Parse an opcode of the long form."""
# Long opcodes are always 2OP. The types of the two operands are # Long opcodes are always 2OP. The types of the two operands are
# 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]]),
return (OPCODE_2OP, opcode[0:5], operands) self._parse_operand(LONG_OPERAND_TYPES[opcode[5]]),
]
return (OPCODE_2OP, opcode[0:5], operands)
def _parse_opcode_short(self, opcode): def _parse_opcode_short(self, opcode):
"""Parse an opcode of the short form.""" """Parse an opcode of the short form."""
# Short opcodes can have either 1 operand, or no operand. # Short opcodes can have either 1 operand, or no operand.
log("Opcode is short") log("Opcode is short")
operand_type = opcode[4:6] operand_type = opcode[4:6]
operand = self._parse_operand(operand_type) operand = self._parse_operand(operand_type)
if operand is None: # 0OP variant if operand is None: # 0OP variant
log("Opcode is 0OP variant") log("Opcode is 0OP variant")
return (OPCODE_0OP, opcode[0:4], []) return (OPCODE_0OP, opcode[0:4], [])
else: else:
log("Opcode is 1OP variant") log("Opcode is 1OP variant")
return (OPCODE_1OP, opcode[0:4], [operand]) return (OPCODE_1OP, opcode[0:4], [operand])
def _parse_opcode_variable(self, opcode): def _parse_opcode_variable(self, opcode):
"""Parse an opcode of the variable form.""" """Parse an opcode of the variable form."""
log("Opcode is variable") log("Opcode is variable")
if opcode[5]: if opcode[5]:
log("Variable opcode of VAR kind") log("Variable opcode of VAR kind")
opcode_type = OPCODE_VAR opcode_type = OPCODE_VAR
else: else:
log("Variable opcode of 2OP kind") log("Variable opcode of 2OP kind")
opcode_type = OPCODE_2OP opcode_type = OPCODE_2OP
opcode_num = opcode[0:5] opcode_num = opcode[0:5]
# Parse the types byte to retrieve the operands. # Parse the types byte to retrieve the operands.
operands = self._parse_operands_byte() operands = self._parse_operands_byte()
# Special case: opcodes 12 and 26 have a second operands byte. # Special case: opcodes 12 and 26 have a second operands byte.
if opcode[0:7] == 0xC or opcode[0:7] == 0x1A: if opcode[0:7] == 0xC or opcode[0:7] == 0x1A:
log("Opcode has second operand byte") log("Opcode has second operand byte")
operands += self._parse_operands_byte() operands += self._parse_operands_byte()
return (opcode_type, opcode_num, operands) return (opcode_type, opcode_num, operands)
def _parse_operand(self, operand_type): def _parse_opcode_extended(self):
"""Read and return an operand of the given type. """Parse an extended opcode (v5+ feature)."""
raise NotImplementedError("Extended opcodes (v5+) not yet implemented")
This assumes that the operand is in memory, at the address pointed def _parse_operand(self, operand_type):
by the Program Counter.""" """Read and return an operand of the given type.
assert operand_type <= 0x3
if operand_type == LARGE_CONSTANT: This assumes that the operand is in memory, at the address pointed
log("Operand is large constant") by the Program Counter."""
operand = self._memory.read_word(self.program_counter) assert operand_type <= 0x3
self.program_counter += 2
elif operand_type == SMALL_CONSTANT:
log("Operand is small constant")
operand = self._get_pc()
elif operand_type == VARIABLE:
variable_number = self._get_pc()
log("Operand is variable %d" % variable_number)
if variable_number == 0:
log("Operand value comes from stack")
operand = self._stack.pop_stack() # TODO: make sure this is right.
elif variable_number < 16:
log("Operand value comes from local variable")
operand = self._stack.get_local_variable(variable_number - 1)
else:
log("Operand value comes from global variable")
operand = self._memory.read_global(variable_number)
elif operand_type == ABSENT:
log("Operand is absent")
operand = None
if operand is not None:
log("Operand value: %d" % operand)
return operand if operand_type == LARGE_CONSTANT:
log("Operand is large constant")
operand = self._memory.read_word(self.program_counter)
self.program_counter += 2
elif operand_type == SMALL_CONSTANT:
log("Operand is small constant")
operand = self._get_pc()
elif operand_type == VARIABLE:
variable_number = self._get_pc()
log(f"Operand is variable {variable_number}")
if variable_number == 0:
log("Operand value comes from stack")
operand = self._stack.pop_stack() # TODO: make sure this is right.
elif variable_number < 16:
log("Operand value comes from local variable")
operand = self._stack.get_local_variable(variable_number - 1)
else:
log("Operand value comes from global variable")
operand = self._memory.read_global(variable_number)
elif operand_type == ABSENT:
log("Operand is absent")
operand = None
if operand is not None:
log(f"Operand value: {operand}")
def _parse_operands_byte(self): return operand
"""Parse operands given by the operand byte and return a list of
values.
"""
operand_byte = BitField(self._get_pc())
operands = []
for operand_type in [operand_byte[6:8], operand_byte[4:6],
operand_byte[2:4], operand_byte[0:2]]:
operand = self._parse_operand(operand_type)
if operand is None:
break
operands.append(operand)
return operands def _parse_operands_byte(self):
"""Parse operands given by the operand byte and return a list of
values.
"""
operand_byte = BitField(self._get_pc())
operands = []
for operand_type in [
operand_byte[6:8],
operand_byte[4:6],
operand_byte[2:4],
operand_byte[0:2],
]:
operand = self._parse_operand(operand_type)
if operand is None:
break
operands.append(operand)
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:
def get_zstring(self): def get_zstring(self):
"""For string opcodes, return the address of the zstring pointed """For string opcodes, return the address of the zstring pointed
to by the PC. Increment PC just past the text.""" to by the PC. Increment PC just past the text."""
start_addr = self.program_counter start_addr = self.program_counter
bf = BitField(0) bf = BitField(0)
while True: while True:
bf.__init__(self._memory[self.program_counter]) bf.__init__(self._memory[self.program_counter])
self.program_counter += 2 self.program_counter += 2
if bf[7] == 1: if bf[7] == 1:
break break
return start_addr return start_addr
def get_store_address(self):
"""For store opcodes, read byte pointed to by PC and return the
variable number in which the operation result should be stored.
Increment the PC as necessary."""
return self._get_pc()
def get_store_address(self): def get_branch_offset(self):
"""For store opcodes, read byte pointed to by PC and return the """For branching opcodes, examine address pointed to by PC, and
variable number in which the operation result should be stored. return two values: first, either True or False (indicating whether
Increment the PC as necessary.""" to branch if true or branch if false), and second, the address to
return self._get_pc() jump to. Increment the PC as necessary."""
bf = BitField(self._get_pc())
branch_if_true = bool(bf[7])
if bf[6]:
branch_offset = bf[0:6]
else:
# We need to do a little magic here. The branch offset is
# written as a signed 14-bit number, with signed meaning '-n' is
# written as '65536-n'. Or in this case, as we have 14 bits,
# '16384-n'.
#
# So, if the MSB (ie. bit 13) is set, we have a negative
# number. We take the value, and substract 16384 to get the
# actual offset as a negative integer.
#
# If the MSB is not set, we just extract the value and return it.
#
# Can you spell "Weird" ?
branch_offset = self._get_pc() + (bf[0:5] << 8)
if bf[5]:
branch_offset -= 8192
def get_branch_offset(self): log(f"Branch if {branch_if_true} to offset {branch_offset:+d}")
"""For branching opcodes, examine address pointed to by PC, and return branch_if_true, branch_offset
return two values: first, either True or False (indicating whether
to branch if true or branch if false), and second, the address to
jump to. Increment the PC as necessary."""
bf = BitField(self._get_pc())
branch_if_true = bool(bf[7])
if bf[6]:
branch_offset = bf[0:6]
else:
# We need to do a little magic here. The branch offset is
# written as a signed 14-bit number, with signed meaning '-n' is
# written as '65536-n'. Or in this case, as we have 14 bits,
# '16384-n'.
#
# So, if the MSB (ie. bit 13) is set, we have a negative
# number. We take the value, and substract 16384 to get the
# actual offset as a negative integer.
#
# If the MSB is not set, we just extract the value and return it.
#
# Can you spell "Weird" ?
branch_offset = self._get_pc() + (bf[0:5] << 8)
if bf[5]:
branch_offset -= 8192
log('Branch if %s to offset %+d' % (branch_if_true, branch_offset))
return branch_if_true, branch_offset

View file

@ -60,244 +60,231 @@ INFINITE_ROWS = 255
class ZScreenObserver: class ZScreenObserver:
"""Observer that is notified of changes in the state of a ZScreen """Observer that is notified of changes in the state of a ZScreen
object. object.
Note that all methods in this class may be called by any thread at Note that all methods in this class may be called by any thread at
any time, so they should take any necessary precautions to ensure any time, so they should take any necessary precautions to ensure
the integrity of any data they modify.""" the integrity of any data they modify."""
def on_screen_size_change(self, zscreen): def on_screen_size_change(self, zscreen):
"""Called when the screen size of a ZScreen changes.""" """Called when the screen size of a ZScreen changes."""
pass pass
def on_font_size_change(self, zscreen): def on_font_size_change(self, zscreen):
"""Called when the font size of a ZScreen changes.""" """Called when the font size of a ZScreen changes."""
pass pass
class ZScreen(zstream.ZBufferableOutputStream): class ZScreen(zstream.ZBufferableOutputStream):
"""Subclass of zstream.ZBufferableOutputStream that provides an """Subclass of zstream.ZBufferableOutputStream that provides an
abstraction of a computer screen.""" abstraction of a computer screen."""
def __init__(self): def __init__(self):
"Constructor for the screen." "Constructor for the screen."
zstream.ZBufferableOutputStream.__init__(self) zstream.ZBufferableOutputStream.__init__(self)
# The size of the screen. # The size of the screen.
self._columns = 79 self._columns = 79
self._rows = 24 self._rows = 24
# The size of the current font, in characters # The size of the current font, in characters
self._fontheight = 1 self._fontheight = 1
self._fontwidth = 1 self._fontwidth = 1
# List of our observers; clients can directly append to and remove # List of our observers; clients can directly append to and remove
# from this. # from this.
self.observers = [] self.observers = []
# Subclasses must define real values for all the features they # Subclasses must define real values for all the features they
# support (or don't support). # support (or don't support).
self.features = { self.features = {
"has_status_line" : False, "has_status_line": False,
"has_upper_window" : False, "has_upper_window": False,
"has_graphics_font" : False, "has_graphics_font": False,
"has_text_colors": False, "has_text_colors": False,
} }
# Window Management # Window Management
# #
# The z-machine has 2 windows for displaying text, "upper" and # The z-machine has 2 windows for displaying text, "upper" and
# "lower". (The upper window has an inital height of 0.) # "lower". (The upper window has an inital height of 0.)
# #
# The upper window is not necessarily where the "status line" # The upper window is not necessarily where the "status line"
# appears; see section 8.6.1.1 of the Z-Machine Standards Document. # appears; see section 8.6.1.1 of the Z-Machine Standards Document.
# #
# The UI is responsible for making the lower window scroll properly, # The UI is responsible for making the lower window scroll properly,
# as well as wrapping words ("buffering"). The upper window, # as well as wrapping words ("buffering"). The upper window,
# however, should *never* scroll or wrap words. # however, should *never* scroll or wrap words.
# #
# The UI is also responsible for displaying [MORE] prompts when # The UI is also responsible for displaying [MORE] prompts when
# printing more text than the screen's rows can display. (Note: if # printing more text than the screen's rows can display. (Note: if
# the number of screen rows is INFINITE_ROWS, then it should never # the number of screen rows is INFINITE_ROWS, then it should never
# prompt [MORE].) # prompt [MORE].)
def get_screen_size(self): def get_screen_size(self):
"""Return the current size of the screen as [rows, columns].""" """Return the current size of the screen as [rows, columns]."""
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.
WINDOW should be one of WINDOW_UPPER or WINDOW_LOWER. WINDOW should be one of WINDOW_UPPER or WINDOW_LOWER.
This method should only be implemented if the This method should only be implemented if the
has_upper_window feature is enabled.""" has_upper_window feature is enabled."""
raise NotImplementedError() raise NotImplementedError()
def split_window(self, height):
"""Make the upper window appear and be HEIGHT lines tall. To
'unsplit' a window, call with a height of 0 lines.
def split_window(self, height): This method should only be implemented if the has_upper_window
"""Make the upper window appear and be HEIGHT lines tall. To feature is enabled."""
'unsplit' a window, call with a height of 0 lines.
This method should only be implemented if the has_upper_window raise NotImplementedError()
feature is enabled."""
raise NotImplementedError() def set_cursor_position(self, x, y):
"""Set the cursor to (row, column) coordinates (X,Y) in the
current window, where (1,1) is the upper-left corner.
This function only does something if the current window is the
upper window; if the current window is the lower window, this
function has no effect.
def set_cursor_position(self, x, y): This method should only be implemented if the has_upper_window
"""Set the cursor to (row, column) coordinates (X,Y) in the feature is enabled, as the upper window is the only window that
current window, where (1,1) is the upper-left corner. supports cursor positioning."""
This function only does something if the current window is the raise NotImplementedError()
upper window; if the current window is the lower window, this
function has no effect.
This method should only be implemented if the has_upper_window
feature is enabled, as the upper window is the only window that
supports cursor positioning."""
raise NotImplementedError() def erase_window(self, window=WINDOW_LOWER, color=COLOR_CURRENT):
"""Erase WINDOW to background COLOR.
WINDOW should be one of WINDOW_UPPER or WINDOW_LOWER.
def erase_window(self, window=WINDOW_LOWER, If the has_upper_window feature is not supported, WINDOW is
color=COLOR_CURRENT): ignored (in such a case, this function should clear the entire
"""Erase WINDOW to background COLOR. screen).
WINDOW should be one of WINDOW_UPPER or WINDOW_LOWER. COLOR should be one of the COLOR_* constants.
If the has_upper_window feature is not supported, WINDOW is If the has_text_colors feature is not supported, COLOR is ignored."""
ignored (in such a case, this function should clear the entire
screen).
COLOR should be one of the COLOR_* constants. raise NotImplementedError()
If the has_text_colors feature is not supported, COLOR is ignored.""" def erase_line(self):
"""Erase from the current cursor position to the end of its line
in the current window.
raise NotImplementedError() This method should only be implemented if the has_upper_window
feature is enabled, as the upper window is the only window that
supports cursor positioning."""
raise NotImplementedError()
def erase_line(self): # Status Line
"""Erase from the current cursor position to the end of its line #
in the current window. # These routines are only called if the has_status_line capability
# is set. Specifically, one of them is called whenever the
# show_status opcode is executed, and just before input is read from
# the user.
This method should only be implemented if the has_upper_window def print_status_score_turns(self, text, score, turns):
feature is enabled, as the upper window is the only window that """Print a status line in the upper window, as follows:
supports cursor positioning."""
raise NotImplementedError() On the left side of the status line, print TEXT.
On the right side of the status line, print SCORE/TURNS.
This method should only be implemented if the has_status_line
feature is enabled.
"""
# Status Line raise NotImplementedError()
#
# These routines are only called if the has_status_line capability
# is set. Specifically, one of them is called whenever the
# show_status opcode is executed, and just before input is read from
# the user.
def print_status_score_turns(self, text, score, turns): 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:
On the left side of the status line, print TEXT. On the left side of the status line, print TEXT.
On the right side of the status line, print SCORE/TURNS. On the right side of the status line, print HOURS:MINUTES.
This method should only be implemented if the has_status_line This method should only be implemented if the has_status_line
feature is enabled. feature is enabled.
""" """
raise NotImplementedError() raise NotImplementedError()
# Text Appearances
#
def print_status_time(self, hours, minutes): def get_font_size(self):
"""Print a status line in the upper window, as follows: """Return the current font's size as [width, height]."""
On the left side of the status line, print TEXT. return [self._fontwidth, self._fontheight]
On the right side of the status line, print HOURS:MINUTES.
This method should only be implemented if the has_status_line def set_font(self, font_number):
feature is enabled. """Set the current window's font to one of
"""
raise NotImplementedError() FONT_NORMAL - normal font
FONT_PICTURE - picture font (IGNORE, this means nothing)
FONT_CHARACTER_GRAPHICS - character graphics font
FONT_FIXED_WIDTH - fixed-width font
If a font is not available, return None. Otherwise, set the
new font, and return the number of the *previous* font.
# Text Appearances The only font that must be supported is FONT_NORMAL; all others
# are optional, as per section 8.1.3 of the Z-Machine Standards
Document."""
def get_font_size(self): raise NotImplementedError()
"""Return the current font's size as [width, height]."""
return [self._fontwidth, self._fontheight] def set_text_style(self, style):
"""Set the current text style to the given text style.
STYLE is a sequence, each element of which should be one of the
following values:
def set_font(self, font_number): STYLE_ROMAN - Roman
"""Set the current window's font to one of STYLE_REVERSE_VIDEO - Reverse video
STYLE_BOLD - Bold
STYLE_ITALIC - Italic
STYLE_FIXED_PITCH - Fixed-width
FONT_NORMAL - normal font It is not a requirement that the screen implementation support
FONT_PICTURE - picture font (IGNORE, this means nothing) every combination of style; if no combinations are possible, it is
FONT_CHARACTER_GRAPHICS - character graphics font acceptable to simply use the first style in the sequence and ignore
FONT_FIXED_WIDTH - fixed-width font the rest.
If a font is not available, return None. Otherwise, set the As per section 8.7.1.1 of the Z-Machine Standards Document, the
new font, and return the number of the *previous* font. implementation need not provide bold or italic, and is free to
interpret them broadly.
"""
The only font that must be supported is FONT_NORMAL; all others raise NotImplementedError()
are optional, as per section 8.1.3 of the Z-Machine Standards
Document."""
raise NotImplementedError() def set_text_color(self, foreground_color, background_color):
"""Set current text foreground and background color. Each color
should correspond to one of the COLOR_* constants.
This method should only be implemented if the has_text_colors
feature is enabled.
"""
def set_text_style(self, style): raise NotImplementedError()
"""Set the current text style to the given text style.
STYLE is a sequence, each element of which should be one of the # Standard output
following values:
STYLE_ROMAN - Roman def write(self, string):
STYLE_REVERSE_VIDEO - Reverse video """Implementation of the ZOutputStream method. Prints the given
STYLE_BOLD - Bold unicode string to the currently active window, using the current
STYLE_ITALIC - Italic text style settings."""
STYLE_FIXED_PITCH - Fixed-width
It is not a requirement that the screen implementation support raise NotImplementedError()
every combination of style; if no combinations are possible, it is
acceptable to simply use the first style in the sequence and ignore
the rest.
As per section 8.7.1.1 of the Z-Machine Standards Document, the
implementation need not provide bold or italic, and is free to
interpret them broadly.
"""
raise NotImplementedError()
def set_text_color(self, foreground_color, background_color):
"""Set current text foreground and background color. Each color
should correspond to one of the COLOR_* constants.
This method should only be implemented if the has_text_colors
feature is enabled.
"""
raise NotImplementedError()
# Standard output
def write(self, string):
"""Implementation of the ZOutputStream method. Prints the given
unicode string to the currently active window, using the current
text style settings."""
raise NotImplementedError()

View file

@ -12,193 +12,189 @@ 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__(
self, start_addr, return_addr, zmem, args, local_vars=None, stack=None
):
"""Initialize a routine object beginning at START_ADDR in ZMEM,
with initial argument values in list ARGS. If LOCAL_VARS is None,
then parse them from START_ADDR."""
def __init__(self, start_addr, return_addr, zmem, args, self.start_addr = start_addr
local_vars=None, stack=None): self.return_addr = return_addr
"""Initialize a routine object beginning at START_ADDR in ZMEM, self.program_counter = 0 # used when execution interrupted
with initial argument values in list ARGS. If LOCAL_VARS is None,
then parse them from START_ADDR."""
self.start_addr = start_addr if stack is None:
self.return_addr = return_addr self.stack = []
self.program_counter = 0 # used when execution interrupted else:
self.stack = stack[:]
if stack is None: if local_vars is not None:
self.stack = [] self.local_vars = local_vars[:]
else: else:
self.stack = stack[:] num_local_vars = zmem[self.start_addr]
if not (0 <= num_local_vars <= 15):
log(f"num local vars is {num_local_vars}")
raise ZStackError
self.start_addr += 1
if local_vars is not None: # Initialize the local vars in the ZRoutine's dictionary. This is
self.local_vars = local_vars[:] # only needed on machines v1 through v4. In v5 machines, all local
else: # variables are preinitialized to zero.
num_local_vars = zmem[self.start_addr] self.local_vars = [0 for _ in range(15)]
if not (0 <= num_local_vars <= 15): if 1 <= zmem.version <= 4:
log("num local vars is %d" % num_local_vars) for i in range(num_local_vars):
raise ZStackError self.local_vars[i] = zmem.read_word(self.start_addr)
self.start_addr += 1 self.start_addr += 2
elif zmem.version != 5:
raise ZStackUnsupportedVersion
# Initialize the local vars in the ZRoutine's dictionary. This is # Place call arguments into local vars, if available
# only needed on machines v1 through v4. In v5 machines, all local for i in range(0, len(args)):
# variables are preinitialized to zero. self.local_vars[i] = args[i]
self.local_vars = [0 for _ in range(15)]
if 1 <= zmem.version <= 4:
for i in range(num_local_vars):
self.local_vars[i] = zmem.read_word(self.start_addr)
self.start_addr += 2
elif zmem.version != 5:
raise ZStackUnsupportedVersion
# Place call arguments into local vars, if available def pretty_print(self):
for i in range(0, len(args)): "Display a ZRoutine nicely, for debugging purposes."
self.local_vars[i] = args[i]
log(f"ZRoutine: start address: {self.start_addr}")
def pretty_print(self): log(f"ZRoutine: return value address: {self.return_addr}")
"Display a ZRoutine nicely, for debugging purposes." log(f"ZRoutine: program counter: {self.program_counter}")
log(f"ZRoutine: local variables: {self.local_vars}")
log("ZRoutine: start address: %d" % self.start_addr)
log("ZRoutine: return value address: %d" % self.return_addr)
log("ZRoutine: program counter: %d" % self.program_counter)
log("ZRoutine: local variables: %d" % 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):
self._memory = zmem
self._stackbottom = ZStackBottom()
self._call_stack = [self._stackbottom]
def __init__(self, zmem): def get_local_variable(self, varnum):
"""Return value of local variable VARNUM from currently-running
routine. VARNUM must be a value between 0 and 15, and must
exist."""
self._memory = zmem if self._call_stack[-1] == self._stackbottom:
self._stackbottom = ZStackBottom() raise ZStackNoRoutine
self._call_stack = [self._stackbottom]
if not 0 <= varnum <= 15:
raise ZStackNoSuchVariable
def get_local_variable(self, varnum): current_routine = self._call_stack[-1]
"""Return value of local variable VARNUM from currently-running
routine. VARNUM must be a value between 0 and 15, and must
exist."""
if self._call_stack[-1] == self._stackbottom: return current_routine.local_vars[varnum] # type: ignore[possibly-missing-attribute]
raise ZStackNoRoutine
if not 0 <= varnum <= 15: def set_local_variable(self, varnum, value):
raise ZStackNoSuchVariable """Set value of local variable VARNUM to VALUE in
currently-running routine. VARNUM must be a value between 0 and
15, and must exist."""
current_routine = self._call_stack[-1] if self._call_stack[1] == self._stackbottom:
raise ZStackNoRoutine
return current_routine.local_vars[varnum] if not 0 <= varnum <= 15:
raise ZStackNoSuchVariable
current_routine = self._call_stack[-1]
def set_local_variable(self, varnum, value): current_routine.local_vars[varnum] = value # type: ignore[possibly-missing-attribute]
"""Set value of local variable VARNUM to VALUE in
currently-running routine. VARNUM must be a value between 0 and
15, and must exist."""
if self._call_stack[1] == self._stackbottom: def push_stack(self, value):
raise ZStackNoRoutine "Push VALUE onto the top of the current routine's data stack."
if not 0 <= varnum <= 15: current_routine = self._call_stack[-1]
raise ZStackNoSuchVariable current_routine.stack.append(value) # type: ignore[possibly-missing-attribute]
current_routine = self._call_stack[-1] def pop_stack(self):
"Remove and return value from the top of the data stack."
current_routine.local_vars[varnum] = value current_routine = self._call_stack[-1]
return current_routine.stack.pop() # type: ignore[possibly-missing-attribute]
def get_stack_frame_index(self):
"Return current stack frame number. For use by 'catch' opcode."
def push_stack(self, value): return len(self._call_stack) - 1
"Push VALUE onto the top of the current routine's data stack."
current_routine = self._call_stack[-1] # Used by quetzal save-file parser to reconstruct stack-frames.
current_routine.stack.append(value) def push_routine(self, routine):
"""Blindly push a ZRoutine object to the call stack.
WARNING: do not use this unless you know what you're doing; you
probably want the more full-featured start_routine() belowe
instead."""
self._call_stack.append(routine)
def pop_stack(self): # ZPU should call this whenever it decides to call a new routine.
"Remove and return value from the top of the data stack." def start_routine(self, routine_addr, return_addr, program_counter, args):
"""Save the state of the currenly running routine (by examining
the current value of the PROGRAM_COUNTER), and prepare for
execution of a new routine at ROUTINE_ADDR with list of initial
arguments ARGS."""
current_routine = self._call_stack[-1] new_routine = ZRoutine(routine_addr, return_addr, self._memory, args)
return current_routine.stack.pop() current_routine = self._call_stack[-1]
current_routine.program_counter = program_counter
self._call_stack.append(new_routine)
return new_routine.start_addr
def get_stack_frame_index(self): # ZPU should call this whenever it decides to return from current
"Return current stack frame number. For use by 'catch' opcode." # routine.
def finish_routine(self, return_value):
"""Toss the currently running routine from the call stack, and
toss any leftover values pushed to the data stack by said routine.
Return the previous routine's program counter address, so that
execution can resume where from it left off."""
return len(self._call_stack) - 1 exiting_routine = self._call_stack.pop()
current_routine = self._call_stack[-1]
# Depending on many things, return stuff.
if exiting_routine.return_addr is not None: # type: ignore[possibly-missing-attribute]
if exiting_routine.return_addr == 0: # type: ignore[possibly-missing-attribute]
# Push to stack
self.push_stack(return_value)
elif 0 < exiting_routine.return_addr < 10: # type: ignore[possibly-missing-attribute]
# Store in local var
self.set_local_variable(exiting_routine.return_addr, return_value) # type: ignore[possibly-missing-attribute]
else:
# Store in global var
self._memory.write_global(exiting_routine.return_addr, return_value) # type: ignore[possibly-missing-attribute]
# Used by quetzal save-file parser to reconstruct stack-frames. return current_routine.program_counter
def push_routine(self, routine):
"""Blindly push a ZRoutine object to the call stack.
WARNING: do not use this unless you know what you're doing; you
probably want the more full-featured start_routine() belowe
instead."""
self._call_stack.append(routine)
# ZPU should call this whenever it decides to call a new routine.
def start_routine(self, routine_addr, return_addr,
program_counter, args):
"""Save the state of the currenly running routine (by examining
the current value of the PROGRAM_COUNTER), and prepare for
execution of a new routine at ROUTINE_ADDR with list of initial
arguments ARGS."""
new_routine = ZRoutine(routine_addr, return_addr,
self._memory, args)
current_routine = self._call_stack[-1]
current_routine.program_counter = program_counter
self._call_stack.append(new_routine)
return new_routine.start_addr
# ZPU should call this whenever it decides to return from current
# routine.
def finish_routine(self, return_value):
"""Toss the currently running routine from the call stack, and
toss any leftover values pushed to the data stack by said routine.
Return the previous routine's program counter address, so that
execution can resume where from it left off."""
exiting_routine = self._call_stack.pop()
current_routine = self._call_stack[-1]
# Depending on many things, return stuff.
if exiting_routine.return_addr != None:
if exiting_routine.return_addr == 0: # Push to stack
self.push_stack(return_value)
elif 0 < exiting_routine.return_addr < 10: # Store in local var
self.set_local_variable(exiting_routine.return_addr,
return_value)
else: # Store in global var
self._memory.write_global(exiting_routine.return_addr,
return_value)
return current_routine.program_counter

View file

@ -5,94 +5,99 @@
# 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."""
def write(self, string): def write(self, string):
"""Prints the given unicode string to the output stream.""" """Prints the given unicode string to the output stream."""
raise NotImplementedError() raise NotImplementedError()
class ZBufferableOutputStream(ZOutputStream): class ZBufferableOutputStream(ZOutputStream):
"""Abstract class representing a buffered output stream for a """Abstract class representing a buffered output stream for a
z-machine, which can be optionally configured at run-time to provide z-machine, which can be optionally configured at run-time to provide
'buffering', also known as word-wrap.""" 'buffering', also known as word-wrap."""
def __init__(self): def __init__(self):
# This is a public variable that determines whether buffering is # This is a public variable that determines whether buffering is
# enabled for this stream or not. Subclasses can make it a # enabled for this stream or not. Subclasses can make it a
# Python property if necessary. # Python property if necessary.
self.buffer_mode = False self.buffer_mode = False
class ZInputStream: class ZInputStream:
"""Abstract class representing an input stream for a z-machine.""" """Abstract class representing an input stream for a z-machine."""
def __init__(self): def __init__(self):
"""Constructor for the input stream.""" """Constructor for the input stream."""
# Subclasses must define real values for all the features they # Subclasses must define real values for all the features they
# support (or don't support). # support (or don't support).
self.features = { self.features = {
"has_timed_input" : False, "has_timed_input": False,
} }
def read_line(self, original_text=None, max_length=0, def read_line(
terminating_characters=None, self,
timed_input_routine=None, timed_input_interval=0): original_text=None,
"""Reads from the input stream and returns a unicode string max_length=0,
representing the characters the end-user entered. The characters terminating_characters=None,
are displayed to the screen as the user types them. timed_input_routine=None,
timed_input_interval=0,
):
"""Reads from the input stream and returns a unicode string
representing the characters the end-user entered. The characters
are displayed to the screen as the user types them.
original_text, if provided, is pre-filled-in unicode text that the original_text, if provided, is pre-filled-in unicode text that the
end-user may delete or otherwise modify if they so choose. end-user may delete or otherwise modify if they so choose.
max_length is the maximum length, in characters, of the text that max_length is the maximum length, in characters, of the text that
the end-user may enter. Any typing the end-user does after these the end-user may enter. Any typing the end-user does after these
many characters have been entered is ignored. 0 means that there many characters have been entered is ignored. 0 means that there
is no practical limit to the number of characters the end-user can is no practical limit to the number of characters the end-user can
enter. enter.
terminating_characters is a string of unicode characters terminating_characters is a string of unicode characters
representing the characters that can signify the end of a line of representing the characters that can signify the end of a line of
input. If not provided, it defaults to a string containing a input. If not provided, it defaults to a string containing a
carriage return character ('\r'). The terminating character is carriage return character ('\r'). The terminating character is
not contained in the returned string. not contained in the returned string.
timed_input_routine is a function that will be called every timed_input_routine is a function that will be called every
time_input_interval milliseconds. This function should be of the time_input_interval milliseconds. This function should be of the
form: form:
def timed_input_routine(interval) def timed_input_routine(interval)
where interval is simply the value of timed_input_interval that where interval is simply the value of timed_input_interval that
was passed in to read_line(). The function should also return was passed in to read_line(). The function should also return
True if input should continue to be collected, or False if input True if input should continue to be collected, or False if input
should stop being collected; if False is returned, then should stop being collected; if False is returned, then
read_line() will return a unicode string representing the read_line() will return a unicode string representing the
characters typed so far. characters typed so far.
The timed input routine will be called from the same thread that The timed input routine will be called from the same thread that
called read_line(). called read_line().
Note, however, that supplying a timed input routine is only useful Note, however, that supplying a timed input routine is only useful
if the has_timed_input feature is supported by the input stream. if the has_timed_input feature is supported by the input stream.
If it is unsupported, then the timed input routine will not be If it is unsupported, then the timed input routine will not be
called.""" called."""
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.
timed_input_routine and timed_input_interval are the same as timed_input_routine and timed_input_interval are the same as
described in the documentation for read_line(). described in the documentation for read_line().
TODO: Should the character be automatically printed to the screen? TODO: Should the character be automatically printed to the screen?
The Z-Machine documentation for the read_char opcode, which this The Z-Machine documentation for the read_char opcode, which this
function is meant to ultimately implement, doesn't specify.""" function is meant to ultimately implement, doesn't specify."""
raise NotImplementedError() raise NotImplementedError()

View file

@ -9,9 +9,9 @@
# Constants for output streams. These are human-readable names for # Constants for output streams. These are human-readable names for
# the stream ID numbers as described in sections 7.1.1 and 7.1.2 # the stream ID numbers as described in sections 7.1.1 and 7.1.2
# of the Z-Machine Standards Document. # of the Z-Machine Standards Document.
OUTPUT_SCREEN = 1 # spews text to the the screen OUTPUT_SCREEN = 1 # spews text to the the screen
OUTPUT_TRANSCRIPT = 2 # contains everything player typed, plus our responses OUTPUT_TRANSCRIPT = 2 # contains everything player typed, plus our responses
OUTPUT_MEMORY = 3 # if the z-machine wants to write to memory OUTPUT_MEMORY = 3 # if the z-machine wants to write to memory
OUTPUT_PLAYER_INPUT = 4 # contains *only* the player's typed commands OUTPUT_PLAYER_INPUT = 4 # contains *only* the player's typed commands
# Constants for input streams. These are human-readable names for the # Constants for input streams. These are human-readable names for the
@ -20,78 +20,81 @@ 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."""
def __init__(self, zmem, zui): def __init__(self, zmem, zui):
# TODO: Actually set/create the streams as necessary. # TODO: Actually set/create the streams as necessary.
self._selectedStreams = [] self._selectedStreams = []
self._streams = {} self._streams = {}
def select(self, stream): def select(self, stream):
"""Selects the given stream ID for output.""" """Selects the given stream ID for output."""
if stream not in self._selectedStreams: if stream not in self._selectedStreams:
self._selectedStreams.append(stream) self._selectedStreams.append(stream)
def unselect(self, stream): def unselect(self, stream):
"""Unselects the given stream ID for output.""" """Unselects the given stream ID for output."""
if stream in self._selectedStreams: if stream in self._selectedStreams:
self._selectedStreams.remove(stream) self._selectedStreams.remove(stream)
def get(self, stream): def get(self, stream):
"""Retrieves the given stream ID.""" """Retrieves the given stream ID."""
return self._streams[stream] return self._streams[stream]
def write(self, string): def write(self, string):
"""Writes the given unicode string to all currently selected output """Writes the given unicode string to all currently selected output
streams.""" streams."""
# TODO: Implement section 7.1.2.2 of the Z-Machine Standards # TODO: Implement section 7.1.2.2 of the Z-Machine Standards
# Document, so that while stream 3 is selected, no text is # Document, so that while stream 3 is selected, no text is
# sent to any other output streams which are selected. (However, # sent to any other output streams which are selected. (However,
# they remain selected.). # they remain selected.).
# TODO: Implement section 7.1.2.2.1, so that newlines are written to # TODO: Implement section 7.1.2.2.1, so that newlines are written to
# output stream 3 as ZSCII 13. # output stream 3 as ZSCII 13.
# TODO: Implement section 7.1.2.3, so that whiles stream 4 is # TODO: Implement section 7.1.2.3, so that whiles stream 4 is
# selected, the only text printed to it is that of the player's # selected, the only text printed to it is that of the player's
# commands and keypresses (as read by read_char). This may not # commands and keypresses (as read by read_char). This may not
# ultimately happen via this method. # ultimately happen via this method.
for stream in self._selectedStreams:
self._streams[stream].write(string)
for stream in self._selectedStreams:
self._streams[stream].write(string)
class ZInputStreamManager: class ZInputStreamManager:
"""Manages input streams for a Z-Machine.""" """Manages input streams for a Z-Machine."""
def __init__(self, zui): def __init__(self, zui):
# TODO: Actually set/create the streams as necessary. # TODO: Actually set/create the streams as necessary.
self._selectedStream = None self._selectedStream = None
self._streams = {} self._streams = {}
def select(self, stream): def select(self, stream):
"""Selects the given stream ID as the currently active input stream.""" """Selects the given stream ID as the currently active input stream."""
# TODO: Ensure section 10.2.4, so that while stream 1 is selected, # TODO: Ensure section 10.2.4, so that while stream 1 is selected,
# the only text printed to it is that of the player's commands and # the only text printed to it is that of the player's commands and
# keypresses (as read by read_char). Not sure where this logic # keypresses (as read by read_char). Not sure where this logic
# will ultimately go, however. # will ultimately go, however.
self._selectedStream = stream self._selectedStream = stream
def getSelected(self): def getSelected(self):
"""Returns the input stream object for the currently active input """Returns the input stream object for the currently active input
stream.""" stream."""
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)
self.output = ZOutputStreamManager(zmem, zui) self.output = ZOutputStreamManager(zmem, 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 = []
@ -34,7 +36,7 @@ class ZStringTranslator:
def _read_char(self, pos): def _read_char(self, pos):
offset = (2 - pos[2]) * 5 offset = (2 - pos[2]) * 5
return pos[1][offset:offset+5] return pos[1][offset : offset + 5]
def _is_final(self, pos): def _is_final(self, pos):
return pos[1][15] == 1 return pos[1][15] == 1
@ -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"]
@ -95,7 +94,7 @@ class ZCharTranslator:
return None return None
alph_addr = self._mem.read_word(0x34) alph_addr = self._mem.read_word(0x34)
alphabet = self._mem[alph_addr:alph_addr+78] alphabet = self._mem[alph_addr : alph_addr + 78]
return [alphabet[0:26], alphabet[26:52], alphabet[52:78]] return [alphabet[0:26], alphabet[26:52], alphabet[52:78]]
def _load_abbrev_tables(self): def _load_abbrev_tables(self):
@ -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:
@ -173,7 +172,7 @@ class ZCharTranslator:
3: lambda s: shift_alphabet(s, -1, False), 3: lambda s: shift_alphabet(s, -1, False),
4: lambda s: shift_alphabet(s, +1, True), 4: lambda s: shift_alphabet(s, +1, True),
5: lambda s: shift_alphabet(s, -1, True), 5: lambda s: shift_alphabet(s, -1, True),
} }
elif self._mem.version == 2: elif self._mem.version == 2:
self._specials = { self._specials = {
1: lambda s: abbreviation(s, 0), 1: lambda s: abbreviation(s, 0),
@ -181,44 +180,44 @@ class ZCharTranslator:
3: lambda s: shift_alphabet(s, -1, False), 3: lambda s: shift_alphabet(s, -1, False),
4: lambda s: shift_alphabet(s, +1, True), 4: lambda s: shift_alphabet(s, +1, True),
5: lambda s: shift_alphabet(s, -1, True), 5: lambda s: shift_alphabet(s, -1, True),
} }
else: # ZM v3-5 else: # ZM v3-5
self._specials = { self._specials = {
1: lambda s: abbreviation(s, 0), 1: lambda s: abbreviation(s, 0),
2: lambda s: abbreviation(s, 1), 2: lambda s: abbreviation(s, 1),
3: lambda s: abbreviation(s, 2), 3: lambda s: abbreviation(s, 2),
4: lambda s: shift_alphabet(s, +1, False), 4: lambda s: shift_alphabet(s, +1, False),
5: lambda s: shift_alphabet(s, -1, False), 5: lambda s: shift_alphabet(s, -1, False),
} }
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,19 +359,14 @@ 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()
# Populate the input and output tables with the ASCII and UTT # Populate the input and output tables with the ASCII and UTT
# characters. # characters.
for code,char in [(x,chr(x)) for x in range(32,127)]: for code, char in [(x, chr(x)) for x in range(32, 127)]:
self._output_table[code] = char self._output_table[code] = char
self._input_table[char] = code self._input_table[char] = code
@ -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: