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:
parent
dcc952d4c5
commit
5ea030a0ac
20 changed files with 2345 additions and 2209 deletions
|
|
@ -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
2
.gitignore
vendored
|
|
@ -5,3 +5,5 @@ data
|
||||||
.worktrees
|
.worktrees
|
||||||
.testmondata
|
.testmondata
|
||||||
*.z*
|
*.z*
|
||||||
|
debug.log
|
||||||
|
disasm.log
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
# root directory of this distribution.
|
# root directory of this distribution.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class BitField:
|
class BitField:
|
||||||
"""An bitfield gives read/write access to the individual bits of a
|
"""An bitfield gives read/write access to the individual bits of a
|
||||||
value, in array and slice notation.
|
value, in array and slice notation.
|
||||||
|
|
@ -58,5 +59,4 @@ class BitField:
|
||||||
|
|
||||||
def to_str(self, len):
|
def to_str(self, len):
|
||||||
"""Print the binary representation of the bitfield."""
|
"""Print the binary representation of the bitfield."""
|
||||||
return ''.join(["%d" % self[i]
|
return "".join([f"{self[i]}" for i in range(len - 1, -1, -1)])
|
||||||
for i in range(len-1,-1,-1)])
|
|
||||||
|
|
|
||||||
|
|
@ -80,37 +80,36 @@ evtype_Redraw = 6
|
||||||
evtype_SoundNotify = 7
|
evtype_SoundNotify = 7
|
||||||
evtype_Hyperlink = 8
|
evtype_Hyperlink = 8
|
||||||
|
|
||||||
class event_t(ctypes.Structure):
|
|
||||||
_fields_ = [("type", glui32),
|
|
||||||
("win", winid_t),
|
|
||||||
("val1", glui32),
|
|
||||||
("val2", glui32)]
|
|
||||||
|
|
||||||
keycode_Unknown = 0xffffffff
|
class event_t(ctypes.Structure):
|
||||||
keycode_Left = 0xfffffffe
|
_fields_ = [("type", glui32), ("win", winid_t), ("val1", glui32), ("val2", glui32)]
|
||||||
keycode_Right = 0xfffffffd
|
|
||||||
keycode_Up = 0xfffffffc
|
|
||||||
keycode_Down = 0xfffffffb
|
keycode_Unknown = 0xFFFFFFFF
|
||||||
keycode_Return = 0xfffffffa
|
keycode_Left = 0xFFFFFFFE
|
||||||
keycode_Delete = 0xfffffff9
|
keycode_Right = 0xFFFFFFFD
|
||||||
keycode_Escape = 0xfffffff8
|
keycode_Up = 0xFFFFFFFC
|
||||||
keycode_Tab = 0xfffffff7
|
keycode_Down = 0xFFFFFFFB
|
||||||
keycode_PageUp = 0xfffffff6
|
keycode_Return = 0xFFFFFFFA
|
||||||
keycode_PageDown = 0xfffffff5
|
keycode_Delete = 0xFFFFFFF9
|
||||||
keycode_Home = 0xfffffff4
|
keycode_Escape = 0xFFFFFFF8
|
||||||
keycode_End = 0xfffffff3
|
keycode_Tab = 0xFFFFFFF7
|
||||||
keycode_Func1 = 0xffffffef
|
keycode_PageUp = 0xFFFFFFF6
|
||||||
keycode_Func2 = 0xffffffee
|
keycode_PageDown = 0xFFFFFFF5
|
||||||
keycode_Func3 = 0xffffffed
|
keycode_Home = 0xFFFFFFF4
|
||||||
keycode_Func4 = 0xffffffec
|
keycode_End = 0xFFFFFFF3
|
||||||
keycode_Func5 = 0xffffffeb
|
keycode_Func1 = 0xFFFFFFEF
|
||||||
keycode_Func6 = 0xffffffea
|
keycode_Func2 = 0xFFFFFFEE
|
||||||
keycode_Func7 = 0xffffffe9
|
keycode_Func3 = 0xFFFFFFED
|
||||||
keycode_Func8 = 0xffffffe8
|
keycode_Func4 = 0xFFFFFFEC
|
||||||
keycode_Func9 = 0xffffffe7
|
keycode_Func5 = 0xFFFFFFEB
|
||||||
keycode_Func10 = 0xffffffe6
|
keycode_Func6 = 0xFFFFFFEA
|
||||||
keycode_Func11 = 0xffffffe5
|
keycode_Func7 = 0xFFFFFFE9
|
||||||
keycode_Func12 = 0xffffffe4
|
keycode_Func8 = 0xFFFFFFE8
|
||||||
|
keycode_Func9 = 0xFFFFFFE7
|
||||||
|
keycode_Func10 = 0xFFFFFFE6
|
||||||
|
keycode_Func11 = 0xFFFFFFE5
|
||||||
|
keycode_Func12 = 0xFFFFFFE4
|
||||||
keycode_MAXVAL = 28
|
keycode_MAXVAL = 28
|
||||||
|
|
||||||
style_Normal = 0
|
style_Normal = 0
|
||||||
|
|
@ -126,9 +125,10 @@ style_User1 = 9
|
||||||
style_User2 = 10
|
style_User2 = 10
|
||||||
style_NUMSTYLES = 11
|
style_NUMSTYLES = 11
|
||||||
|
|
||||||
|
|
||||||
class stream_result_t(ctypes.Structure):
|
class stream_result_t(ctypes.Structure):
|
||||||
_fields_ = [("readcount", glui32),
|
_fields_ = [("readcount", glui32), ("writecount", glui32)]
|
||||||
("writecount", glui32)]
|
|
||||||
|
|
||||||
wintype_AllTypes = 0
|
wintype_AllTypes = 0
|
||||||
wintype_Pair = 1
|
wintype_Pair = 1
|
||||||
|
|
@ -141,17 +141,17 @@ winmethod_Left = 0x00
|
||||||
winmethod_Right = 0x01
|
winmethod_Right = 0x01
|
||||||
winmethod_Above = 0x02
|
winmethod_Above = 0x02
|
||||||
winmethod_Below = 0x03
|
winmethod_Below = 0x03
|
||||||
winmethod_DirMask = 0x0f
|
winmethod_DirMask = 0x0F
|
||||||
|
|
||||||
winmethod_Fixed = 0x10
|
winmethod_Fixed = 0x10
|
||||||
winmethod_Proportional = 0x20
|
winmethod_Proportional = 0x20
|
||||||
winmethod_DivisionMask = 0xf0
|
winmethod_DivisionMask = 0xF0
|
||||||
|
|
||||||
fileusage_Data = 0x00
|
fileusage_Data = 0x00
|
||||||
fileusage_SavedGame = 0x01
|
fileusage_SavedGame = 0x01
|
||||||
fileusage_Transcript = 0x02
|
fileusage_Transcript = 0x02
|
||||||
fileusage_InputRecord = 0x03
|
fileusage_InputRecord = 0x03
|
||||||
fileusage_TypeMask = 0x0f
|
fileusage_TypeMask = 0x0F
|
||||||
|
|
||||||
fileusage_TextMode = 0x100
|
fileusage_TextMode = 0x100
|
||||||
fileusage_BinaryMode = 0x000
|
fileusage_BinaryMode = 0x000
|
||||||
|
|
@ -190,17 +190,26 @@ CORE_GLK_LIB_API = [
|
||||||
(None, "glk_exit", ()),
|
(None, "glk_exit", ()),
|
||||||
(None, "glk_tick", ()),
|
(None, "glk_tick", ()),
|
||||||
(glui32, "glk_gestalt", (glui32, glui32)),
|
(glui32, "glk_gestalt", (glui32, glui32)),
|
||||||
(glui32, "glk_gestalt_ext", (glui32, glui32, ctypes.POINTER(glui32),
|
(glui32, "glk_gestalt_ext", (glui32, glui32, ctypes.POINTER(glui32), glui32)),
|
||||||
glui32)),
|
|
||||||
(winid_t, "glk_window_get_root", ()),
|
(winid_t, "glk_window_get_root", ()),
|
||||||
(winid_t, "glk_window_open", (winid_t, glui32, glui32, glui32, glui32)),
|
(winid_t, "glk_window_open", (winid_t, glui32, glui32, glui32, glui32)),
|
||||||
(None, "glk_window_close", (winid_t, ctypes.POINTER(stream_result_t))),
|
(None, "glk_window_close", (winid_t, ctypes.POINTER(stream_result_t))),
|
||||||
(None, "glk_window_get_size", (winid_t, ctypes.POINTER(glui32),
|
(
|
||||||
ctypes.POINTER(glui32)) ),
|
None,
|
||||||
|
"glk_window_get_size",
|
||||||
|
(winid_t, ctypes.POINTER(glui32), ctypes.POINTER(glui32)),
|
||||||
|
),
|
||||||
(None, "glk_window_set_arrangement", (winid_t, glui32, glui32, winid_t)),
|
(None, "glk_window_set_arrangement", (winid_t, glui32, glui32, winid_t)),
|
||||||
(None, "glk_window_get_arrangement", (winid_t, ctypes.POINTER(glui32),
|
(
|
||||||
|
None,
|
||||||
|
"glk_window_get_arrangement",
|
||||||
|
(
|
||||||
|
winid_t,
|
||||||
ctypes.POINTER(glui32),
|
ctypes.POINTER(glui32),
|
||||||
ctypes.POINTER(winid_t))),
|
ctypes.POINTER(glui32),
|
||||||
|
ctypes.POINTER(winid_t),
|
||||||
|
),
|
||||||
|
),
|
||||||
(winid_t, "glk_window_iterate", (winid_t, ctypes.POINTER(glui32))),
|
(winid_t, "glk_window_iterate", (winid_t, ctypes.POINTER(glui32))),
|
||||||
(glui32, "glk_window_get_rock", (winid_t,)),
|
(glui32, "glk_window_get_rock", (winid_t,)),
|
||||||
(glui32, "glk_window_get_type", (winid_t,)),
|
(glui32, "glk_window_get_type", (winid_t,)),
|
||||||
|
|
@ -213,8 +222,7 @@ CORE_GLK_LIB_API = [
|
||||||
(strid_t, "glk_window_get_echo_stream", (winid_t,)),
|
(strid_t, "glk_window_get_echo_stream", (winid_t,)),
|
||||||
(None, "glk_set_window", (winid_t,)),
|
(None, "glk_set_window", (winid_t,)),
|
||||||
(strid_t, "glk_stream_open_file", (frefid_t, glui32, glui32)),
|
(strid_t, "glk_stream_open_file", (frefid_t, glui32, glui32)),
|
||||||
(strid_t, "glk_stream_open_memory", (ctypes.c_char_p,
|
(strid_t, "glk_stream_open_memory", (ctypes.c_char_p, glui32, glui32, glui32)),
|
||||||
glui32, glui32, glui32)),
|
|
||||||
(None, "glk_stream_close", (strid_t, ctypes.POINTER(stream_result_t))),
|
(None, "glk_stream_close", (strid_t, ctypes.POINTER(stream_result_t))),
|
||||||
(strid_t, "glk_stream_iterate", (strid_t, ctypes.POINTER(glui32))),
|
(strid_t, "glk_stream_iterate", (strid_t, ctypes.POINTER(glui32))),
|
||||||
(glui32, "glk_stream_get_rock", (strid_t,)),
|
(glui32, "glk_stream_get_rock", (strid_t,)),
|
||||||
|
|
@ -236,14 +244,11 @@ CORE_GLK_LIB_API = [
|
||||||
(None, "glk_stylehint_set", (glui32, glui32, glui32, glsi32)),
|
(None, "glk_stylehint_set", (glui32, glui32, glui32, glsi32)),
|
||||||
(None, "glk_stylehint_clear", (glui32, glui32, glui32)),
|
(None, "glk_stylehint_clear", (glui32, glui32, glui32)),
|
||||||
(glui32, "glk_style_distinguish", (winid_t, glui32, glui32)),
|
(glui32, "glk_style_distinguish", (winid_t, glui32, glui32)),
|
||||||
(glui32, "glk_style_measure", (winid_t, glui32, glui32,
|
(glui32, "glk_style_measure", (winid_t, glui32, glui32, ctypes.POINTER(glui32))),
|
||||||
ctypes.POINTER(glui32))),
|
|
||||||
(frefid_t, "glk_fileref_create_temp", (glui32, glui32)),
|
(frefid_t, "glk_fileref_create_temp", (glui32, glui32)),
|
||||||
(frefid_t, "glk_fileref_create_by_name", (glui32, ctypes.c_char_p,
|
(frefid_t, "glk_fileref_create_by_name", (glui32, ctypes.c_char_p, glui32)),
|
||||||
glui32)),
|
|
||||||
(frefid_t, "glk_fileref_create_by_prompt", (glui32, glui32, glui32)),
|
(frefid_t, "glk_fileref_create_by_prompt", (glui32, glui32, glui32)),
|
||||||
(frefid_t, "glk_fileref_create_from_fileref", (glui32, frefid_t,
|
(frefid_t, "glk_fileref_create_from_fileref", (glui32, frefid_t, glui32)),
|
||||||
glui32)),
|
|
||||||
(None, "glk_fileref_destroy", (frefid_t,)),
|
(None, "glk_fileref_destroy", (frefid_t,)),
|
||||||
(frefid_t, "glk_fileref_iterate", (frefid_t, ctypes.POINTER(glui32))),
|
(frefid_t, "glk_fileref_iterate", (frefid_t, ctypes.POINTER(glui32))),
|
||||||
(glui32, "glk_fileref_get_rock", (frefid_t,)),
|
(glui32, "glk_fileref_get_rock", (frefid_t,)),
|
||||||
|
|
@ -252,8 +257,7 @@ CORE_GLK_LIB_API = [
|
||||||
(None, "glk_select", (ctypes.POINTER(event_t),)),
|
(None, "glk_select", (ctypes.POINTER(event_t),)),
|
||||||
(None, "glk_select_poll", (ctypes.POINTER(event_t),)),
|
(None, "glk_select_poll", (ctypes.POINTER(event_t),)),
|
||||||
(None, "glk_request_timer_events", (glui32,)),
|
(None, "glk_request_timer_events", (glui32,)),
|
||||||
(None, "glk_request_line_event", (winid_t, ctypes.c_char_p, glui32,
|
(None, "glk_request_line_event", (winid_t, ctypes.c_char_p, glui32, glui32)),
|
||||||
glui32)),
|
|
||||||
(None, "glk_request_char_event", (winid_t,)),
|
(None, "glk_request_char_event", (winid_t,)),
|
||||||
(None, "glk_request_mouse_event", (winid_t,)),
|
(None, "glk_request_mouse_event", (winid_t,)),
|
||||||
(None, "glk_cancel_line_event", (winid_t, ctypes.POINTER(event_t))),
|
(None, "glk_cancel_line_event", (winid_t, ctypes.POINTER(event_t))),
|
||||||
|
|
@ -269,21 +273,25 @@ UNICODE_GLK_LIB_API = [
|
||||||
(None, "glk_put_buffer_uni", (ctypes.POINTER(glui32), glui32)),
|
(None, "glk_put_buffer_uni", (ctypes.POINTER(glui32), glui32)),
|
||||||
(None, "glk_put_char_stream_uni", (strid_t, glui32)),
|
(None, "glk_put_char_stream_uni", (strid_t, glui32)),
|
||||||
(None, "glk_put_string_stream_uni", (strid_t, ctypes.POINTER(glui32))),
|
(None, "glk_put_string_stream_uni", (strid_t, ctypes.POINTER(glui32))),
|
||||||
(None, "glk_put_buffer_stream_uni", (strid_t, ctypes.POINTER(glui32),
|
(None, "glk_put_buffer_stream_uni", (strid_t, ctypes.POINTER(glui32), glui32)),
|
||||||
glui32)),
|
|
||||||
(glsi32, "glk_get_char_stream_uni", (strid_t,)),
|
(glsi32, "glk_get_char_stream_uni", (strid_t,)),
|
||||||
(glui32, "glk_get_buffer_stream_uni", (strid_t, ctypes.POINTER(glui32),
|
(glui32, "glk_get_buffer_stream_uni", (strid_t, ctypes.POINTER(glui32), glui32)),
|
||||||
glui32)),
|
(glui32, "glk_get_line_stream_uni", (strid_t, ctypes.POINTER(glui32), glui32)),
|
||||||
(glui32, "glk_get_line_stream_uni", (strid_t, ctypes.POINTER(glui32),
|
|
||||||
glui32)),
|
|
||||||
(strid_t, "glk_stream_open_file_uni", (frefid_t, glui32, glui32)),
|
(strid_t, "glk_stream_open_file_uni", (frefid_t, glui32, glui32)),
|
||||||
(strid_t, "glk_stream_open_memory_uni", (ctypes.POINTER(glui32),
|
(
|
||||||
glui32, glui32, glui32)),
|
strid_t,
|
||||||
|
"glk_stream_open_memory_uni",
|
||||||
|
(ctypes.POINTER(glui32), glui32, glui32, glui32),
|
||||||
|
),
|
||||||
(None, "glk_request_char_event_uni", (winid_t,)),
|
(None, "glk_request_char_event_uni", (winid_t,)),
|
||||||
(None, "glk_request_line_event_uni", (winid_t, ctypes.POINTER(glui32),
|
(
|
||||||
glui32, glui32))
|
None,
|
||||||
|
"glk_request_line_event_uni",
|
||||||
|
(winid_t, ctypes.POINTER(glui32), glui32, glui32),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class GlkLib:
|
class GlkLib:
|
||||||
"""Encapsulates the ctypes interface to a Glk shared library. When
|
"""Encapsulates the ctypes interface to a Glk shared library. When
|
||||||
instantiated, it wraps the shared library with the appropriate
|
instantiated, it wraps the shared library with the appropriate
|
||||||
|
|
@ -299,7 +307,7 @@ class GlkLib:
|
||||||
|
|
||||||
self.__bind_prototypes(CORE_GLK_LIB_API)
|
self.__bind_prototypes(CORE_GLK_LIB_API)
|
||||||
|
|
||||||
if self.glk_gestalt(gestalt_Unicode, 0) == 1:
|
if self.glk_gestalt(gestalt_Unicode, 0) == 1: # type: ignore[unresolved-attribute]
|
||||||
self.__bind_prototypes(UNICODE_GLK_LIB_API)
|
self.__bind_prototypes(UNICODE_GLK_LIB_API)
|
||||||
else:
|
else:
|
||||||
self.__bind_not_implemented_prototypes(UNICODE_GLK_LIB_API)
|
self.__bind_not_implemented_prototypes(UNICODE_GLK_LIB_API)
|
||||||
|
|
@ -324,8 +332,7 @@ class GlkLib:
|
||||||
support some optional extension of the Glk API."""
|
support some optional extension of the Glk API."""
|
||||||
|
|
||||||
def notImplementedFunction(*args, **kwargs):
|
def notImplementedFunction(*args, **kwargs):
|
||||||
raise NotImplementedError( "Function not implemented " \
|
raise NotImplementedError("Function not implemented by this Glk library.")
|
||||||
"by this Glk library." )
|
|
||||||
|
|
||||||
for function_prototype in function_prototypes:
|
for function_prototype in function_prototypes:
|
||||||
_, function_name, _ = function_prototype
|
_, function_name, _ = function_prototype
|
||||||
|
|
|
||||||
|
|
@ -29,31 +29,41 @@ from .zlogging import log
|
||||||
# 4-byte chunkname, 4-byte length, length bytes of data
|
# 4-byte chunkname, 4-byte length, length bytes of data
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
|
|
||||||
class QuetzalError(Exception):
|
class QuetzalError(Exception):
|
||||||
"General exception for Quetzal classes."
|
"General exception for Quetzal classes."
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class QuetzalMalformedChunk(QuetzalError):
|
class QuetzalMalformedChunk(QuetzalError):
|
||||||
"Malformed chunk detected."
|
"Malformed chunk detected."
|
||||||
|
|
||||||
|
|
||||||
class QuetzalNoSuchSavefile(QuetzalError):
|
class QuetzalNoSuchSavefile(QuetzalError):
|
||||||
"Cannot locate save-game file."
|
"Cannot locate save-game file."
|
||||||
|
|
||||||
|
|
||||||
class QuetzalUnrecognizedFileFormat(QuetzalError):
|
class QuetzalUnrecognizedFileFormat(QuetzalError):
|
||||||
"Not a valid Quetzal file."
|
"Not a valid Quetzal file."
|
||||||
|
|
||||||
|
|
||||||
class QuetzalIllegalChunkOrder(QuetzalError):
|
class QuetzalIllegalChunkOrder(QuetzalError):
|
||||||
"IFhd chunk came after Umem/Cmem/Stks chunks (see section 5.4)."
|
"IFhd chunk came after Umem/Cmem/Stks chunks (see section 5.4)."
|
||||||
|
|
||||||
|
|
||||||
class QuetzalMismatchedFile(QuetzalError):
|
class QuetzalMismatchedFile(QuetzalError):
|
||||||
"Quetzal file dosen't match current game."
|
"Quetzal file dosen't match current game."
|
||||||
|
|
||||||
|
|
||||||
class QuetzalMemoryOutOfBounds(QuetzalError):
|
class QuetzalMemoryOutOfBounds(QuetzalError):
|
||||||
"Decompressed dynamic memory has gone out of bounds."
|
"Decompressed dynamic memory has gone out of bounds."
|
||||||
|
|
||||||
|
|
||||||
class QuetzalMemoryMismatch(QuetzalError):
|
class QuetzalMemoryMismatch(QuetzalError):
|
||||||
"Savefile's dynamic memory image is incorrectly sized."
|
"Savefile's dynamic memory image is incorrectly sized."
|
||||||
|
|
||||||
|
|
||||||
class QuetzalStackFrameOverflow(QuetzalError):
|
class QuetzalStackFrameOverflow(QuetzalError):
|
||||||
"Stack frame parsing went beyond bounds of 'Stks' chunk."
|
"Stack frame parsing went beyond bounds of 'Stks' chunk."
|
||||||
|
|
||||||
|
|
@ -67,7 +77,6 @@ class QuetzalParser:
|
||||||
self._seen_mem_or_stks = False
|
self._seen_mem_or_stks = False
|
||||||
self._last_loaded_metadata = {} # metadata for tests & debugging
|
self._last_loaded_metadata = {} # metadata for tests & debugging
|
||||||
|
|
||||||
|
|
||||||
def _parse_ifhd(self, data):
|
def _parse_ifhd(self, data):
|
||||||
"""Parse a chunk of type IFhd, and check that the quetzal file
|
"""Parse a chunk of type IFhd, and check that the quetzal file
|
||||||
really belongs to the current story (by comparing release number,
|
really belongs to the current story (by comparing release number,
|
||||||
|
|
@ -87,10 +96,10 @@ class QuetzalParser:
|
||||||
chunk_pc = (data[10] << 16) + (data[11] << 8) + data[12]
|
chunk_pc = (data[10] << 16) + (data[11] << 8) + data[12]
|
||||||
self._zmachine._opdecoder.program_counter = chunk_pc
|
self._zmachine._opdecoder.program_counter = chunk_pc
|
||||||
|
|
||||||
log(" Found release number %d" % chunk_release)
|
log(f" Found release number {chunk_release}")
|
||||||
log(" Found serial number %d" % int(chunk_serial))
|
log(f" Found serial number {int(chunk_serial)}")
|
||||||
log(" Found checksum %d" % chunk_checksum)
|
log(f" Found checksum {chunk_checksum}")
|
||||||
log(" Initial program counter value is %d" % chunk_pc)
|
log(f" Initial program counter value is {chunk_pc}")
|
||||||
self._last_loaded_metadata["release number"] = chunk_release
|
self._last_loaded_metadata["release number"] = chunk_release
|
||||||
self._last_loaded_metadata["serial number"] = chunk_serial
|
self._last_loaded_metadata["serial number"] = chunk_serial
|
||||||
self._last_loaded_metadata["checksum"] = chunk_checksum
|
self._last_loaded_metadata["checksum"] = chunk_checksum
|
||||||
|
|
@ -108,7 +117,6 @@ class QuetzalParser:
|
||||||
raise QuetzalMismatchedFile
|
raise QuetzalMismatchedFile
|
||||||
log(" Quetzal file correctly verifies against original story.")
|
log(" Quetzal file correctly verifies against original story.")
|
||||||
|
|
||||||
|
|
||||||
def _parse_cmem(self, data):
|
def _parse_cmem(self, data):
|
||||||
"""Parse a chunk of type Cmem. Decompress an image of dynamic
|
"""Parse a chunk of type Cmem. Decompress an image of dynamic
|
||||||
memory, and place it into the ZMachine."""
|
memory, and place it into the ZMachine."""
|
||||||
|
|
@ -123,7 +131,7 @@ class QuetzalParser:
|
||||||
savegame_mem = list(pmem[pmem._dynamic_start : (pmem._dynamic_end + 1)])
|
savegame_mem = list(pmem[pmem._dynamic_start : (pmem._dynamic_end + 1)])
|
||||||
memlen = len(savegame_mem)
|
memlen = len(savegame_mem)
|
||||||
memcounter = 0
|
memcounter = 0
|
||||||
log(" Dynamic memory length is %d" % memlen)
|
log(f" Dynamic memory length is {memlen}")
|
||||||
self._last_loaded_metadata["memory length"] = memlen
|
self._last_loaded_metadata["memory length"] = memlen
|
||||||
|
|
||||||
runlength_bytes = data
|
runlength_bytes = data
|
||||||
|
|
@ -137,13 +145,13 @@ class QuetzalParser:
|
||||||
savegame_mem[memcounter] = byte ^ pmem[memcounter]
|
savegame_mem[memcounter] = byte ^ pmem[memcounter]
|
||||||
memcounter += 1
|
memcounter += 1
|
||||||
bytecounter += 1
|
bytecounter += 1
|
||||||
log(" Set byte %d:%d" % (memcounter, savegame_mem[memcounter]))
|
log(f" Set byte {memcounter}:{savegame_mem[memcounter]}")
|
||||||
else:
|
else:
|
||||||
bytecounter += 1
|
bytecounter += 1
|
||||||
num_extra_zeros = runlength_bytes[bytecounter]
|
num_extra_zeros = runlength_bytes[bytecounter]
|
||||||
memcounter += (1 + num_extra_zeros)
|
memcounter += 1 + num_extra_zeros
|
||||||
bytecounter += 1
|
bytecounter += 1
|
||||||
log(" Skipped %d unchanged bytes" % (1 + num_extra_zeros))
|
log(f" Skipped {1 + num_extra_zeros} unchanged bytes")
|
||||||
if memcounter >= memlen:
|
if memcounter >= memlen:
|
||||||
raise QuetzalMemoryOutOfBounds
|
raise QuetzalMemoryOutOfBounds
|
||||||
|
|
||||||
|
|
@ -153,7 +161,6 @@ class QuetzalParser:
|
||||||
cmem[cmem._dynamic_start : (cmem._dynamic_end + 1)] = savegame_mem
|
cmem[cmem._dynamic_start : (cmem._dynamic_end + 1)] = savegame_mem
|
||||||
log(" Successfully installed new dynamic memory.")
|
log(" Successfully installed new dynamic memory.")
|
||||||
|
|
||||||
|
|
||||||
def _parse_umem(self, data):
|
def _parse_umem(self, data):
|
||||||
"""Parse a chunk of type Umem. Suck a raw image of dynamic memory
|
"""Parse a chunk of type Umem. Suck a raw image of dynamic memory
|
||||||
and place it into the ZMachine."""
|
and place it into the ZMachine."""
|
||||||
|
|
@ -166,7 +173,7 @@ class QuetzalParser:
|
||||||
|
|
||||||
cmem = self._zmachine._mem
|
cmem = self._zmachine._mem
|
||||||
dynamic_len = (cmem._dynamic_end - cmem.dynamic_start) + 1
|
dynamic_len = (cmem._dynamic_end - cmem.dynamic_start) + 1
|
||||||
log(" Dynamic memory length is %d" % dynamic_len)
|
log(f" Dynamic memory length is {dynamic_len}")
|
||||||
self._last_loaded_metadata["dynamic memory length"] = dynamic_len
|
self._last_loaded_metadata["dynamic memory length"] = dynamic_len
|
||||||
|
|
||||||
savegame_mem = [ord(x) for x in data]
|
savegame_mem = [ord(x) for x in data]
|
||||||
|
|
@ -176,7 +183,6 @@ class QuetzalParser:
|
||||||
cmem[cmem._dynamic_start : (cmem._dynamic_end + 1)] = savegame_mem
|
cmem[cmem._dynamic_start : (cmem._dynamic_end + 1)] = savegame_mem
|
||||||
log(" Successfully installed new dynamic memory.")
|
log(" Successfully installed new dynamic memory.")
|
||||||
|
|
||||||
|
|
||||||
def _parse_stks(self, data):
|
def _parse_stks(self, data):
|
||||||
"""Parse a chunk of type Stks."""
|
"""Parse a chunk of type Stks."""
|
||||||
|
|
||||||
|
|
@ -195,34 +201,34 @@ class QuetzalParser:
|
||||||
ptr = 0
|
ptr = 0
|
||||||
|
|
||||||
# Read successive stack frames:
|
# Read successive stack frames:
|
||||||
while (ptr < total_len):
|
while ptr < total_len:
|
||||||
log(" Parsing stack frame...")
|
log(" Parsing stack frame...")
|
||||||
return_pc = (bytes[ptr] << 16) + (bytes[ptr + 1] << 8) + bytes[ptr + 3]
|
return_pc = (bytes[ptr] << 16) + (bytes[ptr + 1] << 8) + bytes[ptr + 3]
|
||||||
ptr += 3
|
ptr += 3
|
||||||
flags_bitfield = bitfield.BitField(bytes[ptr])
|
flags_bitfield = bitfield.BitField(bytes[ptr])
|
||||||
ptr += 1
|
ptr += 1
|
||||||
varnum = bytes[ptr] ### TODO: tells us which variable gets the result
|
_varnum = bytes[ptr] ### TODO: tells us which variable gets the result
|
||||||
ptr += 1
|
ptr += 1
|
||||||
argflag = bytes[ptr]
|
_argflag = bytes[ptr]
|
||||||
ptr += 1
|
ptr += 1
|
||||||
evalstack_size = (bytes[ptr] << 8) + bytes[ptr + 1]
|
evalstack_size = (bytes[ptr] << 8) + bytes[ptr + 1]
|
||||||
ptr += 2
|
ptr += 2
|
||||||
|
|
||||||
# read anywhere from 0 to 15 local vars
|
# read anywhere from 0 to 15 local vars
|
||||||
local_vars = []
|
local_vars = []
|
||||||
for i in range(flags_bitfield[0:3]):
|
for _i in range(flags_bitfield[0:3]):
|
||||||
var = (bytes[ptr] << 8) + bytes[ptr + 1]
|
var = (bytes[ptr] << 8) + bytes[ptr + 1]
|
||||||
ptr += 2
|
ptr += 2
|
||||||
local_vars.append(var)
|
local_vars.append(var)
|
||||||
log(" Found %d local vars" % len(local_vars))
|
log(f" Found {len(local_vars)} local vars")
|
||||||
|
|
||||||
# least recent to most recent stack values:
|
# least recent to most recent stack values:
|
||||||
stack_values = []
|
stack_values = []
|
||||||
for i in range(evalstack_size):
|
for _i in range(evalstack_size):
|
||||||
val = (bytes[ptr] << 8) + bytes[ptr + 1]
|
val = (bytes[ptr] << 8) + bytes[ptr + 1]
|
||||||
ptr += 2
|
ptr += 2
|
||||||
stack_values.append(val)
|
stack_values.append(val)
|
||||||
log(" Found %d local stack values" % len(stack_values))
|
log(f" Found {len(stack_values)} local stack values")
|
||||||
|
|
||||||
### Interesting... the reconstructed stack frames have no 'start
|
### Interesting... the reconstructed stack frames have no 'start
|
||||||
### address'. I guess it doesn't matter, since we only need to
|
### address'. I guess it doesn't matter, since we only need to
|
||||||
|
|
@ -232,33 +238,32 @@ class QuetzalParser:
|
||||||
### TODO: I can exactly which of the 7 args is "supplied", but I
|
### TODO: I can exactly which of the 7 args is "supplied", but I
|
||||||
### don't understand where the args *are*??
|
### don't understand where the args *are*??
|
||||||
|
|
||||||
routine = zstackmanager.ZRoutine(0, return_pc, self._zmachine._mem,
|
routine = zstackmanager.ZRoutine(
|
||||||
[], local_vars, stack_values)
|
0, return_pc, self._zmachine._mem, [], local_vars, stack_values
|
||||||
|
)
|
||||||
stackmanager.push_routine(routine)
|
stackmanager.push_routine(routine)
|
||||||
log(" Added new frame to stack.")
|
log(" Added new frame to stack.")
|
||||||
|
|
||||||
if (ptr > total_len):
|
if ptr > total_len:
|
||||||
raise QuetzalStackFrameOverflow
|
raise QuetzalStackFrameOverflow
|
||||||
|
|
||||||
self._zmachine._stackmanager = stackmanager
|
self._zmachine._stackmanager = stackmanager
|
||||||
log(" Successfully installed all stack frames.")
|
log(" Successfully installed all stack frames.")
|
||||||
|
|
||||||
|
|
||||||
def _parse_intd(self, data):
|
def _parse_intd(self, data):
|
||||||
"""Parse a chunk of type IntD, which is interpreter-dependent info."""
|
"""Parse a chunk of type IntD, which is interpreter-dependent info."""
|
||||||
|
|
||||||
log(" Begin parsing of interpreter-dependent metadata")
|
log(" Begin parsing of interpreter-dependent metadata")
|
||||||
bytes = [ord(x) for x in data]
|
bytes = [ord(x) for x in data]
|
||||||
|
|
||||||
os_id = bytes[0:3]
|
_os_id = bytes[0:3]
|
||||||
flags = bytes[4]
|
_flags = bytes[4]
|
||||||
contents_id = bytes[5]
|
_contents_id = bytes[5]
|
||||||
reserved = bytes[6:8]
|
_reserved = bytes[6:8]
|
||||||
interpreter_id = bytes[8:12]
|
_interpreter_id = bytes[8:12]
|
||||||
private_data = bytes[12:]
|
_private_data = bytes[12:]
|
||||||
### TODO: finish this
|
### TODO: finish this
|
||||||
|
|
||||||
|
|
||||||
# The following 3 chunks are totally optional metadata, and are
|
# The following 3 chunks are totally optional metadata, and are
|
||||||
# artifacts of the larger IFF standard. We're not required to do
|
# artifacts of the larger IFF standard. We're not required to do
|
||||||
# anything when we see them, though maybe it would be nice to print
|
# anything when we see them, though maybe it would be nice to print
|
||||||
|
|
@ -267,25 +272,23 @@ class QuetzalParser:
|
||||||
def _parse_auth(self, data):
|
def _parse_auth(self, data):
|
||||||
"""Parse a chunk of type AUTH. Display the author."""
|
"""Parse a chunk of type AUTH. Display the author."""
|
||||||
|
|
||||||
log("Author of file: %s" % data)
|
log(f"Author of file: {data}")
|
||||||
self._last_loaded_metadata["author"] = data
|
self._last_loaded_metadata["author"] = data
|
||||||
|
|
||||||
def _parse_copyright(self, data):
|
def _parse_copyright(self, data):
|
||||||
"""Parse a chunk of type (c) . Display the copyright."""
|
"""Parse a chunk of type (c) . Display the copyright."""
|
||||||
|
|
||||||
log("Copyright: (C) %s" % data)
|
log(f"Copyright: (C) {data}")
|
||||||
self._last_loaded_metadata["copyright"] = data
|
self._last_loaded_metadata["copyright"] = data
|
||||||
|
|
||||||
def _parse_anno(self, data):
|
def _parse_anno(self, data):
|
||||||
"""Parse a chunk of type ANNO. Display any annotation"""
|
"""Parse a chunk of type ANNO. Display any annotation"""
|
||||||
|
|
||||||
log("Annotation: %s" % data)
|
log(f"Annotation: {data}")
|
||||||
self._last_loaded_metadata["annotation"] = data
|
self._last_loaded_metadata["annotation"] = data
|
||||||
|
|
||||||
|
|
||||||
# --------- Public APIs -----------
|
# --------- Public APIs -----------
|
||||||
|
|
||||||
|
|
||||||
def get_last_loaded(self):
|
def get_last_loaded(self):
|
||||||
"""Return a list of metadata about the last loaded Quetzal file, for
|
"""Return a list of metadata about the last loaded Quetzal file, for
|
||||||
debugging and test verification."""
|
debugging and test verification."""
|
||||||
|
|
@ -300,8 +303,8 @@ class QuetzalParser:
|
||||||
if not os.path.isfile(savefile_path):
|
if not os.path.isfile(savefile_path):
|
||||||
raise QuetzalNoSuchSavefile
|
raise QuetzalNoSuchSavefile
|
||||||
|
|
||||||
log("Attempting to load saved game from '%s'" % savefile_path)
|
log(f"Attempting to load saved game from '{savefile_path}'")
|
||||||
self._file = open(savefile_path, 'rb')
|
self._file = open(savefile_path, "rb") # noqa: SIM115
|
||||||
|
|
||||||
# The python 'chunk' module is pretty dumb; it doesn't understand
|
# The python 'chunk' module is pretty dumb; it doesn't understand
|
||||||
# the FORM chunk and the way it contains nested chunks.
|
# the FORM chunk and the way it contains nested chunks.
|
||||||
|
|
@ -316,7 +319,7 @@ class QuetzalParser:
|
||||||
self._len += bytestring[1] << 16
|
self._len += bytestring[1] << 16
|
||||||
self._len += bytestring[2] << 8
|
self._len += bytestring[2] << 8
|
||||||
self._len += bytestring[3]
|
self._len += bytestring[3]
|
||||||
log("Total length of FORM data is %d" % self._len)
|
log(f"Total length of FORM data is {self._len}")
|
||||||
self._last_loaded_metadata["total length"] = self._len
|
self._last_loaded_metadata["total length"] = self._len
|
||||||
|
|
||||||
type = self._file.read(4)
|
type = self._file.read(4)
|
||||||
|
|
@ -329,7 +332,7 @@ class QuetzalParser:
|
||||||
chunkname = c.getname()
|
chunkname = c.getname()
|
||||||
chunksize = c.getsize()
|
chunksize = c.getsize()
|
||||||
data = c.read(chunksize)
|
data = c.read(chunksize)
|
||||||
log("** Found chunk ID %s: length %d" % (chunkname, chunksize))
|
log(f"** Found chunk ID {chunkname}: length {chunksize}")
|
||||||
self._last_loaded_metadata[chunkname] = chunksize
|
self._last_loaded_metadata[chunkname] = chunksize
|
||||||
|
|
||||||
if chunkname == b"IFhd":
|
if chunkname == b"IFhd":
|
||||||
|
|
@ -359,7 +362,6 @@ class QuetzalParser:
|
||||||
log("Finished parsing Quetzal file.")
|
log("Finished parsing Quetzal file.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -385,7 +387,6 @@ class QuetzalWriter:
|
||||||
|
|
||||||
return "0"
|
return "0"
|
||||||
|
|
||||||
|
|
||||||
def _generate_cmem_chunk(self):
|
def _generate_cmem_chunk(self):
|
||||||
"""Return a compressed chunk of data representing the compressed
|
"""Return a compressed chunk of data representing the compressed
|
||||||
image of the zmachine's main memory."""
|
image of the zmachine's main memory."""
|
||||||
|
|
@ -396,9 +397,10 @@ class QuetzalWriter:
|
||||||
# XOR the original game image with the current one
|
# XOR the original game image with the current one
|
||||||
diffarray = list(self._zmachine._pristine_mem)
|
diffarray = list(self._zmachine._pristine_mem)
|
||||||
for index in range(len(self._zmachine._pristine_mem._total_size)):
|
for index in range(len(self._zmachine._pristine_mem._total_size)):
|
||||||
diffarray[index] = self._zmachine._pristine_mem[index] \
|
diffarray[index] = (
|
||||||
^ self._zmachine._mem[index]
|
self._zmachine._pristine_mem[index] ^ self._zmachine._mem[index]
|
||||||
log("XOR array is %s" % diffarray)
|
)
|
||||||
|
log(f"XOR array is {diffarray}")
|
||||||
|
|
||||||
# Run-length encode the resulting list of 0's and 1's.
|
# Run-length encode the resulting list of 0's and 1's.
|
||||||
result = []
|
result = []
|
||||||
|
|
@ -415,7 +417,6 @@ class QuetzalWriter:
|
||||||
result.append(diffarray[index])
|
result.append(diffarray[index])
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _generate_stks_chunk(self):
|
def _generate_stks_chunk(self):
|
||||||
"""Return a stacks chunk, describing the stack state of the
|
"""Return a stacks chunk, describing the stack state of the
|
||||||
zmachine at this moment."""
|
zmachine at this moment."""
|
||||||
|
|
@ -423,7 +424,6 @@ class QuetzalWriter:
|
||||||
### TODO: write this
|
### TODO: write this
|
||||||
return "0"
|
return "0"
|
||||||
|
|
||||||
|
|
||||||
def _generate_anno_chunk(self):
|
def _generate_anno_chunk(self):
|
||||||
"""Return an annotation chunk, containing metadata about the ZVM
|
"""Return an annotation chunk, containing metadata about the ZVM
|
||||||
interpreter which created the savefile."""
|
interpreter which created the savefile."""
|
||||||
|
|
@ -431,24 +431,23 @@ class QuetzalWriter:
|
||||||
### TODO: write this
|
### TODO: write this
|
||||||
return "0"
|
return "0"
|
||||||
|
|
||||||
|
|
||||||
# --------- Public APIs -----------
|
# --------- Public APIs -----------
|
||||||
|
|
||||||
|
|
||||||
def write(self, savefile_path):
|
def write(self, savefile_path):
|
||||||
"""Write the current zmachine state to a new Quetzal-file at
|
"""Write the current zmachine state to a new Quetzal-file at
|
||||||
SAVEFILE_PATH."""
|
SAVEFILE_PATH."""
|
||||||
|
|
||||||
log("Attempting to write game-state to '%s'" % savefile_path)
|
log(f"Attempting to write game-state to '{savefile_path}'")
|
||||||
self._file = open(savefile_path, 'w')
|
self._file = open(savefile_path, "w") # noqa: SIM115
|
||||||
|
|
||||||
ifhd_chunk = self._generate_ifhd_chunk()
|
ifhd_chunk = self._generate_ifhd_chunk()
|
||||||
cmem_chunk = self._generate_cmem_chunk()
|
cmem_chunk = self._generate_cmem_chunk()
|
||||||
stks_chunk = self._generate_stks_chunk()
|
stks_chunk = self._generate_stks_chunk()
|
||||||
anno_chunk = self._generate_anno_chunk()
|
anno_chunk = self._generate_anno_chunk()
|
||||||
|
|
||||||
total_chunk_size = len(ifhd_chunk) + len(cmem_chunk) \
|
_total_chunk_size = (
|
||||||
+ len(stks_chunk) + len(anno_chunk)
|
len(ifhd_chunk) + len(cmem_chunk) + len(stks_chunk) + len(anno_chunk)
|
||||||
|
)
|
||||||
|
|
||||||
# Write main FORM chunk to hold other chunks
|
# Write main FORM chunk to hold other chunks
|
||||||
self._file.write("FORM")
|
self._file.write("FORM")
|
||||||
|
|
@ -456,8 +455,8 @@ class QuetzalWriter:
|
||||||
self._file.write("IFZS")
|
self._file.write("IFZS")
|
||||||
|
|
||||||
# Write nested chunks.
|
# Write nested chunks.
|
||||||
for chunk in (ifhd_chunk, cmem_chunk, stks_chunk, anno_chunk):
|
for chunk_data in (ifhd_chunk, cmem_chunk, stks_chunk, anno_chunk):
|
||||||
self._file.write(chunk)
|
self._file.write(chunk_data)
|
||||||
log("Wrote a chunk.")
|
log("Wrote a chunk.")
|
||||||
self._file.close()
|
self._file.close()
|
||||||
log("Done writing game-state to savefile.")
|
log("Done writing game-state to savefile.")
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ class TrivialAudio(zaudio.ZAudio):
|
||||||
elif bleep_type == zaudio.BLEEP_LOW:
|
elif bleep_type == zaudio.BLEEP_LOW:
|
||||||
sys.stdout.write("AUDIO: low-pitched bleep\n")
|
sys.stdout.write("AUDIO: low-pitched bleep\n")
|
||||||
else:
|
else:
|
||||||
raise AssertionError("Invalid bleep_type: %s" % str(bleep_type))
|
raise AssertionError(f"Invalid bleep_type: {str(bleep_type)}")
|
||||||
|
|
||||||
|
|
||||||
class TrivialScreen(zscreen.ZScreen):
|
class TrivialScreen(zscreen.ZScreen):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -49,17 +50,16 @@ class TrivialScreen(zscreen.ZScreen):
|
||||||
self.__rows_since_last_input = 0
|
self.__rows_since_last_input = 0
|
||||||
|
|
||||||
def split_window(self, height):
|
def split_window(self, height):
|
||||||
log("TODO: split window here to height %d" % height)
|
log(f"TODO: split window here to height {height}")
|
||||||
|
|
||||||
def select_window(self, window_num):
|
def select_window(self, window):
|
||||||
log("TODO: select window %d here" % window_num)
|
log(f"TODO: select window {window} here")
|
||||||
|
|
||||||
def set_cursor_position(self, x, y):
|
def set_cursor_position(self, x, y):
|
||||||
log("TODO: set cursor position to (%d,%d) here" % (x,y))
|
log(f"TODO: set cursor position to ({x},{y}) here")
|
||||||
|
|
||||||
def erase_window(self, window=zscreen.WINDOW_LOWER,
|
def erase_window(self, window=zscreen.WINDOW_LOWER, color=zscreen.COLOR_CURRENT):
|
||||||
color=zscreen.COLOR_CURRENT):
|
for _row in range(self._rows):
|
||||||
for row in range(self._rows):
|
|
||||||
sys.stdout.write("\n")
|
sys.stdout.write("\n")
|
||||||
self.__curr_column = 0
|
self.__curr_column = 0
|
||||||
self.__rows_since_last_input = 0
|
self.__rows_since_last_input = 0
|
||||||
|
|
@ -89,14 +89,13 @@ class TrivialScreen(zscreen.ZScreen):
|
||||||
then erase the [MORE] prompt, leaving the cursor at the same
|
then erase the [MORE] prompt, leaving the cursor at the same
|
||||||
position that it was at before the call was made."""
|
position that it was at before the call was made."""
|
||||||
|
|
||||||
assert self.__curr_column == 0, \
|
assert self.__curr_column == 0, "Precondition: current column must be zero."
|
||||||
"Precondition: current column must be zero."
|
|
||||||
|
|
||||||
MORE_STRING = "[MORE]"
|
MORE_STRING = "[MORE]"
|
||||||
sys.stdout.write(MORE_STRING)
|
sys.stdout.write(MORE_STRING)
|
||||||
_read_char()
|
_read_char()
|
||||||
# Erase the [MORE] prompt and reset the cursor position.
|
# Erase the [MORE] prompt and reset the cursor position.
|
||||||
sys.stdout.write("\r%s\r" % (" " * len(MORE_STRING)))
|
sys.stdout.write(f"\r{' ' * len(MORE_STRING)}\r")
|
||||||
self.__rows_since_last_input = 0
|
self.__rows_since_last_input = 0
|
||||||
|
|
||||||
def on_input_occurred(self, newline_occurred=False):
|
def on_input_occurred(self, newline_occurred=False):
|
||||||
|
|
@ -129,8 +128,10 @@ class TrivialScreen(zscreen.ZScreen):
|
||||||
if newline_printed:
|
if newline_printed:
|
||||||
self.__rows_since_last_input += 1
|
self.__rows_since_last_input += 1
|
||||||
self.__curr_column = 0
|
self.__curr_column = 0
|
||||||
if (self.__rows_since_last_input == self._rows and
|
if (
|
||||||
self._rows != zscreen.INFINITE_ROWS):
|
self.__rows_since_last_input == self._rows
|
||||||
|
and self._rows != zscreen.INFINITE_ROWS
|
||||||
|
):
|
||||||
self.__show_more_prompt()
|
self.__show_more_prompt()
|
||||||
|
|
||||||
def write(self, string):
|
def write(self, string):
|
||||||
|
|
@ -154,6 +155,7 @@ class TrivialScreen(zscreen.ZScreen):
|
||||||
|
|
||||||
self.__unbuffered_write(string)
|
self.__unbuffered_write(string)
|
||||||
|
|
||||||
|
|
||||||
class TrivialKeyboardInputStream(zstream.ZInputStream):
|
class TrivialKeyboardInputStream(zstream.ZInputStream):
|
||||||
def __init__(self, screen):
|
def __init__(self, screen):
|
||||||
zstream.ZInputStream.__init__(self)
|
zstream.ZInputStream.__init__(self)
|
||||||
|
|
@ -162,9 +164,14 @@ class TrivialKeyboardInputStream(zstream.ZInputStream):
|
||||||
"has_timed_input": False,
|
"has_timed_input": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
def read_line(self, original_text=None, max_length=0,
|
def read_line(
|
||||||
|
self,
|
||||||
|
original_text=None,
|
||||||
|
max_length=0,
|
||||||
terminating_characters=None,
|
terminating_characters=None,
|
||||||
timed_input_routine=None, timed_input_interval=0):
|
timed_input_routine=None,
|
||||||
|
timed_input_interval=0,
|
||||||
|
):
|
||||||
result = _read_line(original_text, terminating_characters)
|
result = _read_line(original_text, terminating_characters)
|
||||||
if max_length > 0:
|
if max_length > 0:
|
||||||
result = result[:max_length]
|
result = result[:max_length]
|
||||||
|
|
@ -176,27 +183,25 @@ class TrivialKeyboardInputStream(zstream.ZInputStream):
|
||||||
|
|
||||||
return str(result)
|
return str(result)
|
||||||
|
|
||||||
def read_char(self, timed_input_routine=None,
|
def read_char(self, timed_input_routine=None, timed_input_interval=0):
|
||||||
timed_input_interval=0):
|
|
||||||
result = _read_char()
|
result = _read_char()
|
||||||
self.__screen.on_input_occurred()
|
self.__screen.on_input_occurred()
|
||||||
return ord(result)
|
return ord(result)
|
||||||
|
|
||||||
|
|
||||||
class TrivialFilesystem(zfilesystem.ZFilesystem):
|
class TrivialFilesystem(zfilesystem.ZFilesystem):
|
||||||
def __report_io_error(self, exception):
|
def __report_io_error(self, exception):
|
||||||
sys.stdout.write("FILESYSTEM: An error occurred: %s\n" % exception)
|
sys.stdout.write(f"FILESYSTEM: An error occurred: {exception}\n")
|
||||||
|
|
||||||
def save_game(self, data, suggested_filename=None):
|
def save_game(self, data, suggested_filename=None):
|
||||||
success = False
|
success = False
|
||||||
|
|
||||||
sys.stdout.write("Enter a name for the saved game " \
|
sys.stdout.write("Enter a name for the saved game (hit enter to cancel): ")
|
||||||
"(hit enter to cancel): ")
|
|
||||||
filename = _read_line(suggested_filename)
|
filename = _read_line(suggested_filename)
|
||||||
if filename:
|
if filename:
|
||||||
try:
|
try:
|
||||||
file_obj = open(filename, "wb")
|
with open(filename, "wb") as file_obj:
|
||||||
file_obj.write(data)
|
file_obj.write(data)
|
||||||
file_obj.close()
|
|
||||||
success = True
|
success = True
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.__report_io_error(e)
|
self.__report_io_error(e)
|
||||||
|
|
@ -206,14 +211,14 @@ class TrivialFilesystem(zfilesystem.ZFilesystem):
|
||||||
def restore_game(self):
|
def restore_game(self):
|
||||||
data = None
|
data = None
|
||||||
|
|
||||||
sys.stdout.write("Enter the name of the saved game to restore " \
|
sys.stdout.write(
|
||||||
"(hit enter to cancel): ")
|
"Enter the name of the saved game to restore (hit enter to cancel): "
|
||||||
|
)
|
||||||
filename = _read_line()
|
filename = _read_line()
|
||||||
if filename:
|
if filename:
|
||||||
try:
|
try:
|
||||||
file_obj = open(filename, "rb")
|
with open(filename, "rb") as file_obj:
|
||||||
data = file_obj.read()
|
data = file_obj.read()
|
||||||
file_obj.close()
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.__report_io_error(e)
|
self.__report_io_error(e)
|
||||||
|
|
||||||
|
|
@ -222,12 +227,11 @@ class TrivialFilesystem(zfilesystem.ZFilesystem):
|
||||||
def open_transcript_file_for_writing(self):
|
def open_transcript_file_for_writing(self):
|
||||||
file_obj = None
|
file_obj = None
|
||||||
|
|
||||||
sys.stdout.write("Enter a name for the transcript file " \
|
sys.stdout.write("Enter a name for the transcript file (hit enter to cancel): ")
|
||||||
"(hit enter to cancel): ")
|
|
||||||
filename = _read_line()
|
filename = _read_line()
|
||||||
if filename:
|
if filename:
|
||||||
try:
|
try:
|
||||||
file_obj = open(filename, "w")
|
file_obj = open(filename, "w") # noqa: SIM115
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.__report_io_error(e)
|
self.__report_io_error(e)
|
||||||
|
|
||||||
|
|
@ -236,17 +240,19 @@ class TrivialFilesystem(zfilesystem.ZFilesystem):
|
||||||
def open_transcript_file_for_reading(self):
|
def open_transcript_file_for_reading(self):
|
||||||
file_obj = None
|
file_obj = None
|
||||||
|
|
||||||
sys.stdout.write("Enter the name of the transcript file to read " \
|
sys.stdout.write(
|
||||||
"(hit enter to cancel): ")
|
"Enter the name of the transcript file to read (hit enter to cancel): "
|
||||||
|
)
|
||||||
filename = _read_line()
|
filename = _read_line()
|
||||||
if filename:
|
if filename:
|
||||||
try:
|
try:
|
||||||
file_obj = open(filename)
|
file_obj = open(filename) # noqa: SIM115
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.__report_io_error(e)
|
self.__report_io_error(e)
|
||||||
|
|
||||||
return file_obj
|
return file_obj
|
||||||
|
|
||||||
|
|
||||||
def create_zui():
|
def create_zui():
|
||||||
"""Creates and returns a ZUI instance representing a trivial user
|
"""Creates and returns a ZUI instance representing a trivial user
|
||||||
interface."""
|
interface."""
|
||||||
|
|
@ -256,12 +262,8 @@ def create_zui():
|
||||||
keyboard_input = TrivialKeyboardInputStream(screen)
|
keyboard_input = TrivialKeyboardInputStream(screen)
|
||||||
filesystem = TrivialFilesystem()
|
filesystem = TrivialFilesystem()
|
||||||
|
|
||||||
return zui.ZUI(
|
return zui.ZUI(audio, screen, keyboard_input, filesystem)
|
||||||
audio,
|
|
||||||
screen,
|
|
||||||
keyboard_input,
|
|
||||||
filesystem
|
|
||||||
)
|
|
||||||
|
|
||||||
# Keyboard input functions
|
# Keyboard input functions
|
||||||
|
|
||||||
|
|
@ -269,13 +271,15 @@ _INTERRUPT_CHAR = chr(3)
|
||||||
_BACKSPACE_CHAR = chr(8)
|
_BACKSPACE_CHAR = chr(8)
|
||||||
_DELETE_CHAR = chr(127)
|
_DELETE_CHAR = chr(127)
|
||||||
|
|
||||||
|
|
||||||
def _win32_read_char():
|
def _win32_read_char():
|
||||||
"""Win32-specific function that reads a character of input from the
|
"""Win32-specific function that reads a character of input from the
|
||||||
keyboard and returns it without printing it to the screen."""
|
keyboard and returns it without printing it to the screen."""
|
||||||
|
|
||||||
import msvcrt
|
import msvcrt
|
||||||
|
|
||||||
return str(msvcrt.getch())
|
return str(msvcrt.getch()) # type: ignore[possibly-missing-attribute]
|
||||||
|
|
||||||
|
|
||||||
def _unix_read_char():
|
def _unix_read_char():
|
||||||
"""Unix-specific function that reads a character of input from the
|
"""Unix-specific function that reads a character of input from the
|
||||||
|
|
@ -296,6 +300,7 @@ def _unix_read_char():
|
||||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||||
return str(ch)
|
return str(ch)
|
||||||
|
|
||||||
|
|
||||||
def _read_char():
|
def _read_char():
|
||||||
"""Reads a character of input from the keyboard and returns it
|
"""Reads a character of input from the keyboard and returns it
|
||||||
without printing it to the screen."""
|
without printing it to the screen."""
|
||||||
|
|
@ -312,6 +317,7 @@ def _read_char():
|
||||||
else:
|
else:
|
||||||
return char
|
return char
|
||||||
|
|
||||||
|
|
||||||
def _read_line(original_text=None, terminating_characters=None):
|
def _read_line(original_text=None, terminating_characters=None):
|
||||||
"""Reads a line of input with the given unicode string of original
|
"""Reads a line of input with the given unicode string of original
|
||||||
text, which is editable, and the given unicode string of terminating
|
text, which is editable, and the given unicode string of terminating
|
||||||
|
|
@ -319,7 +325,7 @@ def _read_line(original_text=None, terminating_characters=None):
|
||||||
terminating_characters is a string containing the carriage return
|
terminating_characters is a string containing the carriage return
|
||||||
character ('\r')."""
|
character ('\r')."""
|
||||||
|
|
||||||
if original_text == None:
|
if original_text is None:
|
||||||
original_text = ""
|
original_text = ""
|
||||||
if not terminating_characters:
|
if not terminating_characters:
|
||||||
terminating_characters = "\r"
|
terminating_characters = "\r"
|
||||||
|
|
@ -349,15 +355,17 @@ def _read_line(original_text=None, terminating_characters=None):
|
||||||
if char == "\r":
|
if char == "\r":
|
||||||
char_to_print = "\n"
|
char_to_print = "\n"
|
||||||
elif char == _BACKSPACE_CHAR:
|
elif char == _BACKSPACE_CHAR:
|
||||||
char_to_print = "%s %s" % (_BACKSPACE_CHAR, _BACKSPACE_CHAR)
|
char_to_print = f"{_BACKSPACE_CHAR} {_BACKSPACE_CHAR}"
|
||||||
else:
|
else:
|
||||||
char_to_print = char
|
char_to_print = char
|
||||||
|
|
||||||
sys.stdout.write(char_to_print)
|
sys.stdout.write(char_to_print)
|
||||||
return string
|
return string
|
||||||
|
|
||||||
|
|
||||||
# Word wrapping helper function
|
# Word wrapping helper function
|
||||||
|
|
||||||
|
|
||||||
def _word_wrap(text, width):
|
def _word_wrap(text, width):
|
||||||
"""
|
"""
|
||||||
A word-wrap function that preserves existing line breaks
|
A word-wrap function that preserves existing line breaks
|
||||||
|
|
@ -368,11 +376,16 @@ def _word_wrap(text, width):
|
||||||
# This code was taken from:
|
# This code was taken from:
|
||||||
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061
|
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061
|
||||||
|
|
||||||
return reduce(lambda line, word, width=width: '%s%s%s' %
|
return reduce(
|
||||||
(line,
|
lambda line, word, width=width: "{}{}{}".format(
|
||||||
' \n'[(len(line)-line.rfind('\n')-1
|
line,
|
||||||
+ len(word.split('\n',1)[0]
|
" \n"[
|
||||||
) >= width)],
|
(
|
||||||
word),
|
len(line) - line.rfind("\n") - 1 + len(word.split("\n", 1)[0])
|
||||||
text.split(' ')
|
>= width
|
||||||
|
)
|
||||||
|
],
|
||||||
|
word,
|
||||||
|
),
|
||||||
|
text.split(" "),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ EFFECT_START = 2
|
||||||
EFFECT_STOP = 3
|
EFFECT_STOP = 3
|
||||||
EFFECT_FINISH = 4
|
EFFECT_FINISH = 4
|
||||||
|
|
||||||
|
|
||||||
class ZAudio:
|
class ZAudio:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Constructor of the audio system."""
|
"""Constructor of the audio system."""
|
||||||
|
|
@ -42,8 +43,7 @@ class ZAudio:
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def play_sound_effect(self, id, effect, volume, repeats,
|
def play_sound_effect(self, id, effect, volume, repeats, routine=None):
|
||||||
routine=None):
|
|
||||||
"""The given effect happens to the given sound number. The id
|
"""The given effect happens to the given sound number. The id
|
||||||
must be 3 or above is supplied by the ZAudio object for the
|
must be 3 or above is supplied by the ZAudio object for the
|
||||||
particular game in question.
|
particular game in question.
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
# root directory of this distribution.
|
# root directory of this distribution.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class ZFilesystem:
|
class ZFilesystem:
|
||||||
"""Encapsulates the interactions that the end-user has with the
|
"""Encapsulates the interactions that the end-user has with the
|
||||||
filesystem."""
|
filesystem."""
|
||||||
|
|
@ -27,7 +28,6 @@ class ZFilesystem:
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def restore_game(self):
|
def restore_game(self):
|
||||||
"""Prompt for a filename, and return file's contents. (Presumably
|
"""Prompt for a filename, and return file's contents. (Presumably
|
||||||
the interpreter will attempt to use those contents to restore a
|
the interpreter will attempt to use those contents to restore a
|
||||||
|
|
@ -42,7 +42,6 @@ class ZFilesystem:
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def open_transcript_file_for_writing(self):
|
def open_transcript_file_for_writing(self):
|
||||||
"""Prompt for a filename in which to save either a full game
|
"""Prompt for a filename in which to save either a full game
|
||||||
transcript or just a list of the user's commands. Return standard
|
transcript or just a list of the user's commands. Return standard
|
||||||
|
|
@ -53,7 +52,6 @@ class ZFilesystem:
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def open_transcript_file_for_reading(self):
|
def open_transcript_file_for_reading(self):
|
||||||
"""Prompt for a filename contain user commands, which can be used
|
"""Prompt for a filename contain user commands, which can be used
|
||||||
to drive the interpreter. Return standard python file object that
|
to drive the interpreter. Return standard python file object that
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ from .zstring import ZsciiTranslator, ZStringFactory
|
||||||
class ZLexerError(Exception):
|
class ZLexerError(Exception):
|
||||||
"General exception for ZLexer class"
|
"General exception for ZLexer class"
|
||||||
|
|
||||||
|
|
||||||
# Note that the specification describes tokenisation as a process
|
# Note that the specification describes tokenisation as a process
|
||||||
# whereby the user's input is divided into words, each word converted
|
# whereby the user's input is divided into words, each word converted
|
||||||
# to a z-string, then searched for in the 'standard' dictionary. This
|
# to a z-string, then searched for in the 'standard' dictionary. This
|
||||||
|
|
@ -26,21 +27,20 @@ class ZLexerError(Exception):
|
||||||
# Note that the main API here (tokenise_input()) can work with any
|
# Note that the main API here (tokenise_input()) can work with any
|
||||||
# dictionary, not just the standard one.
|
# dictionary, not just the standard one.
|
||||||
|
|
||||||
|
|
||||||
class ZLexer:
|
class ZLexer:
|
||||||
|
|
||||||
def __init__(self, mem):
|
def __init__(self, mem):
|
||||||
|
|
||||||
self._memory = mem
|
self._memory = mem
|
||||||
self._stringfactory = ZStringFactory(self._memory)
|
self._stringfactory = ZStringFactory(self._memory)
|
||||||
self._zsciitranslator = ZsciiTranslator(self._memory)
|
self._zsciitranslator = ZsciiTranslator(self._memory)
|
||||||
|
|
||||||
# Load and parse game's 'standard' dictionary from static memory.
|
# Load and parse game's 'standard' dictionary from static memory.
|
||||||
dict_addr = self._memory.read_word(0x08)
|
dict_addr = self._memory.read_word(0x08)
|
||||||
self._num_entries, self._entry_length, self._separators, entries_addr = \
|
self._num_entries, self._entry_length, self._separators, entries_addr = (
|
||||||
self._parse_dict_header(dict_addr)
|
self._parse_dict_header(dict_addr)
|
||||||
|
)
|
||||||
self._dict = self.get_dictionary(dict_addr)
|
self._dict = self.get_dictionary(dict_addr)
|
||||||
|
|
||||||
|
|
||||||
def _parse_dict_header(self, address):
|
def _parse_dict_header(self, address):
|
||||||
"""Parse the header of the dictionary at ADDRESS. Return the
|
"""Parse the header of the dictionary at ADDRESS. Return the
|
||||||
number of entries, the length of each entry, a list of zscii
|
number of entries, the length of each entry, a list of zscii
|
||||||
|
|
@ -49,7 +49,7 @@ class ZLexer:
|
||||||
addr = address
|
addr = address
|
||||||
num_separators = self._memory[addr]
|
num_separators = self._memory[addr]
|
||||||
separators = self._memory[(addr + 1) : (addr + num_separators)]
|
separators = self._memory[(addr + 1) : (addr + num_separators)]
|
||||||
addr += (1 + num_separators)
|
addr += 1 + num_separators
|
||||||
entry_length = self._memory[addr]
|
entry_length = self._memory[addr]
|
||||||
addr += 1
|
addr += 1
|
||||||
num_entries = self._memory.read_word(addr)
|
num_entries = self._memory.read_word(addr)
|
||||||
|
|
@ -57,7 +57,6 @@ class ZLexer:
|
||||||
|
|
||||||
return num_entries, entry_length, separators, addr
|
return num_entries, entry_length, separators, addr
|
||||||
|
|
||||||
|
|
||||||
def _tokenise_string(self, string, separators):
|
def _tokenise_string(self, string, separators):
|
||||||
"""Split unicode STRING into a list of words, and return the list.
|
"""Split unicode STRING into a list of words, and return the list.
|
||||||
Whitespace always counts as a word separator, but so do any
|
Whitespace always counts as a word separator, but so do any
|
||||||
|
|
@ -69,17 +68,12 @@ class ZLexer:
|
||||||
sep_string = ""
|
sep_string = ""
|
||||||
for sep in separators:
|
for sep in separators:
|
||||||
sep_string += sep
|
sep_string += sep
|
||||||
if sep_string == "":
|
regex = r"\w+" if sep_string == "" else rf"[{sep_string}]|\w+"
|
||||||
regex = r"\w+"
|
|
||||||
else:
|
|
||||||
regex = r"[%s]|\w+" % sep_string
|
|
||||||
|
|
||||||
return re.findall(regex, string)
|
return re.findall(regex, string)
|
||||||
|
|
||||||
|
|
||||||
# --------- Public APIs -----------
|
# --------- Public APIs -----------
|
||||||
|
|
||||||
|
|
||||||
def get_dictionary(self, address):
|
def get_dictionary(self, address):
|
||||||
"""Load a z-machine-format dictionary at ADDRESS -- which maps
|
"""Load a z-machine-format dictionary at ADDRESS -- which maps
|
||||||
zstrings to bytestrings -- into a python dictionary which maps
|
zstrings to bytestrings -- into a python dictionary which maps
|
||||||
|
|
@ -88,17 +82,15 @@ class ZLexer:
|
||||||
|
|
||||||
dict = {}
|
dict = {}
|
||||||
|
|
||||||
num_entries, entry_length, separators, addr = \
|
num_entries, entry_length, separators, addr = self._parse_dict_header(address)
|
||||||
self._parse_dict_header(address)
|
|
||||||
|
|
||||||
for i in range(0, num_entries):
|
for _i in range(0, num_entries):
|
||||||
text_key = self._stringfactory.get(addr)
|
text_key = self._stringfactory.get(addr)
|
||||||
dict[text_key] = addr
|
dict[text_key] = addr
|
||||||
addr += entry_length
|
addr += entry_length
|
||||||
|
|
||||||
return dict
|
return dict
|
||||||
|
|
||||||
|
|
||||||
def parse_input(self, string, dict_addr=None):
|
def parse_input(self, string, dict_addr=None):
|
||||||
"""Given a unicode string, parse it into words based on a dictionary.
|
"""Given a unicode string, parse it into words based on a dictionary.
|
||||||
|
|
||||||
|
|
@ -119,8 +111,9 @@ class ZLexer:
|
||||||
zseparators = self._separators
|
zseparators = self._separators
|
||||||
dict = self._dict
|
dict = self._dict
|
||||||
else:
|
else:
|
||||||
num_entries, entry_length, zseparators, addr = \
|
num_entries, entry_length, zseparators, addr = self._parse_dict_header(
|
||||||
self._parse_dict_header(dict_addr)
|
dict_addr
|
||||||
|
)
|
||||||
dict = self.get_dictionary(dict_addr)
|
dict = self.get_dictionary(dict_addr)
|
||||||
|
|
||||||
# Our list of word separators are actually zscii codes that must
|
# Our list of word separators are actually zscii codes that must
|
||||||
|
|
@ -133,10 +126,7 @@ class ZLexer:
|
||||||
|
|
||||||
final_list = []
|
final_list = []
|
||||||
for word in token_list:
|
for word in token_list:
|
||||||
if word in dict:
|
byte_addr = dict.get(word, 0)
|
||||||
byte_addr = dict[word]
|
|
||||||
else:
|
|
||||||
byte_addr = 0
|
|
||||||
final_list.append([word, byte_addr])
|
final_list.append([word, byte_addr])
|
||||||
|
|
||||||
return final_list
|
return final_list
|
||||||
|
|
|
||||||
|
|
@ -12,22 +12,23 @@ logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# Create the logging objects regardless. If debugmode is False, then
|
# Create the logging objects regardless. If debugmode is False, then
|
||||||
# they won't actually do anything when used.
|
# they won't actually do anything when used.
|
||||||
mainlog = logging.FileHandler('debug.log', 'a')
|
mainlog_handler = logging.FileHandler("debug.log", "a")
|
||||||
mainlog.setLevel(logging.DEBUG)
|
mainlog_handler.setLevel(logging.DEBUG)
|
||||||
mainlog.setFormatter(logging.Formatter('%(asctime)s: %(message)s'))
|
mainlog_handler.setFormatter(logging.Formatter("%(asctime)s: %(message)s"))
|
||||||
logging.getLogger('mainlog').addHandler(mainlog)
|
logging.getLogger("mainlog").addHandler(mainlog_handler)
|
||||||
|
|
||||||
# We'll store the disassembly in a separate file, for better
|
# We'll store the disassembly in a separate file, for better
|
||||||
# readability.
|
# readability.
|
||||||
disasm = logging.FileHandler('disasm.log', 'a')
|
disasm_handler = logging.FileHandler("disasm.log", "a")
|
||||||
disasm.setLevel(logging.DEBUG)
|
disasm_handler.setLevel(logging.DEBUG)
|
||||||
disasm.setFormatter(logging.Formatter('%(message)s'))
|
disasm_handler.setFormatter(logging.Formatter("%(message)s"))
|
||||||
logging.getLogger('disasm').addHandler(disasm)
|
logging.getLogger("disasm").addHandler(disasm_handler)
|
||||||
|
|
||||||
|
mainlog = logging.getLogger("mainlog")
|
||||||
|
mainlog.info("*** Log reopened ***")
|
||||||
|
disasm = logging.getLogger("disasm")
|
||||||
|
disasm.info("*** Log reopened ***")
|
||||||
|
|
||||||
mainlog = logging.getLogger('mainlog')
|
|
||||||
mainlog.info('*** Log reopened ***')
|
|
||||||
disasm = logging.getLogger('disasm')
|
|
||||||
disasm.info('*** Log reopened ***')
|
|
||||||
|
|
||||||
# Pubilc routines used by other modules
|
# Pubilc routines used by other modules
|
||||||
def set_debug(state):
|
def set_debug(state):
|
||||||
|
|
@ -36,9 +37,10 @@ def set_debug(state):
|
||||||
else:
|
else:
|
||||||
logging.getLogger().setLevel(logging.CRITICAL)
|
logging.getLogger().setLevel(logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
def log(msg):
|
def log(msg):
|
||||||
mainlog.debug(msg)
|
mainlog.debug(msg)
|
||||||
|
|
||||||
|
|
||||||
def log_disasm(pc, opcode_type, opcode_num, opcode_name, args):
|
def log_disasm(pc, opcode_type, opcode_num, opcode_name, args):
|
||||||
disasm.debug("%06x %s:%02x %s %s" % (pc, opcode_type, opcode_num,
|
disasm.debug(f"{pc:06x} {opcode_type}:{opcode_num:02x} {opcode_name} {args}")
|
||||||
opcode_name, args))
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ from .zstring import ZStringFactory
|
||||||
class ZMachineError(Exception):
|
class ZMachineError(Exception):
|
||||||
"""General exception for ZMachine class"""
|
"""General exception for ZMachine class"""
|
||||||
|
|
||||||
|
|
||||||
class ZMachine:
|
class ZMachine:
|
||||||
"""The Z-Machine black box."""
|
"""The Z-Machine black box."""
|
||||||
|
|
||||||
|
|
@ -32,9 +33,15 @@ class ZMachine:
|
||||||
self._opdecoder.program_counter = self._mem.read_word(0x06)
|
self._opdecoder.program_counter = self._mem.read_word(0x06)
|
||||||
self._ui = ui
|
self._ui = ui
|
||||||
self._stream_manager = ZStreamManager(self._mem, self._ui)
|
self._stream_manager = ZStreamManager(self._mem, self._ui)
|
||||||
self._cpu = ZCpu(self._mem, self._opdecoder, self._stackmanager,
|
self._cpu = ZCpu(
|
||||||
self._objectparser, self._stringfactory,
|
self._mem,
|
||||||
self._stream_manager, self._ui)
|
self._opdecoder,
|
||||||
|
self._stackmanager,
|
||||||
|
self._objectparser,
|
||||||
|
self._stringfactory,
|
||||||
|
self._stream_manager,
|
||||||
|
self._ui,
|
||||||
|
)
|
||||||
|
|
||||||
# --------- Public APIs -----------
|
# --------- Public APIs -----------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,37 +18,48 @@ from .zlogging import log
|
||||||
|
|
||||||
class ZMemoryError(Exception):
|
class ZMemoryError(Exception):
|
||||||
"General exception for ZMemory class"
|
"General exception for ZMemory class"
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZMemoryIllegalWrite(ZMemoryError):
|
class ZMemoryIllegalWrite(ZMemoryError):
|
||||||
"Tried to write to a read-only part of memory"
|
"Tried to write to a read-only part of memory"
|
||||||
|
|
||||||
def __init__(self, address):
|
def __init__(self, address):
|
||||||
super().__init__(
|
super().__init__(f"Illegal write to address {address}")
|
||||||
"Illegal write to address %d" % address)
|
|
||||||
|
|
||||||
class ZMemoryBadInitialization(ZMemoryError):
|
class ZMemoryBadInitialization(ZMemoryError):
|
||||||
"Failure to initialize ZMemory class"
|
"Failure to initialize ZMemory class"
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZMemoryOutOfBounds(ZMemoryError):
|
class ZMemoryOutOfBounds(ZMemoryError):
|
||||||
"Accessed an address beyond the bounds of memory."
|
"Accessed an address beyond the bounds of memory."
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZMemoryBadMemoryLayout(ZMemoryError):
|
class ZMemoryBadMemoryLayout(ZMemoryError):
|
||||||
"Static plus dynamic memory exceeds 64k"
|
"Static plus dynamic memory exceeds 64k"
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZMemoryBadStoryfileSize(ZMemoryError):
|
class ZMemoryBadStoryfileSize(ZMemoryError):
|
||||||
"Story is too large for Z-machine version."
|
"Story is too large for Z-machine version."
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZMemoryUnsupportedVersion(ZMemoryError):
|
class ZMemoryUnsupportedVersion(ZMemoryError):
|
||||||
"Unsupported version of Z-story file."
|
"Unsupported version of Z-story file."
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZMemory:
|
class ZMemory:
|
||||||
|
|
||||||
# A list of 64 tuples describing who's allowed to tweak header-bytes.
|
# A list of 64 tuples describing who's allowed to tweak header-bytes.
|
||||||
# Index into the list is the header-byte being tweaked.
|
# Index into the list is the header-byte being tweaked.
|
||||||
# List value is a tuple of the form
|
# List value is a tuple of the form
|
||||||
|
|
@ -59,22 +70,72 @@ class ZMemory:
|
||||||
# enforcing authorization by *bit*, not by byte. Maybe do this
|
# enforcing authorization by *bit*, not by byte. Maybe do this
|
||||||
# someday.
|
# someday.
|
||||||
|
|
||||||
HEADER_PERMS = ([1,0,0], [3,0,1], None, None,
|
HEADER_PERMS = (
|
||||||
[1,0,0], None, [1,0,0], None,
|
[1, 0, 0],
|
||||||
[1,0,0], None, [1,0,0], None,
|
[3, 0, 1],
|
||||||
[1,0,0], None, [1,0,0], None,
|
None,
|
||||||
[1,1,1], [1,1,1], None, None,
|
None,
|
||||||
None, None, None, None,
|
[1, 0, 0],
|
||||||
[2,0,0], None, [3,0,0], None,
|
None,
|
||||||
[3,0,0], None, [4,1,1], [4,1,1],
|
[1, 0, 0],
|
||||||
[4,0,1], [4,0,1], [5,0,1], None,
|
None,
|
||||||
[5,0,1], None, [5,0,1], [5,0,1],
|
[1, 0, 0],
|
||||||
[6,0,0], None, [6,0,0], None,
|
None,
|
||||||
[5,0,1], [5,0,1], [5,0,0], None,
|
[1, 0, 0],
|
||||||
[6,0,1], None, [1,0,1], None,
|
None,
|
||||||
[5,0,0], None, [5,0,0], None,
|
[1, 0, 0],
|
||||||
None, None, None, None,
|
None,
|
||||||
None, None, None, None)
|
[1, 0, 0],
|
||||||
|
None,
|
||||||
|
[1, 1, 1],
|
||||||
|
[1, 1, 1],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
[2, 0, 0],
|
||||||
|
None,
|
||||||
|
[3, 0, 0],
|
||||||
|
None,
|
||||||
|
[3, 0, 0],
|
||||||
|
None,
|
||||||
|
[4, 1, 1],
|
||||||
|
[4, 1, 1],
|
||||||
|
[4, 0, 1],
|
||||||
|
[4, 0, 1],
|
||||||
|
[5, 0, 1],
|
||||||
|
None,
|
||||||
|
[5, 0, 1],
|
||||||
|
None,
|
||||||
|
[5, 0, 1],
|
||||||
|
[5, 0, 1],
|
||||||
|
[6, 0, 0],
|
||||||
|
None,
|
||||||
|
[6, 0, 0],
|
||||||
|
None,
|
||||||
|
[5, 0, 1],
|
||||||
|
[5, 0, 1],
|
||||||
|
[5, 0, 0],
|
||||||
|
None,
|
||||||
|
[6, 0, 1],
|
||||||
|
None,
|
||||||
|
[1, 0, 1],
|
||||||
|
None,
|
||||||
|
[5, 0, 0],
|
||||||
|
None,
|
||||||
|
[5, 0, 0],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, initial_string):
|
def __init__(self, initial_string):
|
||||||
"""Construct class based on a string that represents an initial
|
"""Construct class based on a string that represents an initial
|
||||||
|
|
@ -87,17 +148,18 @@ class ZMemory:
|
||||||
self._memory = bytearray(initial_string)
|
self._memory = bytearray(initial_string)
|
||||||
|
|
||||||
# Figure out the different sections of memory
|
# Figure out the different sections of memory
|
||||||
self._static_start = self.read_word(0x0e)
|
self._static_start = self.read_word(0x0E)
|
||||||
self._static_end = min(0x0ffff, self._total_size)
|
self._static_end = min(0x0FFFF, self._total_size)
|
||||||
self._dynamic_start = 0
|
self._dynamic_start = 0
|
||||||
self._dynamic_end = self._static_start - 1
|
self._dynamic_end = self._static_start - 1
|
||||||
self._high_start = self.read_word(0x04)
|
self._high_start = self.read_word(0x04)
|
||||||
self._high_end = self._total_size
|
self._high_end = self._total_size
|
||||||
self._global_variable_start = self.read_word(0x0c)
|
self._global_variable_start = self.read_word(0x0C)
|
||||||
|
|
||||||
# Dynamic + static must not exceed 64k
|
# Dynamic + static must not exceed 64k
|
||||||
dynamic_plus_static = ((self._dynamic_end - self._dynamic_start)
|
dynamic_plus_static = (self._dynamic_end - self._dynamic_start) + (
|
||||||
+ (self._static_end - self._static_start))
|
self._static_end - self._static_start
|
||||||
|
)
|
||||||
if dynamic_plus_static > 65534:
|
if dynamic_plus_static > 65534:
|
||||||
raise ZMemoryBadMemoryLayout
|
raise ZMemoryBadMemoryLayout
|
||||||
|
|
||||||
|
|
@ -115,10 +177,10 @@ class ZMemory:
|
||||||
raise ZMemoryUnsupportedVersion
|
raise ZMemoryUnsupportedVersion
|
||||||
|
|
||||||
log("Memory system initialized, map follows")
|
log("Memory system initialized, map follows")
|
||||||
log(" Dynamic memory: %x - %x" % (self._dynamic_start, self._dynamic_end))
|
log(f" Dynamic memory: {self._dynamic_start:x} - {self._dynamic_end:x}")
|
||||||
log(" Static memory: %x - %x" % (self._static_start, self._static_end))
|
log(f" Static memory: {self._static_start:x} - {self._static_end:x}")
|
||||||
log(" High memory: %x - %x" % (self._high_start, self._high_end))
|
log(f" High memory: {self._high_start:x} - {self._high_end:x}")
|
||||||
log(" Global variable start: %x" % self._global_variable_start)
|
log(f" Global variable start: {self._global_variable_start:x}")
|
||||||
|
|
||||||
def _check_bounds(self, index):
|
def _check_bounds(self, index):
|
||||||
if isinstance(index, slice):
|
if isinstance(index, slice):
|
||||||
|
|
@ -258,7 +320,7 @@ class ZMemory:
|
||||||
raise ZMemoryOutOfBounds
|
raise ZMemoryOutOfBounds
|
||||||
if not (0x00 <= value <= 0xFFFF):
|
if not (0x00 <= value <= 0xFFFF):
|
||||||
raise ZMemoryIllegalWrite(value)
|
raise ZMemoryIllegalWrite(value)
|
||||||
log("Write %d to global variable %d" % (value, varnum))
|
log(f"Write {value} to global variable {varnum}")
|
||||||
actual_address = self._global_variable_start + ((varnum - 0x10) * 2)
|
actual_address = self._global_variable_start + ((varnum - 0x10) * 2)
|
||||||
bf = bitfield.BitField(value)
|
bf = bitfield.BitField(value)
|
||||||
self._memory[actual_address] = bf[8:15]
|
self._memory[actual_address] = bf[8:15]
|
||||||
|
|
@ -275,4 +337,4 @@ class ZMemory:
|
||||||
while count < self._total_size:
|
while count < self._total_size:
|
||||||
total += self._memory[count]
|
total += self._memory[count]
|
||||||
count += 1
|
count += 1
|
||||||
return (total % 0x10000)
|
return total % 0x10000
|
||||||
|
|
|
||||||
|
|
@ -20,45 +20,59 @@ from .zstring import ZStringFactory
|
||||||
|
|
||||||
class ZObjectError(Exception):
|
class ZObjectError(Exception):
|
||||||
"General exception for ZObject class"
|
"General exception for ZObject class"
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZObjectIllegalObjectNumber(ZObjectError):
|
class ZObjectIllegalObjectNumber(ZObjectError):
|
||||||
"Illegal object number given."
|
"Illegal object number given."
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZObjectIllegalAttributeNumber(ZObjectError):
|
class ZObjectIllegalAttributeNumber(ZObjectError):
|
||||||
"Illegal attribute number given."
|
"Illegal attribute number given."
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZObjectIllegalPropertyNumber(ZObjectError):
|
class ZObjectIllegalPropertyNumber(ZObjectError):
|
||||||
"Illegal property number given."
|
"Illegal property number given."
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZObjectIllegalPropertySet(ZObjectError):
|
class ZObjectIllegalPropertySet(ZObjectError):
|
||||||
"Illegal set of a property whose size is not 1 or 2."
|
"Illegal set of a property whose size is not 1 or 2."
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZObjectIllegalVersion(ZObjectError):
|
class ZObjectIllegalVersion(ZObjectError):
|
||||||
"Unsupported z-machine version."
|
"Unsupported z-machine version."
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZObjectIllegalPropLength(ZObjectError):
|
class ZObjectIllegalPropLength(ZObjectError):
|
||||||
"Illegal property length."
|
"Illegal property length."
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZObjectMalformedTree(ZObjectError):
|
class ZObjectMalformedTree(ZObjectError):
|
||||||
"Object tree is malformed."
|
"Object tree is malformed."
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# The interpreter should only need exactly one instance of this class.
|
# The interpreter should only need exactly one instance of this class.
|
||||||
|
|
||||||
|
|
||||||
class ZObjectParser:
|
class ZObjectParser:
|
||||||
|
|
||||||
def __init__(self, zmem):
|
def __init__(self, zmem):
|
||||||
|
|
||||||
self._memory = zmem
|
self._memory = zmem
|
||||||
self._propdefaults_addr = zmem.read_word(0x0a)
|
self._propdefaults_addr = zmem.read_word(0x0A)
|
||||||
self._stringfactory = ZStringFactory(self._memory)
|
self._stringfactory = ZStringFactory(self._memory)
|
||||||
|
|
||||||
if 1 <= self._memory.version <= 3:
|
if 1 <= self._memory.version <= 3:
|
||||||
|
|
@ -68,7 +82,6 @@ class ZObjectParser:
|
||||||
else:
|
else:
|
||||||
raise ZObjectIllegalVersion
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
|
|
||||||
def _get_object_addr(self, objectnum):
|
def _get_object_addr(self, objectnum):
|
||||||
"""Return address of object number OBJECTNUM."""
|
"""Return address of object number OBJECTNUM."""
|
||||||
|
|
||||||
|
|
@ -79,16 +92,15 @@ class ZObjectParser:
|
||||||
result = self._objecttree_addr + (9 * (objectnum - 1))
|
result = self._objecttree_addr + (9 * (objectnum - 1))
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5:
|
||||||
if not (1 <= objectnum <= 65535):
|
if not (1 <= objectnum <= 65535):
|
||||||
log("error: there is no object %d" % objectnum)
|
log(f"error: there is no object {objectnum}")
|
||||||
raise ZObjectIllegalObjectNumber
|
raise ZObjectIllegalObjectNumber
|
||||||
result = self._objecttree_addr + (14 * (objectnum - 1))
|
result = self._objecttree_addr + (14 * (objectnum - 1))
|
||||||
else:
|
else:
|
||||||
raise ZObjectIllegalVersion
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
log("address of object %d is %d" % (objectnum, result))
|
log(f"address of object {objectnum} is {result}")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _get_parent_sibling_child(self, objectnum):
|
def _get_parent_sibling_child(self, objectnum):
|
||||||
"""Return [parent, sibling, child] object numbers of object OBJECTNUM."""
|
"""Return [parent, sibling, child] object numbers of object OBJECTNUM."""
|
||||||
|
|
||||||
|
|
@ -101,17 +113,20 @@ class ZObjectParser:
|
||||||
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5:
|
||||||
addr += 6 # skip past attributes
|
addr += 6 # skip past attributes
|
||||||
result = [self._memory.read_word(addr),
|
result = [
|
||||||
|
self._memory.read_word(addr),
|
||||||
self._memory.read_word(addr + 2),
|
self._memory.read_word(addr + 2),
|
||||||
self._memory.read_word(addr + 4)]
|
self._memory.read_word(addr + 4),
|
||||||
|
]
|
||||||
else:
|
else:
|
||||||
raise ZObjectIllegalVersion
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
log ("parent/sibling/child of object %d is %d, %d, %d" %
|
log(
|
||||||
(objectnum, result[0], result[1], result[2]))
|
f"parent/sibling/child of object {objectnum} is "
|
||||||
|
f"{result[0]}, {result[1]}, {result[2]}"
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _get_proptable_addr(self, objectnum):
|
def _get_proptable_addr(self, objectnum):
|
||||||
"""Return address of property table of object OBJECTNUM."""
|
"""Return address of property table of object OBJECTNUM."""
|
||||||
|
|
||||||
|
|
@ -141,8 +156,7 @@ class ZObjectParser:
|
||||||
else:
|
else:
|
||||||
raise ZObjectIllegalVersion
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
return (addr + (2 * (propnum - 1)))
|
return addr + (2 * (propnum - 1))
|
||||||
|
|
||||||
|
|
||||||
# --------- Public APIs -----------
|
# --------- Public APIs -----------
|
||||||
|
|
||||||
|
|
@ -167,7 +181,6 @@ class ZObjectParser:
|
||||||
|
|
||||||
return bf[7 - (attrnum % 8)]
|
return bf[7 - (attrnum % 8)]
|
||||||
|
|
||||||
|
|
||||||
def get_all_attributes(self, objectnum):
|
def get_all_attributes(self, objectnum):
|
||||||
"""Return a list of all attribute numbers that are set on object
|
"""Return a list of all attribute numbers that are set on object
|
||||||
OBJECTNUM"""
|
OBJECTNUM"""
|
||||||
|
|
@ -186,28 +199,24 @@ class ZObjectParser:
|
||||||
attrs.append(i)
|
attrs.append(i)
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
def get_parent(self, objectnum):
|
def get_parent(self, objectnum):
|
||||||
"""Return object number of parent of object number OBJECTNUM."""
|
"""Return object number of parent of object number OBJECTNUM."""
|
||||||
|
|
||||||
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
|
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
|
||||||
return parent
|
return parent
|
||||||
|
|
||||||
|
|
||||||
def get_child(self, objectnum):
|
def get_child(self, objectnum):
|
||||||
"""Return object number of child of object number OBJECTNUM."""
|
"""Return object number of child of object number OBJECTNUM."""
|
||||||
|
|
||||||
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
|
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
|
||||||
return child
|
return child
|
||||||
|
|
||||||
|
|
||||||
def get_sibling(self, objectnum):
|
def get_sibling(self, objectnum):
|
||||||
"""Return object number of sibling of object number OBJECTNUM."""
|
"""Return object number of sibling of object number OBJECTNUM."""
|
||||||
|
|
||||||
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
|
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
|
||||||
return sibling
|
return sibling
|
||||||
|
|
||||||
|
|
||||||
def set_parent(self, objectnum, new_parent_num):
|
def set_parent(self, objectnum, new_parent_num):
|
||||||
"""Make OBJECTNUM's parent pointer point to NEW_PARENT_NUM."""
|
"""Make OBJECTNUM's parent pointer point to NEW_PARENT_NUM."""
|
||||||
|
|
||||||
|
|
@ -219,7 +228,6 @@ class ZObjectParser:
|
||||||
else:
|
else:
|
||||||
raise ZObjectIllegalVersion
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
|
|
||||||
def set_child(self, objectnum, new_child_num):
|
def set_child(self, objectnum, new_child_num):
|
||||||
"""Make OBJECTNUM's child pointer point to NEW_PARENT_NUM."""
|
"""Make OBJECTNUM's child pointer point to NEW_PARENT_NUM."""
|
||||||
|
|
||||||
|
|
@ -231,7 +239,6 @@ class ZObjectParser:
|
||||||
else:
|
else:
|
||||||
raise ZObjectIllegalVersion
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
|
|
||||||
def set_sibling(self, objectnum, new_sibling_num):
|
def set_sibling(self, objectnum, new_sibling_num):
|
||||||
"""Make OBJECTNUM's sibling pointer point to NEW_PARENT_NUM."""
|
"""Make OBJECTNUM's sibling pointer point to NEW_PARENT_NUM."""
|
||||||
|
|
||||||
|
|
@ -243,7 +250,6 @@ class ZObjectParser:
|
||||||
else:
|
else:
|
||||||
raise ZObjectIllegalVersion
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
|
|
||||||
def insert_object(self, parent_object, new_child):
|
def insert_object(self, parent_object, new_child):
|
||||||
"""Prepend object NEW_CHILD to the list of PARENT_OBJECT's children."""
|
"""Prepend object NEW_CHILD to the list of PARENT_OBJECT's children."""
|
||||||
|
|
||||||
|
|
@ -277,14 +283,12 @@ class ZObjectParser:
|
||||||
# we reached the end of the list, never got a match
|
# we reached the end of the list, never got a match
|
||||||
raise ZObjectMalformedTree
|
raise ZObjectMalformedTree
|
||||||
|
|
||||||
|
|
||||||
def get_shortname(self, objectnum):
|
def get_shortname(self, objectnum):
|
||||||
"""Return 'short name' of object number OBJECTNUM as ascii string."""
|
"""Return 'short name' of object number OBJECTNUM as ascii string."""
|
||||||
|
|
||||||
addr = self._get_proptable_addr(objectnum)
|
addr = self._get_proptable_addr(objectnum)
|
||||||
return self._stringfactory.get(addr + 1)
|
return self._stringfactory.get(addr + 1)
|
||||||
|
|
||||||
|
|
||||||
def get_prop(self, objectnum, propnum):
|
def get_prop(self, objectnum, propnum):
|
||||||
"""Return either a byte or word value of property PROPNUM of
|
"""Return either a byte or word value of property PROPNUM of
|
||||||
object OBJECTNUM."""
|
object OBJECTNUM."""
|
||||||
|
|
@ -296,7 +300,6 @@ class ZObjectParser:
|
||||||
else:
|
else:
|
||||||
raise ZObjectIllegalPropLength
|
raise ZObjectIllegalPropLength
|
||||||
|
|
||||||
|
|
||||||
def get_prop_addr_len(self, objectnum, propnum):
|
def get_prop_addr_len(self, objectnum, propnum):
|
||||||
"""Return address & length of value for property number PROPNUM of
|
"""Return address & length of value for property number PROPNUM of
|
||||||
object number OBJECTNUM. If object has no such property, then
|
object number OBJECTNUM. If object has no such property, then
|
||||||
|
|
@ -305,11 +308,10 @@ class ZObjectParser:
|
||||||
# start at the beginning of the object's proptable
|
# start at the beginning of the object's proptable
|
||||||
addr = self._get_proptable_addr(objectnum)
|
addr = self._get_proptable_addr(objectnum)
|
||||||
# skip past the shortname of the object
|
# skip past the shortname of the object
|
||||||
addr += (2 * self._memory[addr])
|
addr += 2 * self._memory[addr]
|
||||||
pnum = 0
|
pnum = 0
|
||||||
|
|
||||||
if 1 <= self._memory.version <= 3:
|
if 1 <= self._memory.version <= 3:
|
||||||
|
|
||||||
while self._memory[addr] != 0:
|
while self._memory[addr] != 0:
|
||||||
bf = BitField(self._memory[addr])
|
bf = BitField(self._memory[addr])
|
||||||
addr += 1
|
addr += 1
|
||||||
|
|
@ -320,7 +322,6 @@ class ZObjectParser:
|
||||||
addr += size
|
addr += size
|
||||||
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
elif 4 <= self._memory.version <= 5:
|
||||||
|
|
||||||
while self._memory[addr] != 0:
|
while self._memory[addr] != 0:
|
||||||
bf = BitField(self._memory[addr])
|
bf = BitField(self._memory[addr])
|
||||||
addr += 1
|
addr += 1
|
||||||
|
|
@ -330,10 +331,7 @@ class ZObjectParser:
|
||||||
addr += 1
|
addr += 1
|
||||||
size = bf2[5:0]
|
size = bf2[5:0]
|
||||||
else:
|
else:
|
||||||
if bf[6]:
|
size = 2 if bf[6] else 1
|
||||||
size = 2
|
|
||||||
else:
|
|
||||||
size = 1
|
|
||||||
if pnum == propnum:
|
if pnum == propnum:
|
||||||
return (addr, size)
|
return (addr, size)
|
||||||
addr += size
|
addr += size
|
||||||
|
|
@ -345,7 +343,6 @@ class ZObjectParser:
|
||||||
default_value_addr = self._get_default_property_addr(propnum)
|
default_value_addr = self._get_default_property_addr(propnum)
|
||||||
return (default_value_addr, 2)
|
return (default_value_addr, 2)
|
||||||
|
|
||||||
|
|
||||||
def get_all_properties(self, objectnum):
|
def get_all_properties(self, objectnum):
|
||||||
"""Return a dictionary of all properties listed in the property
|
"""Return a dictionary of all properties listed in the property
|
||||||
table of object OBJECTNUM. (Obviously, this discounts 'default'
|
table of object OBJECTNUM. (Obviously, this discounts 'default'
|
||||||
|
|
@ -359,7 +356,7 @@ class ZObjectParser:
|
||||||
# skip past the shortname of the object
|
# skip past the shortname of the object
|
||||||
shortname_length = self._memory[addr]
|
shortname_length = self._memory[addr]
|
||||||
addr += 1
|
addr += 1
|
||||||
addr += (2*shortname_length)
|
addr += 2 * shortname_length
|
||||||
|
|
||||||
if 1 <= self._memory.version <= 3:
|
if 1 <= self._memory.version <= 3:
|
||||||
while self._memory[addr] != 0:
|
while self._memory[addr] != 0:
|
||||||
|
|
@ -382,10 +379,7 @@ class ZObjectParser:
|
||||||
if size == 0:
|
if size == 0:
|
||||||
size = 64
|
size = 64
|
||||||
else:
|
else:
|
||||||
if bf[6]:
|
size = 2 if bf[6] else 1
|
||||||
size = 2
|
|
||||||
else:
|
|
||||||
size = 1
|
|
||||||
proplist[pnum] = (addr, size)
|
proplist[pnum] = (addr, size)
|
||||||
addr += size
|
addr += size
|
||||||
|
|
||||||
|
|
@ -394,7 +388,6 @@ class ZObjectParser:
|
||||||
|
|
||||||
return proplist
|
return proplist
|
||||||
|
|
||||||
|
|
||||||
def set_property(self, objectnum, propnum, value):
|
def set_property(self, objectnum, propnum, value):
|
||||||
"""Set a property on an object."""
|
"""Set a property on an object."""
|
||||||
proplist = self.get_all_properties(objectnum)
|
proplist = self.get_all_properties(objectnum)
|
||||||
|
|
@ -403,21 +396,20 @@ class ZObjectParser:
|
||||||
|
|
||||||
addr, size = proplist[propnum]
|
addr, size = proplist[propnum]
|
||||||
if size == 1:
|
if size == 1:
|
||||||
self._memory[addr] = (value & 0xFF)
|
self._memory[addr] = value & 0xFF
|
||||||
elif size == 2:
|
elif size == 2:
|
||||||
self._memory.write_word(addr, value)
|
self._memory.write_word(addr, value)
|
||||||
else:
|
else:
|
||||||
raise ZObjectIllegalPropertySet
|
raise ZObjectIllegalPropertySet
|
||||||
|
|
||||||
|
|
||||||
def describe_object(self, objectnum):
|
def describe_object(self, objectnum):
|
||||||
"""For debugging purposes, pretty-print everything known about
|
"""For debugging purposes, pretty-print everything known about
|
||||||
object OBJECTNUM."""
|
object OBJECTNUM."""
|
||||||
|
|
||||||
print("Object number:", objectnum)
|
print("Object number:", objectnum)
|
||||||
print(" Short name:", self.get_shortname(objectnum))
|
print(" Short name:", self.get_shortname(objectnum))
|
||||||
print(" Parent:", self.get_parent(objectnum), end=' ')
|
print(" Parent:", self.get_parent(objectnum), end=" ")
|
||||||
print(" Sibling:", self.get_sibling(objectnum), end=' ')
|
print(" Sibling:", self.get_sibling(objectnum), end=" ")
|
||||||
print(" Child:", self.get_child(objectnum))
|
print(" Child:", self.get_child(objectnum))
|
||||||
print(" Attributes:", self.get_all_attributes(objectnum))
|
print(" Attributes:", self.get_all_attributes(objectnum))
|
||||||
print(" Properties:")
|
print(" Properties:")
|
||||||
|
|
@ -425,8 +417,7 @@ class ZObjectParser:
|
||||||
proplist = self.get_all_properties(objectnum)
|
proplist = self.get_all_properties(objectnum)
|
||||||
for key in list(proplist.keys()):
|
for key in list(proplist.keys()):
|
||||||
(addr, len) = proplist[key]
|
(addr, len) = proplist[key]
|
||||||
print(" [%2d] :" % key, end=' ')
|
print(f" [{key:2d}] :", end=" ")
|
||||||
for i in range(0, len):
|
for i in range(0, len):
|
||||||
print("%02X" % self._memory[addr+i], end=' ')
|
print(f"{self._memory[addr + i]:02X}", end=" ")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ from .zlogging import log
|
||||||
|
|
||||||
class ZOperationError(Exception):
|
class ZOperationError(Exception):
|
||||||
"General exception for ZOperation class"
|
"General exception for ZOperation class"
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Constants defining the known instruction types. These types are
|
# Constants defining the known instruction types. These types are
|
||||||
# related to the number of operands the opcode has: for each operand
|
# related to the number of operands the opcode has: for each operand
|
||||||
# count, there is a separate opcode table, and the actual opcode
|
# count, there is a separate opcode table, and the actual opcode
|
||||||
|
|
@ -27,11 +29,11 @@ OPCODE_EXT = 4
|
||||||
# Mapping of those constants to strings describing the opcode
|
# Mapping of those constants to strings describing the opcode
|
||||||
# classes. Used for pretty-printing only.
|
# classes. Used for pretty-printing only.
|
||||||
OPCODE_STRINGS = {
|
OPCODE_STRINGS = {
|
||||||
OPCODE_0OP: '0OP',
|
OPCODE_0OP: "0OP",
|
||||||
OPCODE_1OP: '1OP',
|
OPCODE_1OP: "1OP",
|
||||||
OPCODE_2OP: '2OP',
|
OPCODE_2OP: "2OP",
|
||||||
OPCODE_VAR: 'VAR',
|
OPCODE_VAR: "VAR",
|
||||||
OPCODE_EXT: 'EXT',
|
OPCODE_EXT: "EXT",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Constants defining the possible operand types.
|
# Constants defining the possible operand types.
|
||||||
|
|
@ -40,6 +42,7 @@ SMALL_CONSTANT = 0x1
|
||||||
VARIABLE = 0x2
|
VARIABLE = 0x2
|
||||||
ABSENT = 0x3
|
ABSENT = 0x3
|
||||||
|
|
||||||
|
|
||||||
class ZOpDecoder:
|
class ZOpDecoder:
|
||||||
def __init__(self, zmem, zstack):
|
def __init__(self, zmem, zstack):
|
||||||
""
|
""
|
||||||
|
|
@ -64,7 +67,7 @@ class ZOpDecoder:
|
||||||
|
|
||||||
opcode = self._get_pc()
|
opcode = self._get_pc()
|
||||||
|
|
||||||
log("Decode opcode %x" % opcode)
|
log(f"Decode opcode {opcode:x}")
|
||||||
|
|
||||||
# Determine the opcode type, and hand off further parsing.
|
# Determine the opcode type, and hand off further parsing.
|
||||||
if self._memory.version == 5 and opcode == 0xBE:
|
if self._memory.version == 5 and opcode == 0xBE:
|
||||||
|
|
@ -88,8 +91,10 @@ class ZOpDecoder:
|
||||||
# encoded in bits 5 and 6 of the opcode.
|
# encoded in bits 5 and 6 of the opcode.
|
||||||
log("Opcode is long")
|
log("Opcode is long")
|
||||||
LONG_OPERAND_TYPES = [SMALL_CONSTANT, VARIABLE]
|
LONG_OPERAND_TYPES = [SMALL_CONSTANT, VARIABLE]
|
||||||
operands = [self._parse_operand(LONG_OPERAND_TYPES[opcode[6]]),
|
operands = [
|
||||||
self._parse_operand(LONG_OPERAND_TYPES[opcode[5]])]
|
self._parse_operand(LONG_OPERAND_TYPES[opcode[6]]),
|
||||||
|
self._parse_operand(LONG_OPERAND_TYPES[opcode[5]]),
|
||||||
|
]
|
||||||
return (OPCODE_2OP, opcode[0:5], operands)
|
return (OPCODE_2OP, opcode[0:5], operands)
|
||||||
|
|
||||||
def _parse_opcode_short(self, opcode):
|
def _parse_opcode_short(self, opcode):
|
||||||
|
|
@ -127,6 +132,10 @@ class ZOpDecoder:
|
||||||
|
|
||||||
return (opcode_type, opcode_num, operands)
|
return (opcode_type, opcode_num, operands)
|
||||||
|
|
||||||
|
def _parse_opcode_extended(self):
|
||||||
|
"""Parse an extended opcode (v5+ feature)."""
|
||||||
|
raise NotImplementedError("Extended opcodes (v5+) not yet implemented")
|
||||||
|
|
||||||
def _parse_operand(self, operand_type):
|
def _parse_operand(self, operand_type):
|
||||||
"""Read and return an operand of the given type.
|
"""Read and return an operand of the given type.
|
||||||
|
|
||||||
|
|
@ -143,7 +152,7 @@ class ZOpDecoder:
|
||||||
operand = self._get_pc()
|
operand = self._get_pc()
|
||||||
elif operand_type == VARIABLE:
|
elif operand_type == VARIABLE:
|
||||||
variable_number = self._get_pc()
|
variable_number = self._get_pc()
|
||||||
log("Operand is variable %d" % variable_number)
|
log(f"Operand is variable {variable_number}")
|
||||||
if variable_number == 0:
|
if variable_number == 0:
|
||||||
log("Operand value comes from stack")
|
log("Operand value comes from stack")
|
||||||
operand = self._stack.pop_stack() # TODO: make sure this is right.
|
operand = self._stack.pop_stack() # TODO: make sure this is right.
|
||||||
|
|
@ -157,7 +166,7 @@ class ZOpDecoder:
|
||||||
log("Operand is absent")
|
log("Operand is absent")
|
||||||
operand = None
|
operand = None
|
||||||
if operand is not None:
|
if operand is not None:
|
||||||
log("Operand value: %d" % operand)
|
log(f"Operand value: {operand}")
|
||||||
|
|
||||||
return operand
|
return operand
|
||||||
|
|
||||||
|
|
@ -167,8 +176,12 @@ class ZOpDecoder:
|
||||||
"""
|
"""
|
||||||
operand_byte = BitField(self._get_pc())
|
operand_byte = BitField(self._get_pc())
|
||||||
operands = []
|
operands = []
|
||||||
for operand_type in [operand_byte[6:8], operand_byte[4:6],
|
for operand_type in [
|
||||||
operand_byte[2:4], operand_byte[0:2]]:
|
operand_byte[6:8],
|
||||||
|
operand_byte[4:6],
|
||||||
|
operand_byte[2:4],
|
||||||
|
operand_byte[0:2],
|
||||||
|
]:
|
||||||
operand = self._parse_operand(operand_type)
|
operand = self._parse_operand(operand_type)
|
||||||
if operand is None:
|
if operand is None:
|
||||||
break
|
break
|
||||||
|
|
@ -176,7 +189,6 @@ class ZOpDecoder:
|
||||||
|
|
||||||
return operands
|
return operands
|
||||||
|
|
||||||
|
|
||||||
# Public funcs that the ZPU may also need to call, depending on the
|
# Public funcs that the ZPU may also need to call, depending on the
|
||||||
# opcode being executed:
|
# opcode being executed:
|
||||||
|
|
||||||
|
|
@ -195,14 +207,12 @@ class ZOpDecoder:
|
||||||
|
|
||||||
return start_addr
|
return start_addr
|
||||||
|
|
||||||
|
|
||||||
def get_store_address(self):
|
def get_store_address(self):
|
||||||
"""For store opcodes, read byte pointed to by PC and return the
|
"""For store opcodes, read byte pointed to by PC and return the
|
||||||
variable number in which the operation result should be stored.
|
variable number in which the operation result should be stored.
|
||||||
Increment the PC as necessary."""
|
Increment the PC as necessary."""
|
||||||
return self._get_pc()
|
return self._get_pc()
|
||||||
|
|
||||||
|
|
||||||
def get_branch_offset(self):
|
def get_branch_offset(self):
|
||||||
"""For branching opcodes, examine address pointed to by PC, and
|
"""For branching opcodes, examine address pointed to by PC, and
|
||||||
return two values: first, either True or False (indicating whether
|
return two values: first, either True or False (indicating whether
|
||||||
|
|
@ -230,5 +240,5 @@ class ZOpDecoder:
|
||||||
if bf[5]:
|
if bf[5]:
|
||||||
branch_offset -= 8192
|
branch_offset -= 8192
|
||||||
|
|
||||||
log('Branch if %s to offset %+d' % (branch_if_true, branch_offset))
|
log(f"Branch if {branch_if_true} to offset {branch_offset:+d}")
|
||||||
return branch_if_true, branch_offset
|
return branch_if_true, branch_offset
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
|
||||||
|
|
||||||
return [self._rows, self._columns]
|
return [self._rows, self._columns]
|
||||||
|
|
||||||
|
|
||||||
def select_window(self, window):
|
def select_window(self, window):
|
||||||
"""Select a window to be the 'active' window, and move that
|
"""Select a window to be the 'active' window, and move that
|
||||||
window's cursor to the upper left.
|
window's cursor to the upper left.
|
||||||
|
|
@ -143,7 +142,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def split_window(self, height):
|
def split_window(self, height):
|
||||||
"""Make the upper window appear and be HEIGHT lines tall. To
|
"""Make the upper window appear and be HEIGHT lines tall. To
|
||||||
'unsplit' a window, call with a height of 0 lines.
|
'unsplit' a window, call with a height of 0 lines.
|
||||||
|
|
@ -153,7 +151,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def set_cursor_position(self, x, y):
|
def set_cursor_position(self, x, y):
|
||||||
"""Set the cursor to (row, column) coordinates (X,Y) in the
|
"""Set the cursor to (row, column) coordinates (X,Y) in the
|
||||||
current window, where (1,1) is the upper-left corner.
|
current window, where (1,1) is the upper-left corner.
|
||||||
|
|
@ -168,9 +165,7 @@ class ZScreen(zstream.ZBufferableOutputStream):
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def erase_window(self, window=WINDOW_LOWER, color=COLOR_CURRENT):
|
||||||
def erase_window(self, window=WINDOW_LOWER,
|
|
||||||
color=COLOR_CURRENT):
|
|
||||||
"""Erase WINDOW to background COLOR.
|
"""Erase WINDOW to background COLOR.
|
||||||
|
|
||||||
WINDOW should be one of WINDOW_UPPER or WINDOW_LOWER.
|
WINDOW should be one of WINDOW_UPPER or WINDOW_LOWER.
|
||||||
|
|
@ -185,7 +180,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def erase_line(self):
|
def erase_line(self):
|
||||||
"""Erase from the current cursor position to the end of its line
|
"""Erase from the current cursor position to the end of its line
|
||||||
in the current window.
|
in the current window.
|
||||||
|
|
@ -196,7 +190,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
# Status Line
|
# Status Line
|
||||||
#
|
#
|
||||||
# These routines are only called if the has_status_line capability
|
# These routines are only called if the has_status_line capability
|
||||||
|
|
@ -216,7 +209,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def print_status_time(self, hours, minutes):
|
def print_status_time(self, hours, minutes):
|
||||||
"""Print a status line in the upper window, as follows:
|
"""Print a status line in the upper window, as follows:
|
||||||
|
|
||||||
|
|
@ -229,7 +221,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
# Text Appearances
|
# Text Appearances
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
@ -238,7 +229,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
|
||||||
|
|
||||||
return [self._fontwidth, self._fontheight]
|
return [self._fontwidth, self._fontheight]
|
||||||
|
|
||||||
|
|
||||||
def set_font(self, font_number):
|
def set_font(self, font_number):
|
||||||
"""Set the current window's font to one of
|
"""Set the current window's font to one of
|
||||||
|
|
||||||
|
|
@ -256,7 +246,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def set_text_style(self, style):
|
def set_text_style(self, style):
|
||||||
"""Set the current text style to the given text style.
|
"""Set the current text style to the given text style.
|
||||||
|
|
||||||
|
|
@ -281,7 +270,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
def set_text_color(self, foreground_color, background_color):
|
def set_text_color(self, foreground_color, background_color):
|
||||||
"""Set current text foreground and background color. Each color
|
"""Set current text foreground and background color. Each color
|
||||||
should correspond to one of the COLOR_* constants.
|
should correspond to one of the COLOR_* constants.
|
||||||
|
|
@ -292,7 +280,6 @@ class ZScreen(zstream.ZBufferableOutputStream):
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
# Standard output
|
# Standard output
|
||||||
|
|
||||||
def write(self, string):
|
def write(self, string):
|
||||||
|
|
|
||||||
|
|
@ -13,30 +13,40 @@ from .zlogging import log
|
||||||
|
|
||||||
class ZStackError(Exception):
|
class ZStackError(Exception):
|
||||||
"General exception for stack or routine-related errors"
|
"General exception for stack or routine-related errors"
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZStackUnsupportedVersion(ZStackError):
|
class ZStackUnsupportedVersion(ZStackError):
|
||||||
"Unsupported version of Z-story file."
|
"Unsupported version of Z-story file."
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZStackNoRoutine(ZStackError):
|
class ZStackNoRoutine(ZStackError):
|
||||||
"No routine is being executed."
|
"No routine is being executed."
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZStackNoSuchVariable(ZStackError):
|
class ZStackNoSuchVariable(ZStackError):
|
||||||
"Trying to access non-existent local variable."
|
"Trying to access non-existent local variable."
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZStackPopError(ZStackError):
|
class ZStackPopError(ZStackError):
|
||||||
"Nothing to pop from stack!"
|
"Nothing to pop from stack!"
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Helper class used by ZStackManager; a 'routine' object which
|
# Helper class used by ZStackManager; a 'routine' object which
|
||||||
# includes its own private stack of data.
|
# includes its own private stack of data.
|
||||||
class ZRoutine:
|
class ZRoutine:
|
||||||
|
def __init__(
|
||||||
def __init__(self, start_addr, return_addr, zmem, args,
|
self, start_addr, return_addr, zmem, args, local_vars=None, stack=None
|
||||||
local_vars=None, stack=None):
|
):
|
||||||
"""Initialize a routine object beginning at START_ADDR in ZMEM,
|
"""Initialize a routine object beginning at START_ADDR in ZMEM,
|
||||||
with initial argument values in list ARGS. If LOCAL_VARS is None,
|
with initial argument values in list ARGS. If LOCAL_VARS is None,
|
||||||
then parse them from START_ADDR."""
|
then parse them from START_ADDR."""
|
||||||
|
|
@ -55,7 +65,7 @@ class ZRoutine:
|
||||||
else:
|
else:
|
||||||
num_local_vars = zmem[self.start_addr]
|
num_local_vars = zmem[self.start_addr]
|
||||||
if not (0 <= num_local_vars <= 15):
|
if not (0 <= num_local_vars <= 15):
|
||||||
log("num local vars is %d" % num_local_vars)
|
log(f"num local vars is {num_local_vars}")
|
||||||
raise ZStackError
|
raise ZStackError
|
||||||
self.start_addr += 1
|
self.start_addr += 1
|
||||||
|
|
||||||
|
|
@ -74,31 +84,26 @@ class ZRoutine:
|
||||||
for i in range(0, len(args)):
|
for i in range(0, len(args)):
|
||||||
self.local_vars[i] = args[i]
|
self.local_vars[i] = args[i]
|
||||||
|
|
||||||
|
|
||||||
def pretty_print(self):
|
def pretty_print(self):
|
||||||
"Display a ZRoutine nicely, for debugging purposes."
|
"Display a ZRoutine nicely, for debugging purposes."
|
||||||
|
|
||||||
log("ZRoutine: start address: %d" % self.start_addr)
|
log(f"ZRoutine: start address: {self.start_addr}")
|
||||||
log("ZRoutine: return value address: %d" % self.return_addr)
|
log(f"ZRoutine: return value address: {self.return_addr}")
|
||||||
log("ZRoutine: program counter: %d" % self.program_counter)
|
log(f"ZRoutine: program counter: {self.program_counter}")
|
||||||
log("ZRoutine: local variables: %d" % self.local_vars)
|
log(f"ZRoutine: local variables: {self.local_vars}")
|
||||||
|
|
||||||
|
|
||||||
class ZStackBottom:
|
class ZStackBottom:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.program_counter = 0 # used as a cache only
|
self.program_counter = 0 # used as a cache only
|
||||||
|
|
||||||
|
|
||||||
class ZStackManager:
|
class ZStackManager:
|
||||||
|
|
||||||
def __init__(self, zmem):
|
def __init__(self, zmem):
|
||||||
|
|
||||||
self._memory = zmem
|
self._memory = zmem
|
||||||
self._stackbottom = ZStackBottom()
|
self._stackbottom = ZStackBottom()
|
||||||
self._call_stack = [self._stackbottom]
|
self._call_stack = [self._stackbottom]
|
||||||
|
|
||||||
|
|
||||||
def get_local_variable(self, varnum):
|
def get_local_variable(self, varnum):
|
||||||
"""Return value of local variable VARNUM from currently-running
|
"""Return value of local variable VARNUM from currently-running
|
||||||
routine. VARNUM must be a value between 0 and 15, and must
|
routine. VARNUM must be a value between 0 and 15, and must
|
||||||
|
|
@ -112,8 +117,7 @@ class ZStackManager:
|
||||||
|
|
||||||
current_routine = self._call_stack[-1]
|
current_routine = self._call_stack[-1]
|
||||||
|
|
||||||
return current_routine.local_vars[varnum]
|
return current_routine.local_vars[varnum] # type: ignore[possibly-missing-attribute]
|
||||||
|
|
||||||
|
|
||||||
def set_local_variable(self, varnum, value):
|
def set_local_variable(self, varnum, value):
|
||||||
"""Set value of local variable VARNUM to VALUE in
|
"""Set value of local variable VARNUM to VALUE in
|
||||||
|
|
@ -128,29 +132,25 @@ class ZStackManager:
|
||||||
|
|
||||||
current_routine = self._call_stack[-1]
|
current_routine = self._call_stack[-1]
|
||||||
|
|
||||||
current_routine.local_vars[varnum] = value
|
current_routine.local_vars[varnum] = value # type: ignore[possibly-missing-attribute]
|
||||||
|
|
||||||
|
|
||||||
def push_stack(self, value):
|
def push_stack(self, value):
|
||||||
"Push VALUE onto the top of the current routine's data stack."
|
"Push VALUE onto the top of the current routine's data stack."
|
||||||
|
|
||||||
current_routine = self._call_stack[-1]
|
current_routine = self._call_stack[-1]
|
||||||
current_routine.stack.append(value)
|
current_routine.stack.append(value) # type: ignore[possibly-missing-attribute]
|
||||||
|
|
||||||
|
|
||||||
def pop_stack(self):
|
def pop_stack(self):
|
||||||
"Remove and return value from the top of the data stack."
|
"Remove and return value from the top of the data stack."
|
||||||
|
|
||||||
current_routine = self._call_stack[-1]
|
current_routine = self._call_stack[-1]
|
||||||
return current_routine.stack.pop()
|
return current_routine.stack.pop() # type: ignore[possibly-missing-attribute]
|
||||||
|
|
||||||
|
|
||||||
def get_stack_frame_index(self):
|
def get_stack_frame_index(self):
|
||||||
"Return current stack frame number. For use by 'catch' opcode."
|
"Return current stack frame number. For use by 'catch' opcode."
|
||||||
|
|
||||||
return len(self._call_stack) - 1
|
return len(self._call_stack) - 1
|
||||||
|
|
||||||
|
|
||||||
# Used by quetzal save-file parser to reconstruct stack-frames.
|
# Used by quetzal save-file parser to reconstruct stack-frames.
|
||||||
def push_routine(self, routine):
|
def push_routine(self, routine):
|
||||||
"""Blindly push a ZRoutine object to the call stack.
|
"""Blindly push a ZRoutine object to the call stack.
|
||||||
|
|
@ -160,24 +160,20 @@ class ZStackManager:
|
||||||
|
|
||||||
self._call_stack.append(routine)
|
self._call_stack.append(routine)
|
||||||
|
|
||||||
|
|
||||||
# ZPU should call this whenever it decides to call a new routine.
|
# ZPU should call this whenever it decides to call a new routine.
|
||||||
def start_routine(self, routine_addr, return_addr,
|
def start_routine(self, routine_addr, return_addr, program_counter, args):
|
||||||
program_counter, args):
|
|
||||||
"""Save the state of the currenly running routine (by examining
|
"""Save the state of the currenly running routine (by examining
|
||||||
the current value of the PROGRAM_COUNTER), and prepare for
|
the current value of the PROGRAM_COUNTER), and prepare for
|
||||||
execution of a new routine at ROUTINE_ADDR with list of initial
|
execution of a new routine at ROUTINE_ADDR with list of initial
|
||||||
arguments ARGS."""
|
arguments ARGS."""
|
||||||
|
|
||||||
new_routine = ZRoutine(routine_addr, return_addr,
|
new_routine = ZRoutine(routine_addr, return_addr, self._memory, args)
|
||||||
self._memory, args)
|
|
||||||
current_routine = self._call_stack[-1]
|
current_routine = self._call_stack[-1]
|
||||||
current_routine.program_counter = program_counter
|
current_routine.program_counter = program_counter
|
||||||
self._call_stack.append(new_routine)
|
self._call_stack.append(new_routine)
|
||||||
|
|
||||||
return new_routine.start_addr
|
return new_routine.start_addr
|
||||||
|
|
||||||
|
|
||||||
# ZPU should call this whenever it decides to return from current
|
# ZPU should call this whenever it decides to return from current
|
||||||
# routine.
|
# routine.
|
||||||
def finish_routine(self, return_value):
|
def finish_routine(self, return_value):
|
||||||
|
|
@ -190,15 +186,15 @@ class ZStackManager:
|
||||||
current_routine = self._call_stack[-1]
|
current_routine = self._call_stack[-1]
|
||||||
|
|
||||||
# Depending on many things, return stuff.
|
# Depending on many things, return stuff.
|
||||||
if exiting_routine.return_addr != None:
|
if exiting_routine.return_addr is not None: # type: ignore[possibly-missing-attribute]
|
||||||
if exiting_routine.return_addr == 0: # Push to stack
|
if exiting_routine.return_addr == 0: # type: ignore[possibly-missing-attribute]
|
||||||
|
# Push to stack
|
||||||
self.push_stack(return_value)
|
self.push_stack(return_value)
|
||||||
elif 0 < exiting_routine.return_addr < 10: # Store in local var
|
elif 0 < exiting_routine.return_addr < 10: # type: ignore[possibly-missing-attribute]
|
||||||
self.set_local_variable(exiting_routine.return_addr,
|
# Store in local var
|
||||||
return_value)
|
self.set_local_variable(exiting_routine.return_addr, return_value) # type: ignore[possibly-missing-attribute]
|
||||||
else: # Store in global var
|
else:
|
||||||
self._memory.write_global(exiting_routine.return_addr,
|
# Store in global var
|
||||||
return_value)
|
self._memory.write_global(exiting_routine.return_addr, return_value) # type: ignore[possibly-missing-attribute]
|
||||||
|
|
||||||
return current_routine.program_counter
|
return current_routine.program_counter
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
# root directory of this distribution.
|
# root directory of this distribution.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class ZOutputStream:
|
class ZOutputStream:
|
||||||
"""Abstract class representing an output stream for a z-machine."""
|
"""Abstract class representing an output stream for a z-machine."""
|
||||||
|
|
||||||
|
|
@ -38,9 +39,14 @@ class ZInputStream:
|
||||||
"has_timed_input": False,
|
"has_timed_input": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
def read_line(self, original_text=None, max_length=0,
|
def read_line(
|
||||||
|
self,
|
||||||
|
original_text=None,
|
||||||
|
max_length=0,
|
||||||
terminating_characters=None,
|
terminating_characters=None,
|
||||||
timed_input_routine=None, timed_input_interval=0):
|
timed_input_routine=None,
|
||||||
|
timed_input_interval=0,
|
||||||
|
):
|
||||||
"""Reads from the input stream and returns a unicode string
|
"""Reads from the input stream and returns a unicode string
|
||||||
representing the characters the end-user entered. The characters
|
representing the characters the end-user entered. The characters
|
||||||
are displayed to the screen as the user types them.
|
are displayed to the screen as the user types them.
|
||||||
|
|
@ -83,8 +89,7 @@ class ZInputStream:
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def read_char(self, timed_input_routine=None,
|
def read_char(self, timed_input_routine=None, timed_input_interval=0):
|
||||||
timed_input_interval=0):
|
|
||||||
"""Reads a single character from the stream and returns it as a
|
"""Reads a single character from the stream and returns it as a
|
||||||
unicode character.
|
unicode character.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ OUTPUT_PLAYER_INPUT = 4 # contains *only* the player's typed commands
|
||||||
INPUT_KEYBOARD = 0
|
INPUT_KEYBOARD = 0
|
||||||
INPUT_FILE = 1
|
INPUT_FILE = 1
|
||||||
|
|
||||||
|
|
||||||
class ZOutputStreamManager:
|
class ZOutputStreamManager:
|
||||||
"""Manages output streams for a Z-Machine."""
|
"""Manages output streams for a Z-Machine."""
|
||||||
|
|
||||||
|
|
@ -66,6 +67,7 @@ class ZOutputStreamManager:
|
||||||
for stream in self._selectedStreams:
|
for stream in self._selectedStreams:
|
||||||
self._streams[stream].write(string)
|
self._streams[stream].write(string)
|
||||||
|
|
||||||
|
|
||||||
class ZInputStreamManager:
|
class ZInputStreamManager:
|
||||||
"""Manages input streams for a Z-Machine."""
|
"""Manages input streams for a Z-Machine."""
|
||||||
|
|
||||||
|
|
@ -91,6 +93,7 @@ class ZInputStreamManager:
|
||||||
|
|
||||||
return self._streams[self._selectedStream]
|
return self._streams[self._selectedStream]
|
||||||
|
|
||||||
|
|
||||||
class ZStreamManager:
|
class ZStreamManager:
|
||||||
def __init__(self, zmem, zui):
|
def __init__(self, zmem, zui):
|
||||||
self.input = ZInputStreamManager(zui)
|
self.input = ZInputStreamManager(zui)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import itertools
|
||||||
class ZStringEndOfString(Exception):
|
class ZStringEndOfString(Exception):
|
||||||
"""No more data left in string."""
|
"""No more data left in string."""
|
||||||
|
|
||||||
|
|
||||||
class ZStringIllegalAbbrevInString(Exception):
|
class ZStringIllegalAbbrevInString(Exception):
|
||||||
"""String abbreviation encountered within a string in a context
|
"""String abbreviation encountered within a string in a context
|
||||||
where it is not allowed."""
|
where it is not allowed."""
|
||||||
|
|
@ -22,6 +23,7 @@ class ZStringTranslator:
|
||||||
|
|
||||||
def get(self, addr):
|
def get(self, addr):
|
||||||
from .bitfield import BitField
|
from .bitfield import BitField
|
||||||
|
|
||||||
pos = (addr, BitField(self._mem.read_word(addr)), 0)
|
pos = (addr, BitField(self._mem.read_word(addr)), 0)
|
||||||
|
|
||||||
s = []
|
s = []
|
||||||
|
|
@ -50,16 +52,13 @@ class ZStringTranslator:
|
||||||
# Kill processing.
|
# Kill processing.
|
||||||
raise ZStringEndOfString
|
raise ZStringEndOfString
|
||||||
# Get and return the next block.
|
# Get and return the next block.
|
||||||
return (pos[0] + 2,
|
return (pos[0] + 2, BitField(self._mem.read_word(pos[0] + 2)), 0)
|
||||||
BitField(self._mem.read_word(pos[0] + 2)),
|
|
||||||
0)
|
|
||||||
|
|
||||||
# Just increment the intra-block counter.
|
# Just increment the intra-block counter.
|
||||||
return (pos[0], pos[1], offset)
|
return (pos[0], pos[1], offset)
|
||||||
|
|
||||||
|
|
||||||
class ZCharTranslator:
|
class ZCharTranslator:
|
||||||
|
|
||||||
# The default alphabet tables for ZChar translation.
|
# The default alphabet tables for ZChar translation.
|
||||||
# As the codes 0-5 are special, alphabets start with code 0x6.
|
# As the codes 0-5 are special, alphabets start with code 0x6.
|
||||||
DEFAULT_A0 = [ord(x) for x in "abcdefghijklmnopqrstuvwxyz"]
|
DEFAULT_A0 = [ord(x) for x in "abcdefghijklmnopqrstuvwxyz"]
|
||||||
|
|
@ -109,8 +108,7 @@ class ZCharTranslator:
|
||||||
xlator = ZStringTranslator(self._mem)
|
xlator = ZStringTranslator(self._mem)
|
||||||
|
|
||||||
def _load_subtable(num, base):
|
def _load_subtable(num, base):
|
||||||
for i,zoff in [(i,base+(num*64)+(i*2))
|
for i, zoff in [(i, base + (num * 64) + (i * 2)) for i in range(0, 32)]:
|
||||||
for i in range(0, 32)]:
|
|
||||||
zaddr = self._mem.read_word(zoff)
|
zaddr = self._mem.read_word(zoff)
|
||||||
zstr = xlator.get(self._mem.word_address(zaddr))
|
zstr = xlator.get(self._mem.word_address(zaddr))
|
||||||
zchr = self.get(zstr, allow_abbreviations=False)
|
zchr = self.get(zstr, allow_abbreviations=False)
|
||||||
|
|
@ -128,11 +126,12 @@ class ZCharTranslator:
|
||||||
"""Load the special character code handlers for the current
|
"""Load the special character code handlers for the current
|
||||||
machine version.
|
machine version.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The following three functions define the three possible
|
# The following three functions define the three possible
|
||||||
# special character code handlers.
|
# special character code handlers.
|
||||||
def newline(state):
|
def newline(state):
|
||||||
"""Append ZSCII 13 (newline) to the output."""
|
"""Append ZSCII 13 (newline) to the output."""
|
||||||
state['zscii'].append(13)
|
state["zscii"].append(13)
|
||||||
|
|
||||||
def shift_alphabet(state, direction, lock):
|
def shift_alphabet(state, direction, lock):
|
||||||
"""Shift the current alphaber up or down. If lock is
|
"""Shift the current alphaber up or down. If lock is
|
||||||
|
|
@ -140,9 +139,9 @@ class ZCharTranslator:
|
||||||
after outputting 1 character. Else, the alphabet will
|
after outputting 1 character. Else, the alphabet will
|
||||||
remain unchanged until the next shift.
|
remain unchanged until the next shift.
|
||||||
"""
|
"""
|
||||||
state['curr_alpha'] = (state['curr_alpha'] + direction) % 3
|
state["curr_alpha"] = (state["curr_alpha"] + direction) % 3
|
||||||
if lock:
|
if lock:
|
||||||
state['prev_alpha'] = state['curr_alpha']
|
state["prev_alpha"] = state["curr_alpha"]
|
||||||
|
|
||||||
def abbreviation(state, abbrev):
|
def abbreviation(state, abbrev):
|
||||||
"""Insert the given abbreviation from the given table into
|
"""Insert the given abbreviation from the given table into
|
||||||
|
|
@ -152,18 +151,18 @@ class ZCharTranslator:
|
||||||
character will be the offset within that table of the
|
character will be the offset within that table of the
|
||||||
abbreviation. Set up a state handler to intercept the next
|
abbreviation. Set up a state handler to intercept the next
|
||||||
character and output the right abbreviation."""
|
character and output the right abbreviation."""
|
||||||
|
|
||||||
def write_abbreviation(state, c, subtable):
|
def write_abbreviation(state, c, subtable):
|
||||||
state['zscii'] += self._abbrevs[(subtable, c)]
|
state["zscii"] += self._abbrevs[(subtable, c)]
|
||||||
del state['state_handler']
|
del state["state_handler"]
|
||||||
|
|
||||||
# If we're parsing an abbreviation, there should be no
|
# If we're parsing an abbreviation, there should be no
|
||||||
# nested abbreviations. So this is just a sanity check for
|
# nested abbreviations. So this is just a sanity check for
|
||||||
# people feeding us bad stories.
|
# people feeding us bad stories.
|
||||||
if not state['allow_abbreviations']:
|
if not state["allow_abbreviations"]:
|
||||||
raise ZStringIllegalAbbrevInString
|
raise ZStringIllegalAbbrevInString
|
||||||
|
|
||||||
state['state_handler'] = lambda s,c: write_abbreviation(s, c,
|
state["state_handler"] = lambda s, c: write_abbreviation(s, c, abbrev)
|
||||||
abbrev)
|
|
||||||
|
|
||||||
# Register the specials handlers depending on machine version.
|
# Register the specials handlers depending on machine version.
|
||||||
if self._mem.version == 1:
|
if self._mem.version == 1:
|
||||||
|
|
@ -192,33 +191,33 @@ class ZCharTranslator:
|
||||||
}
|
}
|
||||||
|
|
||||||
def _special_zscii(self, state, char):
|
def _special_zscii(self, state, char):
|
||||||
if 'zscii_char' not in list(state.keys()):
|
if "zscii_char" not in list(state.keys()):
|
||||||
state['zscii_char'] = char
|
state["zscii_char"] = char
|
||||||
else:
|
else:
|
||||||
zchar = (state['zscii_char'] << 5) + char
|
zchar = (state["zscii_char"] << 5) + char
|
||||||
state['zscii'].append(zchar)
|
state["zscii"].append(zchar)
|
||||||
del state['zscii_char']
|
del state["zscii_char"]
|
||||||
del state['state_handler']
|
del state["state_handler"]
|
||||||
|
|
||||||
def get(self, zstr, allow_abbreviations=True):
|
def get(self, zstr, allow_abbreviations=True):
|
||||||
state = {
|
state = {
|
||||||
'curr_alpha': 0,
|
"curr_alpha": 0,
|
||||||
'prev_alpha': 0,
|
"prev_alpha": 0,
|
||||||
'zscii': [],
|
"zscii": [],
|
||||||
'allow_abbreviations': allow_abbreviations,
|
"allow_abbreviations": allow_abbreviations,
|
||||||
}
|
}
|
||||||
|
|
||||||
for c in zstr:
|
for c in zstr:
|
||||||
if 'state_handler' in list(state.keys()):
|
if "state_handler" in list(state.keys()):
|
||||||
# If a special handler has registered itself, then hand
|
# If a special handler has registered itself, then hand
|
||||||
# processing over to it.
|
# processing over to it.
|
||||||
state['state_handler'](state, c)
|
state["state_handler"](state, c) # type: ignore[call-non-callable]
|
||||||
elif c in list(self._specials.keys()):
|
elif c in list(self._specials.keys()):
|
||||||
# Hand off per-ZM version special char handling.
|
# Hand off per-ZM version special char handling.
|
||||||
self._specials[c](state)
|
self._specials[c](state)
|
||||||
elif state['curr_alpha'] == 2 and c == 6:
|
elif state["curr_alpha"] == 2 and c == 6:
|
||||||
# Handle the strange A2/6 character
|
# Handle the strange A2/6 character
|
||||||
state['state_handler'] = self._special_zscii
|
state["state_handler"] = self._special_zscii
|
||||||
else:
|
else:
|
||||||
# Do the usual Thing: append a zscii code to the
|
# Do the usual Thing: append a zscii code to the
|
||||||
# decoded sequence and revert to the "previous"
|
# decoded sequence and revert to the "previous"
|
||||||
|
|
@ -227,36 +226,97 @@ class ZCharTranslator:
|
||||||
if c == 0:
|
if c == 0:
|
||||||
# Append a space.
|
# Append a space.
|
||||||
z = 32
|
z = 32
|
||||||
elif state['curr_alpha'] == 2:
|
elif state["curr_alpha"] == 2:
|
||||||
# The symbol alphabet table only has 25 chars
|
# The symbol alphabet table only has 25 chars
|
||||||
# because of the A2/6 special char, so we need to
|
# because of the A2/6 special char, so we need to
|
||||||
# adjust differently.
|
# adjust differently.
|
||||||
z = self._alphabet[state['curr_alpha']][c-7]
|
z = self._alphabet[state["curr_alpha"]][c - 7]
|
||||||
else:
|
else:
|
||||||
z = self._alphabet[state['curr_alpha']][c-6]
|
z = self._alphabet[state["curr_alpha"]][c - 6]
|
||||||
state['zscii'].append(z)
|
state["zscii"].append(z)
|
||||||
state['curr_alpha'] = state['prev_alpha']
|
state["curr_alpha"] = state["prev_alpha"]
|
||||||
|
|
||||||
return state['zscii']
|
return state["zscii"]
|
||||||
|
|
||||||
|
|
||||||
class ZsciiTranslator:
|
class ZsciiTranslator:
|
||||||
# The default Unicode Translation Table that maps to ZSCII codes
|
# The default Unicode Translation Table that maps to ZSCII codes
|
||||||
# 155-251. The codes are unicode codepoints for a host of strange
|
# 155-251. The codes are unicode codepoints for a host of strange
|
||||||
# characters.
|
# characters.
|
||||||
DEFAULT_UTT = [chr(x) for x in
|
DEFAULT_UTT = [
|
||||||
(0xe4, 0xf6, 0xfc, 0xc4, 0xd6, 0xdc,
|
chr(x)
|
||||||
0xdf, 0xbb, 0xab, 0xeb, 0xef, 0xff,
|
for x in (
|
||||||
0xcb, 0xcf, 0xe1, 0xe9, 0xed, 0xf3,
|
0xE4,
|
||||||
0xfa, 0xfd, 0xc1, 0xc9, 0xcd, 0xd3,
|
0xF6,
|
||||||
0xda, 0xdd, 0xe0, 0xe8, 0xec, 0xf2,
|
0xFC,
|
||||||
0xf9, 0xc0, 0xc8, 0xcc, 0xd2, 0xd9,
|
0xC4,
|
||||||
0xe2, 0xea, 0xee, 0xf4, 0xfb, 0xc2,
|
0xD6,
|
||||||
0xca, 0xce, 0xd4, 0xdb, 0xe5, 0xc5,
|
0xDC,
|
||||||
0xf8, 0xd8, 0xe3, 0xf1, 0xf5, 0xc3,
|
0xDF,
|
||||||
0xd1, 0xd5, 0xe6, 0xc6, 0xe7, 0xc7,
|
0xBB,
|
||||||
0xfe, 0xf0, 0xde, 0xd0, 0xa3, 0x153,
|
0xAB,
|
||||||
0x152, 0xa1, 0xbf)]
|
0xEB,
|
||||||
|
0xEF,
|
||||||
|
0xFF,
|
||||||
|
0xCB,
|
||||||
|
0xCF,
|
||||||
|
0xE1,
|
||||||
|
0xE9,
|
||||||
|
0xED,
|
||||||
|
0xF3,
|
||||||
|
0xFA,
|
||||||
|
0xFD,
|
||||||
|
0xC1,
|
||||||
|
0xC9,
|
||||||
|
0xCD,
|
||||||
|
0xD3,
|
||||||
|
0xDA,
|
||||||
|
0xDD,
|
||||||
|
0xE0,
|
||||||
|
0xE8,
|
||||||
|
0xEC,
|
||||||
|
0xF2,
|
||||||
|
0xF9,
|
||||||
|
0xC0,
|
||||||
|
0xC8,
|
||||||
|
0xCC,
|
||||||
|
0xD2,
|
||||||
|
0xD9,
|
||||||
|
0xE2,
|
||||||
|
0xEA,
|
||||||
|
0xEE,
|
||||||
|
0xF4,
|
||||||
|
0xFB,
|
||||||
|
0xC2,
|
||||||
|
0xCA,
|
||||||
|
0xCE,
|
||||||
|
0xD4,
|
||||||
|
0xDB,
|
||||||
|
0xE5,
|
||||||
|
0xC5,
|
||||||
|
0xF8,
|
||||||
|
0xD8,
|
||||||
|
0xE3,
|
||||||
|
0xF1,
|
||||||
|
0xF5,
|
||||||
|
0xC3,
|
||||||
|
0xD1,
|
||||||
|
0xD5,
|
||||||
|
0xE6,
|
||||||
|
0xC6,
|
||||||
|
0xE7,
|
||||||
|
0xC7,
|
||||||
|
0xFE,
|
||||||
|
0xF0,
|
||||||
|
0xDE,
|
||||||
|
0xD0,
|
||||||
|
0xA3,
|
||||||
|
0x153,
|
||||||
|
0x152,
|
||||||
|
0xA1,
|
||||||
|
0xBF,
|
||||||
|
)
|
||||||
|
]
|
||||||
# And here is the offset at which the Unicode Translation Table
|
# And here is the offset at which the Unicode Translation Table
|
||||||
# starts.
|
# starts.
|
||||||
UTT_OFFSET = 155
|
UTT_OFFSET = 155
|
||||||
|
|
@ -299,13 +359,8 @@ class ZsciiTranslator:
|
||||||
|
|
||||||
def __init__(self, zmem):
|
def __init__(self, zmem):
|
||||||
self._mem = zmem
|
self._mem = zmem
|
||||||
self._output_table = {
|
self._output_table = {0: "", 10: "\n"}
|
||||||
0 : "",
|
self._input_table = {"\n": 10}
|
||||||
10: "\n"
|
|
||||||
}
|
|
||||||
self._input_table = {
|
|
||||||
"\n": 10
|
|
||||||
}
|
|
||||||
|
|
||||||
self._load_unicode_table()
|
self._load_unicode_table()
|
||||||
|
|
||||||
|
|
@ -324,8 +379,11 @@ class ZsciiTranslator:
|
||||||
# Oh and we also pull the items from the subclass into this
|
# Oh and we also pull the items from the subclass into this
|
||||||
# instance, so as to make reference to these special codes
|
# instance, so as to make reference to these special codes
|
||||||
# easier.
|
# easier.
|
||||||
for name,code in [(c,v) for c,v in list(self.Input.__dict__.items())
|
for name, code in [
|
||||||
if not c.startswith('__')]:
|
(c, v)
|
||||||
|
for c, v in list(self.Input.__dict__.items())
|
||||||
|
if not c.startswith("__")
|
||||||
|
]:
|
||||||
self._input_table[code] = code
|
self._input_table[code] = code
|
||||||
setattr(self, name, code)
|
setattr(self, name, code)
|
||||||
|
|
||||||
|
|
@ -350,18 +408,22 @@ class ZsciiTranslator:
|
||||||
#
|
#
|
||||||
# Then there is a unicode translation table other than the
|
# Then there is a unicode translation table other than the
|
||||||
# default that needs loading.
|
# default that needs loading.
|
||||||
if (ext_table_addr != 0 and
|
if (
|
||||||
self._mem.read_word(ext_table_addr) >= 3 and
|
ext_table_addr != 0
|
||||||
self._mem.read_word(ext_table_addr+6) != 0):
|
and self._mem.read_word(ext_table_addr) >= 3
|
||||||
|
and self._mem.read_word(ext_table_addr + 6) != 0
|
||||||
|
):
|
||||||
|
# Get the unicode translation table address
|
||||||
|
utt_addr = self._mem.read_word(ext_table_addr + 6)
|
||||||
|
|
||||||
# The first byte is the number of unicode characters
|
# The first byte is the number of unicode characters
|
||||||
# in the table.
|
# in the table.
|
||||||
utt_len = self._mem[ext_table_addr]
|
utt_len = self._mem[utt_addr]
|
||||||
|
|
||||||
# Build the range of addresses to load from, and build
|
# Build the range of addresses to load from, and build
|
||||||
# the unicode translation table as a list of unicode
|
# the unicode translation table as a list of unicode
|
||||||
# chars.
|
# chars.
|
||||||
utt_range = range(ext_table+1, ext_table+1+(utt_len*2), 2)
|
utt_range = range(utt_addr + 1, utt_addr + 1 + (utt_len * 2), 2)
|
||||||
utt = [chr(self._mem.read_word(i)) for i in utt_range]
|
utt = [chr(self._mem.read_word(i)) for i in utt_range]
|
||||||
else:
|
else:
|
||||||
utt = self.DEFAULT_UTT
|
utt = self.DEFAULT_UTT
|
||||||
|
|
@ -380,7 +442,7 @@ class ZsciiTranslator:
|
||||||
try:
|
try:
|
||||||
return self._output_table[index]
|
return self._output_table[index]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise IndexError("No such ZSCII character")
|
raise IndexError("No such ZSCII character") from None
|
||||||
|
|
||||||
def utoz(self, char):
|
def utoz(self, char):
|
||||||
"""Translate the given Unicode code into the corresponding
|
"""Translate the given Unicode code into the corresponding
|
||||||
|
|
@ -389,10 +451,10 @@ class ZsciiTranslator:
|
||||||
try:
|
try:
|
||||||
return self._input_table[char]
|
return self._input_table[char]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise IndexError("No such input character")
|
raise IndexError("No such input character") from None
|
||||||
|
|
||||||
def get(self, zscii):
|
def get(self, zscii):
|
||||||
return ''.join([self.ztou(c) for c in zscii])
|
return "".join([self.ztou(c) for c in zscii])
|
||||||
|
|
||||||
|
|
||||||
class ZStringFactory:
|
class ZStringFactory:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue