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
- `scripts/` - standalone tools (map renderer, etc)
- `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

2
.gitignore vendored
View file

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

View file

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

View file

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

View file

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

View file

@ -29,31 +29,41 @@ from .zlogging import log
# 4-byte chunkname, 4-byte length, length bytes of data
# ...
class QuetzalError(Exception):
"General exception for Quetzal classes."
pass
class QuetzalMalformedChunk(QuetzalError):
"Malformed chunk detected."
class QuetzalNoSuchSavefile(QuetzalError):
"Cannot locate save-game file."
class QuetzalUnrecognizedFileFormat(QuetzalError):
"Not a valid Quetzal file."
class QuetzalIllegalChunkOrder(QuetzalError):
"IFhd chunk came after Umem/Cmem/Stks chunks (see section 5.4)."
class QuetzalMismatchedFile(QuetzalError):
"Quetzal file dosen't match current game."
class QuetzalMemoryOutOfBounds(QuetzalError):
"Decompressed dynamic memory has gone out of bounds."
class QuetzalMemoryMismatch(QuetzalError):
"Savefile's dynamic memory image is incorrectly sized."
class QuetzalStackFrameOverflow(QuetzalError):
"Stack frame parsing went beyond bounds of 'Stks' chunk."
@ -67,7 +77,6 @@ class QuetzalParser:
self._seen_mem_or_stks = False
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,
@ -87,10 +96,10 @@ class QuetzalParser:
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)
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
@ -108,7 +117,6 @@ class QuetzalParser:
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."""
@ -123,7 +131,7 @@ class QuetzalParser:
savegame_mem = list(pmem[pmem._dynamic_start : (pmem._dynamic_end + 1)])
memlen = len(savegame_mem)
memcounter = 0
log(" Dynamic memory length is %d" % memlen)
log(f" Dynamic memory length is {memlen}")
self._last_loaded_metadata["memory length"] = memlen
runlength_bytes = data
@ -137,13 +145,13 @@ class QuetzalParser:
savegame_mem[memcounter] = byte ^ pmem[memcounter]
memcounter += 1
bytecounter += 1
log(" Set byte %d:%d" % (memcounter, savegame_mem[memcounter]))
log(f" Set byte {memcounter}:{savegame_mem[memcounter]}")
else:
bytecounter += 1
num_extra_zeros = runlength_bytes[bytecounter]
memcounter += (1 + num_extra_zeros)
memcounter += 1 + num_extra_zeros
bytecounter += 1
log(" Skipped %d unchanged bytes" % (1 + num_extra_zeros))
log(f" Skipped {1 + num_extra_zeros} unchanged bytes")
if memcounter >= memlen:
raise QuetzalMemoryOutOfBounds
@ -153,7 +161,6 @@ class QuetzalParser:
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."""
@ -166,7 +173,7 @@ class QuetzalParser:
cmem = self._zmachine._mem
dynamic_len = (cmem._dynamic_end - cmem.dynamic_start) + 1
log(" Dynamic memory length is %d" % dynamic_len)
log(f" Dynamic memory length is {dynamic_len}")
self._last_loaded_metadata["dynamic memory length"] = dynamic_len
savegame_mem = [ord(x) for x in data]
@ -176,7 +183,6 @@ class QuetzalParser:
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."""
@ -195,34 +201,34 @@ class QuetzalParser:
ptr = 0
# Read successive stack frames:
while (ptr < total_len):
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
_varnum = bytes[ptr] ### TODO: tells us which variable gets the result
ptr += 1
argflag = bytes[ptr]
_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]):
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))
log(f" Found {len(local_vars)} local vars")
# least recent to most recent stack values:
stack_values = []
for i in range(evalstack_size):
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))
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
@ -232,33 +238,32 @@ class QuetzalParser:
### 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)
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):
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:]
_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
@ -267,25 +272,23 @@ class QuetzalParser:
def _parse_auth(self, data):
"""Parse a chunk of type AUTH. Display the author."""
log("Author of file: %s" % data)
log(f"Author of file: {data}")
self._last_loaded_metadata["author"] = data
def _parse_copyright(self, data):
"""Parse a chunk of type (c) . Display the copyright."""
log("Copyright: (C) %s" % data)
log(f"Copyright: (C) {data}")
self._last_loaded_metadata["copyright"] = data
def _parse_anno(self, data):
"""Parse a chunk of type ANNO. Display any annotation"""
log("Annotation: %s" % data)
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."""
@ -300,8 +303,8 @@ class QuetzalParser:
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')
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.
@ -316,7 +319,7 @@ class QuetzalParser:
self._len += bytestring[1] << 16
self._len += bytestring[2] << 8
self._len += bytestring[3]
log("Total length of FORM data is %d" % self._len)
log(f"Total length of FORM data is {self._len}")
self._last_loaded_metadata["total length"] = self._len
type = self._file.read(4)
@ -329,7 +332,7 @@ class QuetzalParser:
chunkname = c.getname()
chunksize = c.getsize()
data = c.read(chunksize)
log("** Found chunk ID %s: length %d" % (chunkname, chunksize))
log(f"** Found chunk ID {chunkname}: length {chunksize}")
self._last_loaded_metadata[chunkname] = chunksize
if chunkname == b"IFhd":
@ -359,7 +362,6 @@ class QuetzalParser:
log("Finished parsing Quetzal file.")
# ------------------------------------------------------------------------------
@ -385,7 +387,6 @@ class QuetzalWriter:
return "0"
def _generate_cmem_chunk(self):
"""Return a compressed chunk of data representing the compressed
image of the zmachine's main memory."""
@ -396,9 +397,10 @@ class QuetzalWriter:
# XOR the original game image with the current one
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("XOR array is %s" % diffarray)
diffarray[index] = (
self._zmachine._pristine_mem[index] ^ self._zmachine._mem[index]
)
log(f"XOR array is {diffarray}")
# Run-length encode the resulting list of 0's and 1's.
result = []
@ -415,7 +417,6 @@ class QuetzalWriter:
result.append(diffarray[index])
return result
def _generate_stks_chunk(self):
"""Return a stacks chunk, describing the stack state of the
zmachine at this moment."""
@ -423,7 +424,6 @@ class QuetzalWriter:
### TODO: write this
return "0"
def _generate_anno_chunk(self):
"""Return an annotation chunk, containing metadata about the ZVM
interpreter which created the savefile."""
@ -431,24 +431,23 @@ class QuetzalWriter:
### TODO: write this
return "0"
# --------- Public APIs -----------
def write(self, savefile_path):
"""Write the current zmachine state to a new Quetzal-file at
SAVEFILE_PATH."""
log("Attempting to write game-state to '%s'" % savefile_path)
self._file = open(savefile_path, 'w')
log(f"Attempting to write game-state to '{savefile_path}'")
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()
total_chunk_size = len(ifhd_chunk) + len(cmem_chunk) \
+ len(stks_chunk) + len(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")
@ -456,8 +455,8 @@ class QuetzalWriter:
self._file.write("IFZS")
# Write nested chunks.
for chunk in (ifhd_chunk, cmem_chunk, stks_chunk, anno_chunk):
self._file.write(chunk)
for chunk_data in (ifhd_chunk, cmem_chunk, stks_chunk, anno_chunk):
self._file.write(chunk_data)
log("Wrote a chunk.")
self._file.close()
log("Done writing game-state to savefile.")

View file

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

View file

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

View file

@ -10,6 +10,7 @@
# root directory of this distribution.
#
class ZFilesystem:
"""Encapsulates the interactions that the end-user has with the
filesystem."""
@ -27,7 +28,6 @@ class ZFilesystem:
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
@ -42,7 +42,6 @@ class ZFilesystem:
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
@ -53,7 +52,6 @@ class ZFilesystem:
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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -131,7 +131,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
return [self._rows, self._columns]
def select_window(self, window):
"""Select a window to be the 'active' window, and move that
window's cursor to the upper left.
@ -143,7 +142,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
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.
@ -153,7 +151,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
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.
@ -168,9 +165,7 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError()
def erase_window(self, window=WINDOW_LOWER,
color=COLOR_CURRENT):
def erase_window(self, window=WINDOW_LOWER, color=COLOR_CURRENT):
"""Erase WINDOW to background COLOR.
WINDOW should be one of WINDOW_UPPER or WINDOW_LOWER.
@ -185,7 +180,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError()
def erase_line(self):
"""Erase from the current cursor position to the end of its line
in the current window.
@ -196,7 +190,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError()
# Status Line
#
# These routines are only called if the has_status_line capability
@ -216,7 +209,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError()
def print_status_time(self, hours, minutes):
"""Print a status line in the upper window, as follows:
@ -229,7 +221,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError()
# Text Appearances
#
@ -238,7 +229,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
return [self._fontwidth, self._fontheight]
def set_font(self, font_number):
"""Set the current window's font to one of
@ -256,7 +246,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError()
def set_text_style(self, style):
"""Set the current text style to the given text style.
@ -281,7 +270,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
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.
@ -292,7 +280,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
raise NotImplementedError()
# Standard output
def write(self, string):

View file

@ -13,30 +13,40 @@ from .zlogging import log
class ZStackError(Exception):
"General exception for stack or routine-related errors"
pass
class ZStackUnsupportedVersion(ZStackError):
"Unsupported version of Z-story file."
pass
class ZStackNoRoutine(ZStackError):
"No routine is being executed."
pass
class ZStackNoSuchVariable(ZStackError):
"Trying to access non-existent local variable."
pass
class ZStackPopError(ZStackError):
"Nothing to pop from stack!"
pass
# Helper class used by ZStackManager; a 'routine' object which
# includes its own private stack of data.
class ZRoutine:
def __init__(self, start_addr, return_addr, zmem, args,
local_vars=None, stack=None):
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."""
@ -55,7 +65,7 @@ class ZRoutine:
else:
num_local_vars = zmem[self.start_addr]
if not (0 <= num_local_vars <= 15):
log("num local vars is %d" % num_local_vars)
log(f"num local vars is {num_local_vars}")
raise ZStackError
self.start_addr += 1
@ -74,31 +84,26 @@ class ZRoutine:
for i in range(0, len(args)):
self.local_vars[i] = args[i]
def pretty_print(self):
"Display a ZRoutine nicely, for debugging purposes."
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)
log(f"ZRoutine: start address: {self.start_addr}")
log(f"ZRoutine: return value address: {self.return_addr}")
log(f"ZRoutine: program counter: {self.program_counter}")
log(f"ZRoutine: local variables: {self.local_vars}")
class ZStackBottom:
def __init__(self):
self.program_counter = 0 # used as a cache only
class ZStackManager:
def __init__(self, zmem):
self._memory = zmem
self._stackbottom = ZStackBottom()
self._call_stack = [self._stackbottom]
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
@ -112,8 +117,7 @@ class ZStackManager:
current_routine = self._call_stack[-1]
return current_routine.local_vars[varnum]
return current_routine.local_vars[varnum] # type: ignore[possibly-missing-attribute]
def set_local_variable(self, varnum, value):
"""Set value of local variable VARNUM to VALUE in
@ -128,29 +132,25 @@ class ZStackManager:
current_routine = self._call_stack[-1]
current_routine.local_vars[varnum] = value
current_routine.local_vars[varnum] = value # type: ignore[possibly-missing-attribute]
def push_stack(self, value):
"Push VALUE onto the top of the current routine's data stack."
current_routine = self._call_stack[-1]
current_routine.stack.append(value)
current_routine.stack.append(value) # type: ignore[possibly-missing-attribute]
def pop_stack(self):
"Remove and return value from the top of the data stack."
current_routine = self._call_stack[-1]
return current_routine.stack.pop()
return current_routine.stack.pop() # type: ignore[possibly-missing-attribute]
def get_stack_frame_index(self):
"Return current stack frame number. For use by 'catch' opcode."
return len(self._call_stack) - 1
# Used by quetzal save-file parser to reconstruct stack-frames.
def push_routine(self, routine):
"""Blindly push a ZRoutine object to the call stack.
@ -160,24 +160,20 @@ class ZStackManager:
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):
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)
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):
@ -190,15 +186,15 @@ class ZStackManager:
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
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: # 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)
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]
return current_routine.program_counter

View file

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

View file

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

View file

@ -11,6 +11,7 @@ import itertools
class ZStringEndOfString(Exception):
"""No more data left in string."""
class ZStringIllegalAbbrevInString(Exception):
"""String abbreviation encountered within a string in a context
where it is not allowed."""
@ -22,6 +23,7 @@ class ZStringTranslator:
def get(self, addr):
from .bitfield import BitField
pos = (addr, BitField(self._mem.read_word(addr)), 0)
s = []
@ -50,16 +52,13 @@ class ZStringTranslator:
# Kill processing.
raise ZStringEndOfString
# Get and return the next block.
return (pos[0] + 2,
BitField(self._mem.read_word(pos[0] + 2)),
0)
return (pos[0] + 2, BitField(self._mem.read_word(pos[0] + 2)), 0)
# Just increment the intra-block counter.
return (pos[0], pos[1], offset)
class ZCharTranslator:
# The default alphabet tables for ZChar translation.
# As the codes 0-5 are special, alphabets start with code 0x6.
DEFAULT_A0 = [ord(x) for x in "abcdefghijklmnopqrstuvwxyz"]
@ -109,8 +108,7 @@ class ZCharTranslator:
xlator = ZStringTranslator(self._mem)
def _load_subtable(num, base):
for i,zoff in [(i,base+(num*64)+(i*2))
for i in range(0, 32)]:
for i, zoff in [(i, base + (num * 64) + (i * 2)) for i in range(0, 32)]:
zaddr = self._mem.read_word(zoff)
zstr = xlator.get(self._mem.word_address(zaddr))
zchr = self.get(zstr, allow_abbreviations=False)
@ -128,11 +126,12 @@ class ZCharTranslator:
"""Load the special character code handlers for the current
machine version.
"""
# The following three functions define the three possible
# special character code handlers.
def newline(state):
"""Append ZSCII 13 (newline) to the output."""
state['zscii'].append(13)
state["zscii"].append(13)
def shift_alphabet(state, direction, lock):
"""Shift the current alphaber up or down. If lock is
@ -140,9 +139,9 @@ class ZCharTranslator:
after outputting 1 character. Else, the alphabet will
remain unchanged until the next shift.
"""
state['curr_alpha'] = (state['curr_alpha'] + direction) % 3
state["curr_alpha"] = (state["curr_alpha"] + direction) % 3
if lock:
state['prev_alpha'] = state['curr_alpha']
state["prev_alpha"] = state["curr_alpha"]
def abbreviation(state, abbrev):
"""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
abbreviation. Set up a state handler to intercept the next
character and output the right abbreviation."""
def write_abbreviation(state, c, subtable):
state['zscii'] += self._abbrevs[(subtable, c)]
del state['state_handler']
state["zscii"] += self._abbrevs[(subtable, c)]
del state["state_handler"]
# If we're parsing an abbreviation, there should be no
# nested abbreviations. So this is just a sanity check for
# people feeding us bad stories.
if not state['allow_abbreviations']:
if not state["allow_abbreviations"]:
raise ZStringIllegalAbbrevInString
state['state_handler'] = lambda s,c: write_abbreviation(s, c,
abbrev)
state["state_handler"] = lambda s, c: write_abbreviation(s, c, abbrev)
# Register the specials handlers depending on machine version.
if self._mem.version == 1:
@ -192,33 +191,33 @@ class ZCharTranslator:
}
def _special_zscii(self, state, char):
if 'zscii_char' not in list(state.keys()):
state['zscii_char'] = char
if "zscii_char" not in list(state.keys()):
state["zscii_char"] = char
else:
zchar = (state['zscii_char'] << 5) + char
state['zscii'].append(zchar)
del state['zscii_char']
del state['state_handler']
zchar = (state["zscii_char"] << 5) + char
state["zscii"].append(zchar)
del state["zscii_char"]
del state["state_handler"]
def get(self, zstr, allow_abbreviations=True):
state = {
'curr_alpha': 0,
'prev_alpha': 0,
'zscii': [],
'allow_abbreviations': allow_abbreviations,
"curr_alpha": 0,
"prev_alpha": 0,
"zscii": [],
"allow_abbreviations": allow_abbreviations,
}
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
# 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()):
# Hand off per-ZM version special char handling.
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
state['state_handler'] = self._special_zscii
state["state_handler"] = self._special_zscii
else:
# Do the usual Thing: append a zscii code to the
# decoded sequence and revert to the "previous"
@ -227,36 +226,97 @@ class ZCharTranslator:
if c == 0:
# Append a space.
z = 32
elif state['curr_alpha'] == 2:
elif state["curr_alpha"] == 2:
# The symbol alphabet table only has 25 chars
# because of the A2/6 special char, so we need to
# adjust differently.
z = self._alphabet[state['curr_alpha']][c-7]
z = self._alphabet[state["curr_alpha"]][c - 7]
else:
z = self._alphabet[state['curr_alpha']][c-6]
state['zscii'].append(z)
state['curr_alpha'] = state['prev_alpha']
z = self._alphabet[state["curr_alpha"]][c - 6]
state["zscii"].append(z)
state["curr_alpha"] = state["prev_alpha"]
return state['zscii']
return state["zscii"]
class ZsciiTranslator:
# The default Unicode Translation Table that maps to ZSCII codes
# 155-251. The codes are unicode codepoints for a host of strange
# characters.
DEFAULT_UTT = [chr(x) for x in
(0xe4, 0xf6, 0xfc, 0xc4, 0xd6, 0xdc,
0xdf, 0xbb, 0xab, 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)]
DEFAULT_UTT = [
chr(x)
for x in (
0xE4,
0xF6,
0xFC,
0xC4,
0xD6,
0xDC,
0xDF,
0xBB,
0xAB,
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
# starts.
UTT_OFFSET = 155
@ -299,13 +359,8 @@ class ZsciiTranslator:
def __init__(self, zmem):
self._mem = zmem
self._output_table = {
0 : "",
10: "\n"
}
self._input_table = {
"\n": 10
}
self._output_table = {0: "", 10: "\n"}
self._input_table = {"\n": 10}
self._load_unicode_table()
@ -324,8 +379,11 @@ class ZsciiTranslator:
# Oh and we also pull the items from the subclass into this
# instance, so as to make reference to these special codes
# easier.
for name,code in [(c,v) for c,v in list(self.Input.__dict__.items())
if not c.startswith('__')]:
for name, code in [
(c, v)
for c, v in list(self.Input.__dict__.items())
if not c.startswith("__")
]:
self._input_table[code] = code
setattr(self, name, code)
@ -350,18 +408,22 @@ class ZsciiTranslator:
#
# Then there is a unicode translation table other than the
# default that needs loading.
if (ext_table_addr != 0 and
self._mem.read_word(ext_table_addr) >= 3 and
self._mem.read_word(ext_table_addr+6) != 0):
if (
ext_table_addr != 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
# 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
# the unicode translation table as a list of unicode
# 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]
else:
utt = self.DEFAULT_UTT
@ -380,7 +442,7 @@ class ZsciiTranslator:
try:
return self._output_table[index]
except KeyError:
raise IndexError("No such ZSCII character")
raise IndexError("No such ZSCII character") from None
def utoz(self, char):
"""Translate the given Unicode code into the corresponding
@ -389,10 +451,10 @@ class ZsciiTranslator:
try:
return self._input_table[char]
except KeyError:
raise IndexError("No such input character")
raise IndexError("No such input character") from None
def get(self, zscii):
return ''.join([self.ztou(c) for c in zscii])
return "".join([self.ztou(c) for c in zscii])
class ZStringFactory: