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.
|
||||||
|
|
@ -31,7 +32,7 @@ class BitField:
|
||||||
start, stop = index.start, index.stop
|
start, stop = index.start, index.stop
|
||||||
if start > stop:
|
if start > stop:
|
||||||
(start, stop) = (stop, start)
|
(start, stop) = (stop, start)
|
||||||
mask = (1<<(stop - start)) -1
|
mask = (1 << (stop - start)) - 1
|
||||||
return (self._d >> start) & mask
|
return (self._d >> start) & mask
|
||||||
else:
|
else:
|
||||||
return (self._d >> index) & 1
|
return (self._d >> index) & 1
|
||||||
|
|
@ -42,7 +43,7 @@ class BitField:
|
||||||
value = ord(value)
|
value = ord(value)
|
||||||
if isinstance(index, slice):
|
if isinstance(index, slice):
|
||||||
start, stop = index.start, index.stop
|
start, stop = index.start, index.stop
|
||||||
mask = (1<<(stop - start)) -1
|
mask = (1 << (stop - start)) - 1
|
||||||
value = (value & mask) << start
|
value = (value & mask) << start
|
||||||
mask = mask << start
|
mask = mask << start
|
||||||
self._d = (self._d & ~mask) | value
|
self._d = (self._d & ~mask) | value
|
||||||
|
|
@ -50,7 +51,7 @@ class BitField:
|
||||||
else:
|
else:
|
||||||
value = (value) << index
|
value = (value) << index
|
||||||
mask = (1) << index
|
mask = (1) << index
|
||||||
self._d = (self._d & ~mask) | value
|
self._d = (self._d & ~mask) | value
|
||||||
|
|
||||||
def __int__(self):
|
def __int__(self):
|
||||||
"""Return the whole bitfield as an integer."""
|
"""Return the whole bitfield as an integer."""
|
||||||
|
|
@ -58,5 +59,4 @@ class BitField:
|
||||||
|
|
||||||
def to_str(self, len):
|
def to_str(self, len):
|
||||||
"""Print the binary representation of the bitfield."""
|
"""Print the binary representation of the bitfield."""
|
||||||
return ''.join(["%d" % self[i]
|
return "".join([f"{self[i]}" for i in range(len - 1, -1, -1)])
|
||||||
for i in range(len-1,-1,-1)])
|
|
||||||
|
|
|
||||||
|
|
@ -80,38 +80,37 @@ evtype_Redraw = 6
|
||||||
evtype_SoundNotify = 7
|
evtype_SoundNotify = 7
|
||||||
evtype_Hyperlink = 8
|
evtype_Hyperlink = 8
|
||||||
|
|
||||||
class event_t(ctypes.Structure):
|
|
||||||
_fields_ = [("type", glui32),
|
|
||||||
("win", winid_t),
|
|
||||||
("val1", glui32),
|
|
||||||
("val2", glui32)]
|
|
||||||
|
|
||||||
keycode_Unknown = 0xffffffff
|
class event_t(ctypes.Structure):
|
||||||
keycode_Left = 0xfffffffe
|
_fields_ = [("type", glui32), ("win", winid_t), ("val1", glui32), ("val2", glui32)]
|
||||||
keycode_Right = 0xfffffffd
|
|
||||||
keycode_Up = 0xfffffffc
|
|
||||||
keycode_Down = 0xfffffffb
|
keycode_Unknown = 0xFFFFFFFF
|
||||||
keycode_Return = 0xfffffffa
|
keycode_Left = 0xFFFFFFFE
|
||||||
keycode_Delete = 0xfffffff9
|
keycode_Right = 0xFFFFFFFD
|
||||||
keycode_Escape = 0xfffffff8
|
keycode_Up = 0xFFFFFFFC
|
||||||
keycode_Tab = 0xfffffff7
|
keycode_Down = 0xFFFFFFFB
|
||||||
keycode_PageUp = 0xfffffff6
|
keycode_Return = 0xFFFFFFFA
|
||||||
keycode_PageDown = 0xfffffff5
|
keycode_Delete = 0xFFFFFFF9
|
||||||
keycode_Home = 0xfffffff4
|
keycode_Escape = 0xFFFFFFF8
|
||||||
keycode_End = 0xfffffff3
|
keycode_Tab = 0xFFFFFFF7
|
||||||
keycode_Func1 = 0xffffffef
|
keycode_PageUp = 0xFFFFFFF6
|
||||||
keycode_Func2 = 0xffffffee
|
keycode_PageDown = 0xFFFFFFF5
|
||||||
keycode_Func3 = 0xffffffed
|
keycode_Home = 0xFFFFFFF4
|
||||||
keycode_Func4 = 0xffffffec
|
keycode_End = 0xFFFFFFF3
|
||||||
keycode_Func5 = 0xffffffeb
|
keycode_Func1 = 0xFFFFFFEF
|
||||||
keycode_Func6 = 0xffffffea
|
keycode_Func2 = 0xFFFFFFEE
|
||||||
keycode_Func7 = 0xffffffe9
|
keycode_Func3 = 0xFFFFFFED
|
||||||
keycode_Func8 = 0xffffffe8
|
keycode_Func4 = 0xFFFFFFEC
|
||||||
keycode_Func9 = 0xffffffe7
|
keycode_Func5 = 0xFFFFFFEB
|
||||||
keycode_Func10 = 0xffffffe6
|
keycode_Func6 = 0xFFFFFFEA
|
||||||
keycode_Func11 = 0xffffffe5
|
keycode_Func7 = 0xFFFFFFE9
|
||||||
keycode_Func12 = 0xffffffe4
|
keycode_Func8 = 0xFFFFFFE8
|
||||||
keycode_MAXVAL = 28
|
keycode_Func9 = 0xFFFFFFE7
|
||||||
|
keycode_Func10 = 0xFFFFFFE6
|
||||||
|
keycode_Func11 = 0xFFFFFFE5
|
||||||
|
keycode_Func12 = 0xFFFFFFE4
|
||||||
|
keycode_MAXVAL = 28
|
||||||
|
|
||||||
style_Normal = 0
|
style_Normal = 0
|
||||||
style_Emphasized = 1
|
style_Emphasized = 1
|
||||||
|
|
@ -126,9 +125,10 @@ style_User1 = 9
|
||||||
style_User2 = 10
|
style_User2 = 10
|
||||||
style_NUMSTYLES = 11
|
style_NUMSTYLES = 11
|
||||||
|
|
||||||
|
|
||||||
class stream_result_t(ctypes.Structure):
|
class stream_result_t(ctypes.Structure):
|
||||||
_fields_ = [("readcount", glui32),
|
_fields_ = [("readcount", glui32), ("writecount", glui32)]
|
||||||
("writecount", glui32)]
|
|
||||||
|
|
||||||
wintype_AllTypes = 0
|
wintype_AllTypes = 0
|
||||||
wintype_Pair = 1
|
wintype_Pair = 1
|
||||||
|
|
@ -137,23 +137,23 @@ wintype_TextBuffer = 3
|
||||||
wintype_TextGrid = 4
|
wintype_TextGrid = 4
|
||||||
wintype_Graphics = 5
|
wintype_Graphics = 5
|
||||||
|
|
||||||
winmethod_Left = 0x00
|
winmethod_Left = 0x00
|
||||||
winmethod_Right = 0x01
|
winmethod_Right = 0x01
|
||||||
winmethod_Above = 0x02
|
winmethod_Above = 0x02
|
||||||
winmethod_Below = 0x03
|
winmethod_Below = 0x03
|
||||||
winmethod_DirMask = 0x0f
|
winmethod_DirMask = 0x0F
|
||||||
|
|
||||||
winmethod_Fixed = 0x10
|
winmethod_Fixed = 0x10
|
||||||
winmethod_Proportional = 0x20
|
winmethod_Proportional = 0x20
|
||||||
winmethod_DivisionMask = 0xf0
|
winmethod_DivisionMask = 0xF0
|
||||||
|
|
||||||
fileusage_Data = 0x00
|
fileusage_Data = 0x00
|
||||||
fileusage_SavedGame = 0x01
|
fileusage_SavedGame = 0x01
|
||||||
fileusage_Transcript = 0x02
|
fileusage_Transcript = 0x02
|
||||||
fileusage_InputRecord = 0x03
|
fileusage_InputRecord = 0x03
|
||||||
fileusage_TypeMask = 0x0f
|
fileusage_TypeMask = 0x0F
|
||||||
|
|
||||||
fileusage_TextMode = 0x100
|
fileusage_TextMode = 0x100
|
||||||
fileusage_BinaryMode = 0x000
|
fileusage_BinaryMode = 0x000
|
||||||
|
|
||||||
filemode_Write = 0x01
|
filemode_Write = 0x01
|
||||||
|
|
@ -190,17 +190,26 @@ CORE_GLK_LIB_API = [
|
||||||
(None, "glk_exit", ()),
|
(None, "glk_exit", ()),
|
||||||
(None, "glk_tick", ()),
|
(None, "glk_tick", ()),
|
||||||
(glui32, "glk_gestalt", (glui32, glui32)),
|
(glui32, "glk_gestalt", (glui32, glui32)),
|
||||||
(glui32, "glk_gestalt_ext", (glui32, glui32, ctypes.POINTER(glui32),
|
(glui32, "glk_gestalt_ext", (glui32, glui32, ctypes.POINTER(glui32), glui32)),
|
||||||
glui32)),
|
|
||||||
(winid_t, "glk_window_get_root", ()),
|
(winid_t, "glk_window_get_root", ()),
|
||||||
(winid_t, "glk_window_open", (winid_t, glui32, glui32, glui32, glui32)),
|
(winid_t, "glk_window_open", (winid_t, glui32, glui32, glui32, glui32)),
|
||||||
(None, "glk_window_close", (winid_t, ctypes.POINTER(stream_result_t))),
|
(None, "glk_window_close", (winid_t, ctypes.POINTER(stream_result_t))),
|
||||||
(None, "glk_window_get_size", (winid_t, ctypes.POINTER(glui32),
|
(
|
||||||
ctypes.POINTER(glui32)) ),
|
None,
|
||||||
|
"glk_window_get_size",
|
||||||
|
(winid_t, ctypes.POINTER(glui32), ctypes.POINTER(glui32)),
|
||||||
|
),
|
||||||
(None, "glk_window_set_arrangement", (winid_t, glui32, glui32, winid_t)),
|
(None, "glk_window_set_arrangement", (winid_t, glui32, glui32, winid_t)),
|
||||||
(None, "glk_window_get_arrangement", (winid_t, ctypes.POINTER(glui32),
|
(
|
||||||
ctypes.POINTER(glui32),
|
None,
|
||||||
ctypes.POINTER(winid_t))),
|
"glk_window_get_arrangement",
|
||||||
|
(
|
||||||
|
winid_t,
|
||||||
|
ctypes.POINTER(glui32),
|
||||||
|
ctypes.POINTER(glui32),
|
||||||
|
ctypes.POINTER(winid_t),
|
||||||
|
),
|
||||||
|
),
|
||||||
(winid_t, "glk_window_iterate", (winid_t, ctypes.POINTER(glui32))),
|
(winid_t, "glk_window_iterate", (winid_t, ctypes.POINTER(glui32))),
|
||||||
(glui32, "glk_window_get_rock", (winid_t,)),
|
(glui32, "glk_window_get_rock", (winid_t,)),
|
||||||
(glui32, "glk_window_get_type", (winid_t,)),
|
(glui32, "glk_window_get_type", (winid_t,)),
|
||||||
|
|
@ -213,8 +222,7 @@ CORE_GLK_LIB_API = [
|
||||||
(strid_t, "glk_window_get_echo_stream", (winid_t,)),
|
(strid_t, "glk_window_get_echo_stream", (winid_t,)),
|
||||||
(None, "glk_set_window", (winid_t,)),
|
(None, "glk_set_window", (winid_t,)),
|
||||||
(strid_t, "glk_stream_open_file", (frefid_t, glui32, glui32)),
|
(strid_t, "glk_stream_open_file", (frefid_t, glui32, glui32)),
|
||||||
(strid_t, "glk_stream_open_memory", (ctypes.c_char_p,
|
(strid_t, "glk_stream_open_memory", (ctypes.c_char_p, glui32, glui32, glui32)),
|
||||||
glui32, glui32, glui32)),
|
|
||||||
(None, "glk_stream_close", (strid_t, ctypes.POINTER(stream_result_t))),
|
(None, "glk_stream_close", (strid_t, ctypes.POINTER(stream_result_t))),
|
||||||
(strid_t, "glk_stream_iterate", (strid_t, ctypes.POINTER(glui32))),
|
(strid_t, "glk_stream_iterate", (strid_t, ctypes.POINTER(glui32))),
|
||||||
(glui32, "glk_stream_get_rock", (strid_t,)),
|
(glui32, "glk_stream_get_rock", (strid_t,)),
|
||||||
|
|
@ -236,14 +244,11 @@ CORE_GLK_LIB_API = [
|
||||||
(None, "glk_stylehint_set", (glui32, glui32, glui32, glsi32)),
|
(None, "glk_stylehint_set", (glui32, glui32, glui32, glsi32)),
|
||||||
(None, "glk_stylehint_clear", (glui32, glui32, glui32)),
|
(None, "glk_stylehint_clear", (glui32, glui32, glui32)),
|
||||||
(glui32, "glk_style_distinguish", (winid_t, glui32, glui32)),
|
(glui32, "glk_style_distinguish", (winid_t, glui32, glui32)),
|
||||||
(glui32, "glk_style_measure", (winid_t, glui32, glui32,
|
(glui32, "glk_style_measure", (winid_t, glui32, glui32, ctypes.POINTER(glui32))),
|
||||||
ctypes.POINTER(glui32))),
|
|
||||||
(frefid_t, "glk_fileref_create_temp", (glui32, glui32)),
|
(frefid_t, "glk_fileref_create_temp", (glui32, glui32)),
|
||||||
(frefid_t, "glk_fileref_create_by_name", (glui32, ctypes.c_char_p,
|
(frefid_t, "glk_fileref_create_by_name", (glui32, ctypes.c_char_p, glui32)),
|
||||||
glui32)),
|
|
||||||
(frefid_t, "glk_fileref_create_by_prompt", (glui32, glui32, glui32)),
|
(frefid_t, "glk_fileref_create_by_prompt", (glui32, glui32, glui32)),
|
||||||
(frefid_t, "glk_fileref_create_from_fileref", (glui32, frefid_t,
|
(frefid_t, "glk_fileref_create_from_fileref", (glui32, frefid_t, glui32)),
|
||||||
glui32)),
|
|
||||||
(None, "glk_fileref_destroy", (frefid_t,)),
|
(None, "glk_fileref_destroy", (frefid_t,)),
|
||||||
(frefid_t, "glk_fileref_iterate", (frefid_t, ctypes.POINTER(glui32))),
|
(frefid_t, "glk_fileref_iterate", (frefid_t, ctypes.POINTER(glui32))),
|
||||||
(glui32, "glk_fileref_get_rock", (frefid_t,)),
|
(glui32, "glk_fileref_get_rock", (frefid_t,)),
|
||||||
|
|
@ -252,14 +257,13 @@ CORE_GLK_LIB_API = [
|
||||||
(None, "glk_select", (ctypes.POINTER(event_t),)),
|
(None, "glk_select", (ctypes.POINTER(event_t),)),
|
||||||
(None, "glk_select_poll", (ctypes.POINTER(event_t),)),
|
(None, "glk_select_poll", (ctypes.POINTER(event_t),)),
|
||||||
(None, "glk_request_timer_events", (glui32,)),
|
(None, "glk_request_timer_events", (glui32,)),
|
||||||
(None, "glk_request_line_event", (winid_t, ctypes.c_char_p, glui32,
|
(None, "glk_request_line_event", (winid_t, ctypes.c_char_p, glui32, glui32)),
|
||||||
glui32)),
|
|
||||||
(None, "glk_request_char_event", (winid_t,)),
|
(None, "glk_request_char_event", (winid_t,)),
|
||||||
(None, "glk_request_mouse_event", (winid_t,)),
|
(None, "glk_request_mouse_event", (winid_t,)),
|
||||||
(None, "glk_cancel_line_event", (winid_t, ctypes.POINTER(event_t))),
|
(None, "glk_cancel_line_event", (winid_t, ctypes.POINTER(event_t))),
|
||||||
(None, "glk_cancel_char_event", (winid_t,)),
|
(None, "glk_cancel_char_event", (winid_t,)),
|
||||||
(None, "glk_cancel_mouse_event", (winid_t,)),
|
(None, "glk_cancel_mouse_event", (winid_t,)),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Function prototypes for the optional Unicode extension of the Glk
|
# Function prototypes for the optional Unicode extension of the Glk
|
||||||
# API.
|
# API.
|
||||||
|
|
@ -269,20 +273,24 @@ UNICODE_GLK_LIB_API = [
|
||||||
(None, "glk_put_buffer_uni", (ctypes.POINTER(glui32), glui32)),
|
(None, "glk_put_buffer_uni", (ctypes.POINTER(glui32), glui32)),
|
||||||
(None, "glk_put_char_stream_uni", (strid_t, glui32)),
|
(None, "glk_put_char_stream_uni", (strid_t, glui32)),
|
||||||
(None, "glk_put_string_stream_uni", (strid_t, ctypes.POINTER(glui32))),
|
(None, "glk_put_string_stream_uni", (strid_t, ctypes.POINTER(glui32))),
|
||||||
(None, "glk_put_buffer_stream_uni", (strid_t, ctypes.POINTER(glui32),
|
(None, "glk_put_buffer_stream_uni", (strid_t, ctypes.POINTER(glui32), glui32)),
|
||||||
glui32)),
|
|
||||||
(glsi32, "glk_get_char_stream_uni", (strid_t,)),
|
(glsi32, "glk_get_char_stream_uni", (strid_t,)),
|
||||||
(glui32, "glk_get_buffer_stream_uni", (strid_t, ctypes.POINTER(glui32),
|
(glui32, "glk_get_buffer_stream_uni", (strid_t, ctypes.POINTER(glui32), glui32)),
|
||||||
glui32)),
|
(glui32, "glk_get_line_stream_uni", (strid_t, ctypes.POINTER(glui32), glui32)),
|
||||||
(glui32, "glk_get_line_stream_uni", (strid_t, ctypes.POINTER(glui32),
|
|
||||||
glui32)),
|
|
||||||
(strid_t, "glk_stream_open_file_uni", (frefid_t, glui32, glui32)),
|
(strid_t, "glk_stream_open_file_uni", (frefid_t, glui32, glui32)),
|
||||||
(strid_t, "glk_stream_open_memory_uni", (ctypes.POINTER(glui32),
|
(
|
||||||
glui32, glui32, glui32)),
|
strid_t,
|
||||||
|
"glk_stream_open_memory_uni",
|
||||||
|
(ctypes.POINTER(glui32), glui32, glui32, glui32),
|
||||||
|
),
|
||||||
(None, "glk_request_char_event_uni", (winid_t,)),
|
(None, "glk_request_char_event_uni", (winid_t,)),
|
||||||
(None, "glk_request_line_event_uni", (winid_t, ctypes.POINTER(glui32),
|
(
|
||||||
glui32, glui32))
|
None,
|
||||||
]
|
"glk_request_line_event_uni",
|
||||||
|
(winid_t, ctypes.POINTER(glui32), glui32, glui32),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class GlkLib:
|
class GlkLib:
|
||||||
"""Encapsulates the ctypes interface to a Glk shared library. When
|
"""Encapsulates the ctypes interface to a Glk shared library. When
|
||||||
|
|
@ -299,7 +307,7 @@ class GlkLib:
|
||||||
|
|
||||||
self.__bind_prototypes(CORE_GLK_LIB_API)
|
self.__bind_prototypes(CORE_GLK_LIB_API)
|
||||||
|
|
||||||
if self.glk_gestalt(gestalt_Unicode, 0) == 1:
|
if self.glk_gestalt(gestalt_Unicode, 0) == 1: # type: ignore[unresolved-attribute]
|
||||||
self.__bind_prototypes(UNICODE_GLK_LIB_API)
|
self.__bind_prototypes(UNICODE_GLK_LIB_API)
|
||||||
else:
|
else:
|
||||||
self.__bind_not_implemented_prototypes(UNICODE_GLK_LIB_API)
|
self.__bind_not_implemented_prototypes(UNICODE_GLK_LIB_API)
|
||||||
|
|
@ -324,8 +332,7 @@ class GlkLib:
|
||||||
support some optional extension of the Glk API."""
|
support some optional extension of the Glk API."""
|
||||||
|
|
||||||
def notImplementedFunction(*args, **kwargs):
|
def notImplementedFunction(*args, **kwargs):
|
||||||
raise NotImplementedError( "Function not implemented " \
|
raise NotImplementedError("Function not implemented by this Glk library.")
|
||||||
"by this Glk library." )
|
|
||||||
|
|
||||||
for function_prototype in function_prototypes:
|
for function_prototype in function_prototypes:
|
||||||
_, function_name, _ = function_prototype
|
_, function_name, _ = function_prototype
|
||||||
|
|
|
||||||
|
|
@ -29,435 +29,434 @@ from .zlogging import log
|
||||||
# 4-byte chunkname, 4-byte length, length bytes of data
|
# 4-byte chunkname, 4-byte length, length bytes of data
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
|
|
||||||
class QuetzalError(Exception):
|
class QuetzalError(Exception):
|
||||||
"General exception for Quetzal classes."
|
"General exception for Quetzal classes."
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class QuetzalMalformedChunk(QuetzalError):
|
class QuetzalMalformedChunk(QuetzalError):
|
||||||
"Malformed chunk detected."
|
"Malformed chunk detected."
|
||||||
|
|
||||||
|
|
||||||
class QuetzalNoSuchSavefile(QuetzalError):
|
class QuetzalNoSuchSavefile(QuetzalError):
|
||||||
"Cannot locate save-game file."
|
"Cannot locate save-game file."
|
||||||
|
|
||||||
|
|
||||||
class QuetzalUnrecognizedFileFormat(QuetzalError):
|
class QuetzalUnrecognizedFileFormat(QuetzalError):
|
||||||
"Not a valid Quetzal file."
|
"Not a valid Quetzal file."
|
||||||
|
|
||||||
|
|
||||||
class QuetzalIllegalChunkOrder(QuetzalError):
|
class QuetzalIllegalChunkOrder(QuetzalError):
|
||||||
"IFhd chunk came after Umem/Cmem/Stks chunks (see section 5.4)."
|
"IFhd chunk came after Umem/Cmem/Stks chunks (see section 5.4)."
|
||||||
|
|
||||||
|
|
||||||
class QuetzalMismatchedFile(QuetzalError):
|
class QuetzalMismatchedFile(QuetzalError):
|
||||||
"Quetzal file dosen't match current game."
|
"Quetzal file dosen't match current game."
|
||||||
|
|
||||||
|
|
||||||
class QuetzalMemoryOutOfBounds(QuetzalError):
|
class QuetzalMemoryOutOfBounds(QuetzalError):
|
||||||
"Decompressed dynamic memory has gone out of bounds."
|
"Decompressed dynamic memory has gone out of bounds."
|
||||||
|
|
||||||
|
|
||||||
class QuetzalMemoryMismatch(QuetzalError):
|
class QuetzalMemoryMismatch(QuetzalError):
|
||||||
"Savefile's dynamic memory image is incorrectly sized."
|
"Savefile's dynamic memory image is incorrectly sized."
|
||||||
|
|
||||||
|
|
||||||
class QuetzalStackFrameOverflow(QuetzalError):
|
class QuetzalStackFrameOverflow(QuetzalError):
|
||||||
"Stack frame parsing went beyond bounds of 'Stks' chunk."
|
"Stack frame parsing went beyond bounds of 'Stks' chunk."
|
||||||
|
|
||||||
|
|
||||||
class QuetzalParser:
|
class QuetzalParser:
|
||||||
"""A class to read a Quetzal save-file and modify a z-machine."""
|
"""A class to read a Quetzal save-file and modify a z-machine."""
|
||||||
|
|
||||||
def __init__(self, zmachine):
|
def __init__(self, zmachine):
|
||||||
log("Creating new instance of QuetzalParser")
|
log("Creating new instance of QuetzalParser")
|
||||||
self._zmachine = zmachine
|
self._zmachine = zmachine
|
||||||
self._seen_mem_or_stks = False
|
self._seen_mem_or_stks = False
|
||||||
self._last_loaded_metadata = {} # metadata for tests & debugging
|
self._last_loaded_metadata = {} # metadata for tests & debugging
|
||||||
|
|
||||||
|
def _parse_ifhd(self, data):
|
||||||
|
"""Parse a chunk of type IFhd, and check that the quetzal file
|
||||||
|
really belongs to the current story (by comparing release number,
|
||||||
|
serial number, and checksum.)"""
|
||||||
|
|
||||||
|
# Spec says that this chunk *must* come before memory or stack chunks.
|
||||||
|
if self._seen_mem_or_stks:
|
||||||
|
raise QuetzalIllegalChunkOrder
|
||||||
|
|
||||||
|
bytes = data
|
||||||
|
if len(bytes) != 13:
|
||||||
|
raise QuetzalMalformedChunk
|
||||||
|
|
||||||
|
chunk_release = (data[0] << 8) + data[1]
|
||||||
|
chunk_serial = data[2:8]
|
||||||
|
chunk_checksum = (data[8] << 8) + data[9]
|
||||||
|
chunk_pc = (data[10] << 16) + (data[11] << 8) + data[12]
|
||||||
|
self._zmachine._opdecoder.program_counter = chunk_pc
|
||||||
|
|
||||||
|
log(f" Found release number {chunk_release}")
|
||||||
|
log(f" Found serial number {int(chunk_serial)}")
|
||||||
|
log(f" Found checksum {chunk_checksum}")
|
||||||
|
log(f" Initial program counter value is {chunk_pc}")
|
||||||
|
self._last_loaded_metadata["release number"] = chunk_release
|
||||||
|
self._last_loaded_metadata["serial number"] = chunk_serial
|
||||||
|
self._last_loaded_metadata["checksum"] = chunk_checksum
|
||||||
|
self._last_loaded_metadata["program counter"] = chunk_pc
|
||||||
|
|
||||||
|
# Verify the save-file params against the current z-story header
|
||||||
|
mem = self._zmachine._mem
|
||||||
|
if mem.read_word(2) != chunk_release:
|
||||||
|
raise QuetzalMismatchedFile
|
||||||
|
serial_bytes = chunk_serial
|
||||||
|
if serial_bytes != mem[0x12:0x18]:
|
||||||
|
raise QuetzalMismatchedFile
|
||||||
|
mem_checksum = mem.read_word(0x1C)
|
||||||
|
if mem_checksum != 0 and (mem_checksum != chunk_checksum):
|
||||||
|
raise QuetzalMismatchedFile
|
||||||
|
log(" Quetzal file correctly verifies against original story.")
|
||||||
|
|
||||||
|
def _parse_cmem(self, data):
|
||||||
|
"""Parse a chunk of type Cmem. Decompress an image of dynamic
|
||||||
|
memory, and place it into the ZMachine."""
|
||||||
|
|
||||||
|
log(" Decompressing dynamic memory image")
|
||||||
|
self._seen_mem_or_stks = True
|
||||||
|
|
||||||
|
# Just duplicate the dynamic memory block of the pristine story image,
|
||||||
|
# and then make tweaks to it as we decode the runlength-encoding.
|
||||||
|
pmem = self._zmachine._pristine_mem
|
||||||
|
cmem = self._zmachine._mem
|
||||||
|
savegame_mem = list(pmem[pmem._dynamic_start : (pmem._dynamic_end + 1)])
|
||||||
|
memlen = len(savegame_mem)
|
||||||
|
memcounter = 0
|
||||||
|
log(f" Dynamic memory length is {memlen}")
|
||||||
|
self._last_loaded_metadata["memory length"] = memlen
|
||||||
|
|
||||||
|
runlength_bytes = data
|
||||||
|
bytelen = len(runlength_bytes)
|
||||||
|
bytecounter = 0
|
||||||
|
|
||||||
|
log(" Decompressing dynamic memory image")
|
||||||
|
while bytecounter < bytelen:
|
||||||
|
byte = runlength_bytes[bytecounter]
|
||||||
|
if byte != 0:
|
||||||
|
savegame_mem[memcounter] = byte ^ pmem[memcounter]
|
||||||
|
memcounter += 1
|
||||||
|
bytecounter += 1
|
||||||
|
log(f" Set byte {memcounter}:{savegame_mem[memcounter]}")
|
||||||
|
else:
|
||||||
|
bytecounter += 1
|
||||||
|
num_extra_zeros = runlength_bytes[bytecounter]
|
||||||
|
memcounter += 1 + num_extra_zeros
|
||||||
|
bytecounter += 1
|
||||||
|
log(f" Skipped {1 + num_extra_zeros} unchanged bytes")
|
||||||
|
if memcounter >= memlen:
|
||||||
|
raise QuetzalMemoryOutOfBounds
|
||||||
|
|
||||||
|
# If memcounter finishes less then memlen, that's totally fine, it
|
||||||
|
# just means there are no more diffs to apply.
|
||||||
|
|
||||||
|
cmem[cmem._dynamic_start : (cmem._dynamic_end + 1)] = savegame_mem
|
||||||
|
log(" Successfully installed new dynamic memory.")
|
||||||
|
|
||||||
|
def _parse_umem(self, data):
|
||||||
|
"""Parse a chunk of type Umem. Suck a raw image of dynamic memory
|
||||||
|
and place it into the ZMachine."""
|
||||||
|
|
||||||
|
### TODO: test this by either finding an interpreter that ouptuts
|
||||||
|
## this type of chunk, or by having own QuetzalWriter class
|
||||||
|
## (optionally) do it.
|
||||||
|
log(" Loading uncompressed dynamic memory image")
|
||||||
|
self._seen_mem_or_stks = True
|
||||||
|
|
||||||
|
cmem = self._zmachine._mem
|
||||||
|
dynamic_len = (cmem._dynamic_end - cmem.dynamic_start) + 1
|
||||||
|
log(f" Dynamic memory length is {dynamic_len}")
|
||||||
|
self._last_loaded_metadata["dynamic memory length"] = dynamic_len
|
||||||
|
|
||||||
|
savegame_mem = [ord(x) for x in data]
|
||||||
|
if len(savegame_mem) != dynamic_len:
|
||||||
|
raise QuetzalMemoryMismatch
|
||||||
|
|
||||||
|
cmem[cmem._dynamic_start : (cmem._dynamic_end + 1)] = savegame_mem
|
||||||
|
log(" Successfully installed new dynamic memory.")
|
||||||
|
|
||||||
|
def _parse_stks(self, data):
|
||||||
|
"""Parse a chunk of type Stks."""
|
||||||
|
|
||||||
|
log(" Begin parsing of stack frames")
|
||||||
|
|
||||||
|
# Our strategy here is simply to create an entirely new
|
||||||
|
# ZStackManager object and populate it with a series of ZRoutine
|
||||||
|
# stack-frames parses from the quetzal file. We then attach this
|
||||||
|
# new ZStackManager to our z-machine, and allow the old one to be
|
||||||
|
# garbage collected.
|
||||||
|
stackmanager = zstackmanager.ZStackManager(self._zmachine._mem)
|
||||||
|
|
||||||
|
self._seen_mem_or_stks = True
|
||||||
|
bytes = data
|
||||||
|
total_len = len(bytes)
|
||||||
|
ptr = 0
|
||||||
|
|
||||||
|
# Read successive stack frames:
|
||||||
|
while ptr < total_len:
|
||||||
|
log(" Parsing stack frame...")
|
||||||
|
return_pc = (bytes[ptr] << 16) + (bytes[ptr + 1] << 8) + bytes[ptr + 3]
|
||||||
|
ptr += 3
|
||||||
|
flags_bitfield = bitfield.BitField(bytes[ptr])
|
||||||
|
ptr += 1
|
||||||
|
_varnum = bytes[ptr] ### TODO: tells us which variable gets the result
|
||||||
|
ptr += 1
|
||||||
|
_argflag = bytes[ptr]
|
||||||
|
ptr += 1
|
||||||
|
evalstack_size = (bytes[ptr] << 8) + bytes[ptr + 1]
|
||||||
|
ptr += 2
|
||||||
|
|
||||||
|
# read anywhere from 0 to 15 local vars
|
||||||
|
local_vars = []
|
||||||
|
for _i in range(flags_bitfield[0:3]):
|
||||||
|
var = (bytes[ptr] << 8) + bytes[ptr + 1]
|
||||||
|
ptr += 2
|
||||||
|
local_vars.append(var)
|
||||||
|
log(f" Found {len(local_vars)} local vars")
|
||||||
|
|
||||||
|
# least recent to most recent stack values:
|
||||||
|
stack_values = []
|
||||||
|
for _i in range(evalstack_size):
|
||||||
|
val = (bytes[ptr] << 8) + bytes[ptr + 1]
|
||||||
|
ptr += 2
|
||||||
|
stack_values.append(val)
|
||||||
|
log(f" Found {len(stack_values)} local stack values")
|
||||||
|
|
||||||
|
### Interesting... the reconstructed stack frames have no 'start
|
||||||
|
### address'. I guess it doesn't matter, since we only need to
|
||||||
|
### pop back to particular return addresses to resume each
|
||||||
|
### routine.
|
||||||
|
|
||||||
|
### TODO: I can exactly which of the 7 args is "supplied", but I
|
||||||
|
### don't understand where the args *are*??
|
||||||
|
|
||||||
|
routine = zstackmanager.ZRoutine(
|
||||||
|
0, return_pc, self._zmachine._mem, [], local_vars, stack_values
|
||||||
|
)
|
||||||
|
stackmanager.push_routine(routine)
|
||||||
|
log(" Added new frame to stack.")
|
||||||
|
|
||||||
|
if ptr > total_len:
|
||||||
|
raise QuetzalStackFrameOverflow
|
||||||
|
|
||||||
|
self._zmachine._stackmanager = stackmanager
|
||||||
|
log(" Successfully installed all stack frames.")
|
||||||
|
|
||||||
|
def _parse_intd(self, data):
|
||||||
|
"""Parse a chunk of type IntD, which is interpreter-dependent info."""
|
||||||
|
|
||||||
|
log(" Begin parsing of interpreter-dependent metadata")
|
||||||
|
bytes = [ord(x) for x in data]
|
||||||
|
|
||||||
|
_os_id = bytes[0:3]
|
||||||
|
_flags = bytes[4]
|
||||||
|
_contents_id = bytes[5]
|
||||||
|
_reserved = bytes[6:8]
|
||||||
|
_interpreter_id = bytes[8:12]
|
||||||
|
_private_data = bytes[12:]
|
||||||
|
### TODO: finish this
|
||||||
|
|
||||||
|
# The following 3 chunks are totally optional metadata, and are
|
||||||
|
# artifacts of the larger IFF standard. We're not required to do
|
||||||
|
# anything when we see them, though maybe it would be nice to print
|
||||||
|
# them to the user?
|
||||||
|
|
||||||
|
def _parse_auth(self, data):
|
||||||
|
"""Parse a chunk of type AUTH. Display the author."""
|
||||||
|
|
||||||
|
log(f"Author of file: {data}")
|
||||||
|
self._last_loaded_metadata["author"] = data
|
||||||
|
|
||||||
|
def _parse_copyright(self, data):
|
||||||
|
"""Parse a chunk of type (c) . Display the copyright."""
|
||||||
|
|
||||||
|
log(f"Copyright: (C) {data}")
|
||||||
|
self._last_loaded_metadata["copyright"] = data
|
||||||
|
|
||||||
|
def _parse_anno(self, data):
|
||||||
|
"""Parse a chunk of type ANNO. Display any annotation"""
|
||||||
|
|
||||||
|
log(f"Annotation: {data}")
|
||||||
|
self._last_loaded_metadata["annotation"] = data
|
||||||
|
|
||||||
|
# --------- Public APIs -----------
|
||||||
|
|
||||||
|
def get_last_loaded(self):
|
||||||
|
"""Return a list of metadata about the last loaded Quetzal file, for
|
||||||
|
debugging and test verification."""
|
||||||
|
return self._last_loaded_metadata
|
||||||
|
|
||||||
|
def load(self, savefile_path):
|
||||||
|
"""Parse each chunk of the Quetzal file at SAVEFILE_PATH,
|
||||||
|
initializing associated zmachine subsystems as needed."""
|
||||||
|
|
||||||
|
self._last_loaded_metadata = {}
|
||||||
|
|
||||||
|
if not os.path.isfile(savefile_path):
|
||||||
|
raise QuetzalNoSuchSavefile
|
||||||
|
|
||||||
|
log(f"Attempting to load saved game from '{savefile_path}'")
|
||||||
|
self._file = open(savefile_path, "rb") # noqa: SIM115
|
||||||
|
|
||||||
|
# The python 'chunk' module is pretty dumb; it doesn't understand
|
||||||
|
# the FORM chunk and the way it contains nested chunks.
|
||||||
|
# Therefore, we deliberately seek 12 bytes into the file so that
|
||||||
|
# we can start sucking out chunks. This also allows us to
|
||||||
|
# validate that the FORM type is "IFZS".
|
||||||
|
header = self._file.read(4)
|
||||||
|
if header != b"FORM":
|
||||||
|
raise QuetzalUnrecognizedFileFormat
|
||||||
|
bytestring = self._file.read(4)
|
||||||
|
self._len = bytestring[0] << 24
|
||||||
|
self._len += bytestring[1] << 16
|
||||||
|
self._len += bytestring[2] << 8
|
||||||
|
self._len += bytestring[3]
|
||||||
|
log(f"Total length of FORM data is {self._len}")
|
||||||
|
self._last_loaded_metadata["total length"] = self._len
|
||||||
|
|
||||||
|
type = self._file.read(4)
|
||||||
|
if type != b"IFZS":
|
||||||
|
raise QuetzalUnrecognizedFileFormat
|
||||||
|
|
||||||
|
try:
|
||||||
|
while 1:
|
||||||
|
c = chunk.Chunk(self._file)
|
||||||
|
chunkname = c.getname()
|
||||||
|
chunksize = c.getsize()
|
||||||
|
data = c.read(chunksize)
|
||||||
|
log(f"** Found chunk ID {chunkname}: length {chunksize}")
|
||||||
|
self._last_loaded_metadata[chunkname] = chunksize
|
||||||
|
|
||||||
|
if chunkname == b"IFhd":
|
||||||
|
self._parse_ifhd(data)
|
||||||
|
elif chunkname == b"CMem":
|
||||||
|
self._parse_cmem(data)
|
||||||
|
elif chunkname == b"UMem":
|
||||||
|
self._parse_umem(data)
|
||||||
|
elif chunkname == b"Stks":
|
||||||
|
self._parse_stks(data)
|
||||||
|
elif chunkname == b"IntD":
|
||||||
|
self._parse_intd(data)
|
||||||
|
elif chunkname == b"AUTH":
|
||||||
|
self._parse_auth(data)
|
||||||
|
elif chunkname == b"(c) ":
|
||||||
|
self._parse_copyright(data)
|
||||||
|
elif chunkname == b"ANNO":
|
||||||
|
self._parse_anno(data)
|
||||||
|
else:
|
||||||
|
# spec says to ignore and skip past unrecognized chunks
|
||||||
|
pass
|
||||||
|
|
||||||
|
except EOFError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._file.close()
|
||||||
|
log("Finished parsing Quetzal file.")
|
||||||
|
|
||||||
|
|
||||||
def _parse_ifhd(self, data):
|
# ------------------------------------------------------------------------------
|
||||||
"""Parse a chunk of type IFhd, and check that the quetzal file
|
|
||||||
really belongs to the current story (by comparing release number,
|
|
||||||
serial number, and checksum.)"""
|
|
||||||
|
|
||||||
# Spec says that this chunk *must* come before memory or stack chunks.
|
|
||||||
if self._seen_mem_or_stks:
|
|
||||||
raise QuetzalIllegalChunkOrder
|
|
||||||
|
|
||||||
bytes = data
|
|
||||||
if len(bytes) != 13:
|
|
||||||
raise QuetzalMalformedChunk
|
|
||||||
|
|
||||||
chunk_release = (data[0] << 8) + data[1]
|
|
||||||
chunk_serial = data[2:8]
|
|
||||||
chunk_checksum = (data[8] << 8) + data[9]
|
|
||||||
chunk_pc = (data[10] << 16) + (data[11] << 8) + data[12]
|
|
||||||
self._zmachine._opdecoder.program_counter = chunk_pc
|
|
||||||
|
|
||||||
log(" Found release number %d" % chunk_release)
|
|
||||||
log(" Found serial number %d" % int(chunk_serial))
|
|
||||||
log(" Found checksum %d" % chunk_checksum)
|
|
||||||
log(" Initial program counter value is %d" % chunk_pc)
|
|
||||||
self._last_loaded_metadata["release number"] = chunk_release
|
|
||||||
self._last_loaded_metadata["serial number"] = chunk_serial
|
|
||||||
self._last_loaded_metadata["checksum"] = chunk_checksum
|
|
||||||
self._last_loaded_metadata["program counter"] = chunk_pc
|
|
||||||
|
|
||||||
# Verify the save-file params against the current z-story header
|
|
||||||
mem = self._zmachine._mem
|
|
||||||
if mem.read_word(2) != chunk_release:
|
|
||||||
raise QuetzalMismatchedFile
|
|
||||||
serial_bytes = chunk_serial
|
|
||||||
if serial_bytes != mem[0x12:0x18]:
|
|
||||||
raise QuetzalMismatchedFile
|
|
||||||
mem_checksum = mem.read_word(0x1C)
|
|
||||||
if mem_checksum != 0 and (mem_checksum != chunk_checksum):
|
|
||||||
raise QuetzalMismatchedFile
|
|
||||||
log(" Quetzal file correctly verifies against original story.")
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_cmem(self, data):
|
|
||||||
"""Parse a chunk of type Cmem. Decompress an image of dynamic
|
|
||||||
memory, and place it into the ZMachine."""
|
|
||||||
|
|
||||||
log(" Decompressing dynamic memory image")
|
|
||||||
self._seen_mem_or_stks = True
|
|
||||||
|
|
||||||
# Just duplicate the dynamic memory block of the pristine story image,
|
|
||||||
# and then make tweaks to it as we decode the runlength-encoding.
|
|
||||||
pmem = self._zmachine._pristine_mem
|
|
||||||
cmem = self._zmachine._mem
|
|
||||||
savegame_mem = list(pmem[pmem._dynamic_start:(pmem._dynamic_end + 1)])
|
|
||||||
memlen = len(savegame_mem)
|
|
||||||
memcounter = 0
|
|
||||||
log(" Dynamic memory length is %d" % memlen)
|
|
||||||
self._last_loaded_metadata["memory length"] = memlen
|
|
||||||
|
|
||||||
runlength_bytes = data
|
|
||||||
bytelen = len(runlength_bytes)
|
|
||||||
bytecounter = 0
|
|
||||||
|
|
||||||
log(" Decompressing dynamic memory image")
|
|
||||||
while bytecounter < bytelen:
|
|
||||||
byte = runlength_bytes[bytecounter]
|
|
||||||
if byte != 0:
|
|
||||||
savegame_mem[memcounter] = byte ^ pmem[memcounter]
|
|
||||||
memcounter += 1
|
|
||||||
bytecounter += 1
|
|
||||||
log(" Set byte %d:%d" % (memcounter, savegame_mem[memcounter]))
|
|
||||||
else:
|
|
||||||
bytecounter += 1
|
|
||||||
num_extra_zeros = runlength_bytes[bytecounter]
|
|
||||||
memcounter += (1 + num_extra_zeros)
|
|
||||||
bytecounter += 1
|
|
||||||
log(" Skipped %d unchanged bytes" % (1 + num_extra_zeros))
|
|
||||||
if memcounter >= memlen:
|
|
||||||
raise QuetzalMemoryOutOfBounds
|
|
||||||
|
|
||||||
# If memcounter finishes less then memlen, that's totally fine, it
|
|
||||||
# just means there are no more diffs to apply.
|
|
||||||
|
|
||||||
cmem[cmem._dynamic_start:(cmem._dynamic_end + 1)] = savegame_mem
|
|
||||||
log(" Successfully installed new dynamic memory.")
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_umem(self, data):
|
|
||||||
"""Parse a chunk of type Umem. Suck a raw image of dynamic memory
|
|
||||||
and place it into the ZMachine."""
|
|
||||||
|
|
||||||
### TODO: test this by either finding an interpreter that ouptuts
|
|
||||||
## this type of chunk, or by having own QuetzalWriter class
|
|
||||||
## (optionally) do it.
|
|
||||||
log(" Loading uncompressed dynamic memory image")
|
|
||||||
self._seen_mem_or_stks = True
|
|
||||||
|
|
||||||
cmem = self._zmachine._mem
|
|
||||||
dynamic_len = (cmem._dynamic_end - cmem.dynamic_start) + 1
|
|
||||||
log(" Dynamic memory length is %d" % dynamic_len)
|
|
||||||
self._last_loaded_metadata["dynamic memory length"] = dynamic_len
|
|
||||||
|
|
||||||
savegame_mem = [ord(x) for x in data]
|
|
||||||
if len(savegame_mem) != dynamic_len:
|
|
||||||
raise QuetzalMemoryMismatch
|
|
||||||
|
|
||||||
cmem[cmem._dynamic_start:(cmem._dynamic_end + 1)] = savegame_mem
|
|
||||||
log(" Successfully installed new dynamic memory.")
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_stks(self, data):
|
|
||||||
"""Parse a chunk of type Stks."""
|
|
||||||
|
|
||||||
log(" Begin parsing of stack frames")
|
|
||||||
|
|
||||||
# Our strategy here is simply to create an entirely new
|
|
||||||
# ZStackManager object and populate it with a series of ZRoutine
|
|
||||||
# stack-frames parses from the quetzal file. We then attach this
|
|
||||||
# new ZStackManager to our z-machine, and allow the old one to be
|
|
||||||
# garbage collected.
|
|
||||||
stackmanager = zstackmanager.ZStackManager(self._zmachine._mem)
|
|
||||||
|
|
||||||
self._seen_mem_or_stks = True
|
|
||||||
bytes = data
|
|
||||||
total_len = len(bytes)
|
|
||||||
ptr = 0
|
|
||||||
|
|
||||||
# Read successive stack frames:
|
|
||||||
while (ptr < total_len):
|
|
||||||
log(" Parsing stack frame...")
|
|
||||||
return_pc = (bytes[ptr] << 16) + (bytes[ptr + 1] << 8) + bytes[ptr + 3]
|
|
||||||
ptr += 3
|
|
||||||
flags_bitfield = bitfield.BitField(bytes[ptr])
|
|
||||||
ptr += 1
|
|
||||||
varnum = bytes[ptr] ### TODO: tells us which variable gets the result
|
|
||||||
ptr += 1
|
|
||||||
argflag = bytes[ptr]
|
|
||||||
ptr += 1
|
|
||||||
evalstack_size = (bytes[ptr] << 8) + bytes[ptr + 1]
|
|
||||||
ptr += 2
|
|
||||||
|
|
||||||
# read anywhere from 0 to 15 local vars
|
|
||||||
local_vars = []
|
|
||||||
for i in range(flags_bitfield[0:3]):
|
|
||||||
var = (bytes[ptr] << 8) + bytes[ptr + 1]
|
|
||||||
ptr += 2
|
|
||||||
local_vars.append(var)
|
|
||||||
log(" Found %d local vars" % len(local_vars))
|
|
||||||
|
|
||||||
# least recent to most recent stack values:
|
|
||||||
stack_values = []
|
|
||||||
for i in range(evalstack_size):
|
|
||||||
val = (bytes[ptr] << 8) + bytes[ptr + 1]
|
|
||||||
ptr += 2
|
|
||||||
stack_values.append(val)
|
|
||||||
log(" Found %d local stack values" % len(stack_values))
|
|
||||||
|
|
||||||
### Interesting... the reconstructed stack frames have no 'start
|
|
||||||
### address'. I guess it doesn't matter, since we only need to
|
|
||||||
### pop back to particular return addresses to resume each
|
|
||||||
### routine.
|
|
||||||
|
|
||||||
### TODO: I can exactly which of the 7 args is "supplied", but I
|
|
||||||
### don't understand where the args *are*??
|
|
||||||
|
|
||||||
routine = zstackmanager.ZRoutine(0, return_pc, self._zmachine._mem,
|
|
||||||
[], local_vars, stack_values)
|
|
||||||
stackmanager.push_routine(routine)
|
|
||||||
log(" Added new frame to stack.")
|
|
||||||
|
|
||||||
if (ptr > total_len):
|
|
||||||
raise QuetzalStackFrameOverflow
|
|
||||||
|
|
||||||
self._zmachine._stackmanager = stackmanager
|
|
||||||
log(" Successfully installed all stack frames.")
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_intd(self, data):
|
|
||||||
"""Parse a chunk of type IntD, which is interpreter-dependent info."""
|
|
||||||
|
|
||||||
log(" Begin parsing of interpreter-dependent metadata")
|
|
||||||
bytes = [ord(x) for x in data]
|
|
||||||
|
|
||||||
os_id = bytes[0:3]
|
|
||||||
flags = bytes[4]
|
|
||||||
contents_id = bytes[5]
|
|
||||||
reserved = bytes[6:8]
|
|
||||||
interpreter_id = bytes[8:12]
|
|
||||||
private_data = bytes[12:]
|
|
||||||
### TODO: finish this
|
|
||||||
|
|
||||||
|
|
||||||
# The following 3 chunks are totally optional metadata, and are
|
|
||||||
# artifacts of the larger IFF standard. We're not required to do
|
|
||||||
# anything when we see them, though maybe it would be nice to print
|
|
||||||
# them to the user?
|
|
||||||
|
|
||||||
def _parse_auth(self, data):
|
|
||||||
"""Parse a chunk of type AUTH. Display the author."""
|
|
||||||
|
|
||||||
log("Author of file: %s" % data)
|
|
||||||
self._last_loaded_metadata["author"] = data
|
|
||||||
|
|
||||||
def _parse_copyright(self, data):
|
|
||||||
"""Parse a chunk of type (c) . Display the copyright."""
|
|
||||||
|
|
||||||
log("Copyright: (C) %s" % data)
|
|
||||||
self._last_loaded_metadata["copyright"] = data
|
|
||||||
|
|
||||||
def _parse_anno(self, data):
|
|
||||||
"""Parse a chunk of type ANNO. Display any annotation"""
|
|
||||||
|
|
||||||
log("Annotation: %s" % data)
|
|
||||||
self._last_loaded_metadata["annotation"] = data
|
|
||||||
|
|
||||||
|
|
||||||
#--------- Public APIs -----------
|
|
||||||
|
|
||||||
|
|
||||||
def get_last_loaded(self):
|
|
||||||
"""Return a list of metadata about the last loaded Quetzal file, for
|
|
||||||
debugging and test verification."""
|
|
||||||
return self._last_loaded_metadata
|
|
||||||
|
|
||||||
def load(self, savefile_path):
|
|
||||||
"""Parse each chunk of the Quetzal file at SAVEFILE_PATH,
|
|
||||||
initializing associated zmachine subsystems as needed."""
|
|
||||||
|
|
||||||
self._last_loaded_metadata = {}
|
|
||||||
|
|
||||||
if not os.path.isfile(savefile_path):
|
|
||||||
raise QuetzalNoSuchSavefile
|
|
||||||
|
|
||||||
log("Attempting to load saved game from '%s'" % savefile_path)
|
|
||||||
self._file = open(savefile_path, 'rb')
|
|
||||||
|
|
||||||
# The python 'chunk' module is pretty dumb; it doesn't understand
|
|
||||||
# the FORM chunk and the way it contains nested chunks.
|
|
||||||
# Therefore, we deliberately seek 12 bytes into the file so that
|
|
||||||
# we can start sucking out chunks. This also allows us to
|
|
||||||
# validate that the FORM type is "IFZS".
|
|
||||||
header = self._file.read(4)
|
|
||||||
if header != b"FORM":
|
|
||||||
raise QuetzalUnrecognizedFileFormat
|
|
||||||
bytestring = self._file.read(4)
|
|
||||||
self._len = bytestring[0] << 24
|
|
||||||
self._len += bytestring[1] << 16
|
|
||||||
self._len += bytestring[2] << 8
|
|
||||||
self._len += bytestring[3]
|
|
||||||
log("Total length of FORM data is %d" % self._len)
|
|
||||||
self._last_loaded_metadata["total length"] = self._len
|
|
||||||
|
|
||||||
type = self._file.read(4)
|
|
||||||
if type != b"IFZS":
|
|
||||||
raise QuetzalUnrecognizedFileFormat
|
|
||||||
|
|
||||||
try:
|
|
||||||
while 1:
|
|
||||||
c = chunk.Chunk(self._file)
|
|
||||||
chunkname = c.getname()
|
|
||||||
chunksize = c.getsize()
|
|
||||||
data = c.read(chunksize)
|
|
||||||
log("** Found chunk ID %s: length %d" % (chunkname, chunksize))
|
|
||||||
self._last_loaded_metadata[chunkname] = chunksize
|
|
||||||
|
|
||||||
if chunkname == b"IFhd":
|
|
||||||
self._parse_ifhd(data)
|
|
||||||
elif chunkname == b"CMem":
|
|
||||||
self._parse_cmem(data)
|
|
||||||
elif chunkname == b"UMem":
|
|
||||||
self._parse_umem(data)
|
|
||||||
elif chunkname == b"Stks":
|
|
||||||
self._parse_stks(data)
|
|
||||||
elif chunkname == b"IntD":
|
|
||||||
self._parse_intd(data)
|
|
||||||
elif chunkname == b"AUTH":
|
|
||||||
self._parse_auth(data)
|
|
||||||
elif chunkname == b"(c) ":
|
|
||||||
self._parse_copyright(data)
|
|
||||||
elif chunkname == b"ANNO":
|
|
||||||
self._parse_anno(data)
|
|
||||||
else:
|
|
||||||
# spec says to ignore and skip past unrecognized chunks
|
|
||||||
pass
|
|
||||||
|
|
||||||
except EOFError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self._file.close()
|
|
||||||
log("Finished parsing Quetzal file.")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class QuetzalWriter:
|
class QuetzalWriter:
|
||||||
"""A class to write the current state of a z-machine into a
|
"""A class to write the current state of a z-machine into a
|
||||||
Quetzal-format file."""
|
Quetzal-format file."""
|
||||||
|
|
||||||
def __init__(self, zmachine):
|
def __init__(self, zmachine):
|
||||||
log("Creating new instance of QuetzalWriter")
|
log("Creating new instance of QuetzalWriter")
|
||||||
self._zmachine = zmachine
|
self._zmachine = zmachine
|
||||||
|
|
||||||
def _generate_ifhd_chunk(self):
|
def _generate_ifhd_chunk(self):
|
||||||
"""Return a chunk of type IFhd, containing metadata about the
|
"""Return a chunk of type IFhd, containing metadata about the
|
||||||
zmachine and story being played."""
|
zmachine and story being played."""
|
||||||
|
|
||||||
### TODO: write this. payload must be *exactly* 13 bytes, even if
|
### TODO: write this. payload must be *exactly* 13 bytes, even if
|
||||||
### it means padding the program counter.
|
### it means padding the program counter.
|
||||||
|
|
||||||
### Some old infocom games don't have checksums stored in header.
|
### Some old infocom games don't have checksums stored in header.
|
||||||
### If not, generate it from the *original* story file memory
|
### If not, generate it from the *original* story file memory
|
||||||
### image and put it into this chunk. See ZMemory.generate_checksum().
|
### image and put it into this chunk. See ZMemory.generate_checksum().
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return "0"
|
return "0"
|
||||||
|
|
||||||
|
def _generate_cmem_chunk(self):
|
||||||
|
"""Return a compressed chunk of data representing the compressed
|
||||||
|
image of the zmachine's main memory."""
|
||||||
|
|
||||||
def _generate_cmem_chunk(self):
|
### TODO: debug this when ready
|
||||||
"""Return a compressed chunk of data representing the compressed
|
return "0"
|
||||||
image of the zmachine's main memory."""
|
|
||||||
|
|
||||||
### TODO: debug this when ready
|
# XOR the original game image with the current one
|
||||||
return "0"
|
diffarray = list(self._zmachine._pristine_mem)
|
||||||
|
for index in range(len(self._zmachine._pristine_mem._total_size)):
|
||||||
|
diffarray[index] = (
|
||||||
|
self._zmachine._pristine_mem[index] ^ self._zmachine._mem[index]
|
||||||
|
)
|
||||||
|
log(f"XOR array is {diffarray}")
|
||||||
|
|
||||||
# XOR the original game image with the current one
|
# Run-length encode the resulting list of 0's and 1's.
|
||||||
diffarray = list(self._zmachine._pristine_mem)
|
result = []
|
||||||
for index in range(len(self._zmachine._pristine_mem._total_size)):
|
zerocounter = 0
|
||||||
diffarray[index] = self._zmachine._pristine_mem[index] \
|
for index in range(len(diffarray)):
|
||||||
^ self._zmachine._mem[index]
|
if diffarray[index] == 0:
|
||||||
log("XOR array is %s" % diffarray)
|
zerocounter += 1
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if zerocounter > 0:
|
||||||
|
result.append(0)
|
||||||
|
result.append(zerocounter)
|
||||||
|
zerocounter = 0
|
||||||
|
result.append(diffarray[index])
|
||||||
|
return result
|
||||||
|
|
||||||
# Run-length encode the resulting list of 0's and 1's.
|
def _generate_stks_chunk(self):
|
||||||
result = []
|
"""Return a stacks chunk, describing the stack state of the
|
||||||
zerocounter = 0
|
zmachine at this moment."""
|
||||||
for index in range(len(diffarray)):
|
|
||||||
if diffarray[index] == 0:
|
|
||||||
zerocounter += 1
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
if zerocounter > 0:
|
|
||||||
result.append(0)
|
|
||||||
result.append(zerocounter)
|
|
||||||
zerocounter = 0
|
|
||||||
result.append(diffarray[index])
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
### TODO: write this
|
||||||
|
return "0"
|
||||||
|
|
||||||
def _generate_stks_chunk(self):
|
def _generate_anno_chunk(self):
|
||||||
"""Return a stacks chunk, describing the stack state of the
|
"""Return an annotation chunk, containing metadata about the ZVM
|
||||||
zmachine at this moment."""
|
interpreter which created the savefile."""
|
||||||
|
|
||||||
### TODO: write this
|
### TODO: write this
|
||||||
return "0"
|
return "0"
|
||||||
|
|
||||||
|
# --------- Public APIs -----------
|
||||||
|
|
||||||
def _generate_anno_chunk(self):
|
def write(self, savefile_path):
|
||||||
"""Return an annotation chunk, containing metadata about the ZVM
|
"""Write the current zmachine state to a new Quetzal-file at
|
||||||
interpreter which created the savefile."""
|
SAVEFILE_PATH."""
|
||||||
|
|
||||||
### TODO: write this
|
log(f"Attempting to write game-state to '{savefile_path}'")
|
||||||
return "0"
|
self._file = open(savefile_path, "w") # noqa: SIM115
|
||||||
|
|
||||||
|
ifhd_chunk = self._generate_ifhd_chunk()
|
||||||
|
cmem_chunk = self._generate_cmem_chunk()
|
||||||
|
stks_chunk = self._generate_stks_chunk()
|
||||||
|
anno_chunk = self._generate_anno_chunk()
|
||||||
|
|
||||||
#--------- Public APIs -----------
|
_total_chunk_size = (
|
||||||
|
len(ifhd_chunk) + len(cmem_chunk) + len(stks_chunk) + len(anno_chunk)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Write main FORM chunk to hold other chunks
|
||||||
|
self._file.write("FORM")
|
||||||
|
### TODO: self._file_write(total_chunk_size) -- spread it over 4 bytes
|
||||||
|
self._file.write("IFZS")
|
||||||
|
|
||||||
def write(self, savefile_path):
|
# Write nested chunks.
|
||||||
"""Write the current zmachine state to a new Quetzal-file at
|
for chunk_data in (ifhd_chunk, cmem_chunk, stks_chunk, anno_chunk):
|
||||||
SAVEFILE_PATH."""
|
self._file.write(chunk_data)
|
||||||
|
log("Wrote a chunk.")
|
||||||
log("Attempting to write game-state to '%s'" % savefile_path)
|
self._file.close()
|
||||||
self._file = open(savefile_path, 'w')
|
log("Done writing game-state to savefile.")
|
||||||
|
|
||||||
ifhd_chunk = self._generate_ifhd_chunk()
|
|
||||||
cmem_chunk = self._generate_cmem_chunk()
|
|
||||||
stks_chunk = self._generate_stks_chunk()
|
|
||||||
anno_chunk = self._generate_anno_chunk()
|
|
||||||
|
|
||||||
total_chunk_size = len(ifhd_chunk) + len(cmem_chunk) \
|
|
||||||
+ len(stks_chunk) + len(anno_chunk)
|
|
||||||
|
|
||||||
# Write main FORM chunk to hold other chunks
|
|
||||||
self._file.write("FORM")
|
|
||||||
### TODO: self._file_write(total_chunk_size) -- spread it over 4 bytes
|
|
||||||
self._file.write("IFZS")
|
|
||||||
|
|
||||||
# Write nested chunks.
|
|
||||||
for chunk in (ifhd_chunk, cmem_chunk, stks_chunk, anno_chunk):
|
|
||||||
self._file.write(chunk)
|
|
||||||
log("Wrote a chunk.")
|
|
||||||
self._file.close()
|
|
||||||
log("Done writing game-state to savefile.")
|
|
||||||
|
|
|
||||||
|
|
@ -22,246 +22,248 @@ from .zlogging import log
|
||||||
|
|
||||||
|
|
||||||
class TrivialAudio(zaudio.ZAudio):
|
class TrivialAudio(zaudio.ZAudio):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
zaudio.ZAudio.__init__(self)
|
zaudio.ZAudio.__init__(self)
|
||||||
self.features = {
|
self.features = {
|
||||||
"has_more_than_a_bleep": False,
|
"has_more_than_a_bleep": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def play_bleep(self, bleep_type):
|
||||||
|
if bleep_type == zaudio.BLEEP_HIGH:
|
||||||
|
sys.stdout.write("AUDIO: high-pitched bleep\n")
|
||||||
|
elif bleep_type == zaudio.BLEEP_LOW:
|
||||||
|
sys.stdout.write("AUDIO: low-pitched bleep\n")
|
||||||
|
else:
|
||||||
|
raise AssertionError(f"Invalid bleep_type: {str(bleep_type)}")
|
||||||
|
|
||||||
def play_bleep(self, bleep_type):
|
|
||||||
if bleep_type == zaudio.BLEEP_HIGH:
|
|
||||||
sys.stdout.write("AUDIO: high-pitched bleep\n")
|
|
||||||
elif bleep_type == zaudio.BLEEP_LOW:
|
|
||||||
sys.stdout.write("AUDIO: low-pitched bleep\n")
|
|
||||||
else:
|
|
||||||
raise AssertionError("Invalid bleep_type: %s" % str(bleep_type))
|
|
||||||
|
|
||||||
class TrivialScreen(zscreen.ZScreen):
|
class TrivialScreen(zscreen.ZScreen):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
zscreen.ZScreen.__init__(self)
|
zscreen.ZScreen.__init__(self)
|
||||||
self.__styleIsAllUppercase = False
|
self.__styleIsAllUppercase = False
|
||||||
|
|
||||||
# Current column of text being printed.
|
# Current column of text being printed.
|
||||||
self.__curr_column = 0
|
|
||||||
|
|
||||||
# Number of rows displayed since we last took input; needed to
|
|
||||||
# keep track of when we need to display the [MORE] prompt.
|
|
||||||
self.__rows_since_last_input = 0
|
|
||||||
|
|
||||||
def split_window(self, height):
|
|
||||||
log("TODO: split window here to height %d" % height)
|
|
||||||
|
|
||||||
def select_window(self, window_num):
|
|
||||||
log("TODO: select window %d here" % window_num)
|
|
||||||
|
|
||||||
def set_cursor_position(self, x, y):
|
|
||||||
log("TODO: set cursor position to (%d,%d) here" % (x,y))
|
|
||||||
|
|
||||||
def erase_window(self, window=zscreen.WINDOW_LOWER,
|
|
||||||
color=zscreen.COLOR_CURRENT):
|
|
||||||
for row in range(self._rows):
|
|
||||||
sys.stdout.write("\n")
|
|
||||||
self.__curr_column = 0
|
|
||||||
self.__rows_since_last_input = 0
|
|
||||||
|
|
||||||
def set_font(self, font_number):
|
|
||||||
if font_number == zscreen.FONT_NORMAL:
|
|
||||||
return font_number
|
|
||||||
else:
|
|
||||||
# We aren't going to support anything but the normal font.
|
|
||||||
return None
|
|
||||||
|
|
||||||
def set_text_style(self, style):
|
|
||||||
# We're pretty much limited to stdio here; even if we might be
|
|
||||||
# able to use terminal hackery under Unix, supporting styled text
|
|
||||||
# in a Windows console is problematic [1]. The closest thing we
|
|
||||||
# can do is have our "bold" style be all-caps, so we'll do that.
|
|
||||||
#
|
|
||||||
# [1] http://mail.python.org/pipermail/tutor/2004-February/028474.html
|
|
||||||
|
|
||||||
if style == zscreen.STYLE_BOLD:
|
|
||||||
self.__styleIsAllUppercase = True
|
|
||||||
else:
|
|
||||||
self.__styleIsAllUppercase = False
|
|
||||||
|
|
||||||
def __show_more_prompt(self):
|
|
||||||
"""Display a [MORE] prompt, wait for the user to press a key, and
|
|
||||||
then erase the [MORE] prompt, leaving the cursor at the same
|
|
||||||
position that it was at before the call was made."""
|
|
||||||
|
|
||||||
assert self.__curr_column == 0, \
|
|
||||||
"Precondition: current column must be zero."
|
|
||||||
|
|
||||||
MORE_STRING = "[MORE]"
|
|
||||||
sys.stdout.write(MORE_STRING)
|
|
||||||
_read_char()
|
|
||||||
# Erase the [MORE] prompt and reset the cursor position.
|
|
||||||
sys.stdout.write("\r%s\r" % (" " * len(MORE_STRING)))
|
|
||||||
self.__rows_since_last_input = 0
|
|
||||||
|
|
||||||
def on_input_occurred(self, newline_occurred=False):
|
|
||||||
"""Callback function that should be called whenever keyboard input
|
|
||||||
has occurred; this is so we can keep track of when we need to
|
|
||||||
display a [MORE] prompt."""
|
|
||||||
|
|
||||||
self.__rows_since_last_input = 0
|
|
||||||
if newline_occurred:
|
|
||||||
self.__curr_column = 0
|
|
||||||
|
|
||||||
def __unbuffered_write(self, string):
|
|
||||||
"""Write the given string, inserting newlines at the end of
|
|
||||||
columns as appropriate, and displaying [MORE] prompts when
|
|
||||||
appropriate. This function does not perform word-wrapping."""
|
|
||||||
|
|
||||||
for char in string:
|
|
||||||
newline_printed = False
|
|
||||||
sys.stdout.write(char)
|
|
||||||
|
|
||||||
if char == "\n":
|
|
||||||
newline_printed = True
|
|
||||||
else:
|
|
||||||
self.__curr_column += 1
|
|
||||||
|
|
||||||
if self.__curr_column == self._columns:
|
|
||||||
sys.stdout.write("\n")
|
|
||||||
newline_printed = True
|
|
||||||
|
|
||||||
if newline_printed:
|
|
||||||
self.__rows_since_last_input += 1
|
|
||||||
self.__curr_column = 0
|
self.__curr_column = 0
|
||||||
if (self.__rows_since_last_input == self._rows and
|
|
||||||
self._rows != zscreen.INFINITE_ROWS):
|
|
||||||
self.__show_more_prompt()
|
|
||||||
|
|
||||||
def write(self, string):
|
# Number of rows displayed since we last took input; needed to
|
||||||
if self.__styleIsAllUppercase:
|
# keep track of when we need to display the [MORE] prompt.
|
||||||
# Apply our fake "bold" transformation.
|
self.__rows_since_last_input = 0
|
||||||
string = string.upper()
|
|
||||||
|
|
||||||
if self.buffer_mode:
|
def split_window(self, height):
|
||||||
# This is a hack to get words to wrap properly, based on our
|
log(f"TODO: split window here to height {height}")
|
||||||
# current cursor position.
|
|
||||||
|
|
||||||
# First, add whitespace padding up to the column of text that
|
def select_window(self, window):
|
||||||
# we're at.
|
log(f"TODO: select window {window} here")
|
||||||
string = (" " * self.__curr_column) + string
|
|
||||||
|
|
||||||
# Next, word wrap our current string.
|
def set_cursor_position(self, x, y):
|
||||||
string = _word_wrap(string, self._columns-1)
|
log(f"TODO: set cursor position to ({x},{y}) here")
|
||||||
|
|
||||||
# Now remove the whitespace padding.
|
def erase_window(self, window=zscreen.WINDOW_LOWER, color=zscreen.COLOR_CURRENT):
|
||||||
string = string[self.__curr_column:]
|
for _row in range(self._rows):
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
self.__curr_column = 0
|
||||||
|
self.__rows_since_last_input = 0
|
||||||
|
|
||||||
|
def set_font(self, font_number):
|
||||||
|
if font_number == zscreen.FONT_NORMAL:
|
||||||
|
return font_number
|
||||||
|
else:
|
||||||
|
# We aren't going to support anything but the normal font.
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_text_style(self, style):
|
||||||
|
# We're pretty much limited to stdio here; even if we might be
|
||||||
|
# able to use terminal hackery under Unix, supporting styled text
|
||||||
|
# in a Windows console is problematic [1]. The closest thing we
|
||||||
|
# can do is have our "bold" style be all-caps, so we'll do that.
|
||||||
|
#
|
||||||
|
# [1] http://mail.python.org/pipermail/tutor/2004-February/028474.html
|
||||||
|
|
||||||
|
if style == zscreen.STYLE_BOLD:
|
||||||
|
self.__styleIsAllUppercase = True
|
||||||
|
else:
|
||||||
|
self.__styleIsAllUppercase = False
|
||||||
|
|
||||||
|
def __show_more_prompt(self):
|
||||||
|
"""Display a [MORE] prompt, wait for the user to press a key, and
|
||||||
|
then erase the [MORE] prompt, leaving the cursor at the same
|
||||||
|
position that it was at before the call was made."""
|
||||||
|
|
||||||
|
assert self.__curr_column == 0, "Precondition: current column must be zero."
|
||||||
|
|
||||||
|
MORE_STRING = "[MORE]"
|
||||||
|
sys.stdout.write(MORE_STRING)
|
||||||
|
_read_char()
|
||||||
|
# Erase the [MORE] prompt and reset the cursor position.
|
||||||
|
sys.stdout.write(f"\r{' ' * len(MORE_STRING)}\r")
|
||||||
|
self.__rows_since_last_input = 0
|
||||||
|
|
||||||
|
def on_input_occurred(self, newline_occurred=False):
|
||||||
|
"""Callback function that should be called whenever keyboard input
|
||||||
|
has occurred; this is so we can keep track of when we need to
|
||||||
|
display a [MORE] prompt."""
|
||||||
|
|
||||||
|
self.__rows_since_last_input = 0
|
||||||
|
if newline_occurred:
|
||||||
|
self.__curr_column = 0
|
||||||
|
|
||||||
|
def __unbuffered_write(self, string):
|
||||||
|
"""Write the given string, inserting newlines at the end of
|
||||||
|
columns as appropriate, and displaying [MORE] prompts when
|
||||||
|
appropriate. This function does not perform word-wrapping."""
|
||||||
|
|
||||||
|
for char in string:
|
||||||
|
newline_printed = False
|
||||||
|
sys.stdout.write(char)
|
||||||
|
|
||||||
|
if char == "\n":
|
||||||
|
newline_printed = True
|
||||||
|
else:
|
||||||
|
self.__curr_column += 1
|
||||||
|
|
||||||
|
if self.__curr_column == self._columns:
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
newline_printed = True
|
||||||
|
|
||||||
|
if newline_printed:
|
||||||
|
self.__rows_since_last_input += 1
|
||||||
|
self.__curr_column = 0
|
||||||
|
if (
|
||||||
|
self.__rows_since_last_input == self._rows
|
||||||
|
and self._rows != zscreen.INFINITE_ROWS
|
||||||
|
):
|
||||||
|
self.__show_more_prompt()
|
||||||
|
|
||||||
|
def write(self, string):
|
||||||
|
if self.__styleIsAllUppercase:
|
||||||
|
# Apply our fake "bold" transformation.
|
||||||
|
string = string.upper()
|
||||||
|
|
||||||
|
if self.buffer_mode:
|
||||||
|
# This is a hack to get words to wrap properly, based on our
|
||||||
|
# current cursor position.
|
||||||
|
|
||||||
|
# First, add whitespace padding up to the column of text that
|
||||||
|
# we're at.
|
||||||
|
string = (" " * self.__curr_column) + string
|
||||||
|
|
||||||
|
# Next, word wrap our current string.
|
||||||
|
string = _word_wrap(string, self._columns - 1)
|
||||||
|
|
||||||
|
# Now remove the whitespace padding.
|
||||||
|
string = string[self.__curr_column :]
|
||||||
|
|
||||||
|
self.__unbuffered_write(string)
|
||||||
|
|
||||||
self.__unbuffered_write(string)
|
|
||||||
|
|
||||||
class TrivialKeyboardInputStream(zstream.ZInputStream):
|
class TrivialKeyboardInputStream(zstream.ZInputStream):
|
||||||
def __init__(self, screen):
|
def __init__(self, screen):
|
||||||
zstream.ZInputStream.__init__(self)
|
zstream.ZInputStream.__init__(self)
|
||||||
self.__screen = screen
|
self.__screen = screen
|
||||||
self.features = {
|
self.features = {
|
||||||
"has_timed_input" : False,
|
"has_timed_input": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
def read_line(self, original_text=None, max_length=0,
|
def read_line(
|
||||||
terminating_characters=None,
|
self,
|
||||||
timed_input_routine=None, timed_input_interval=0):
|
original_text=None,
|
||||||
result = _read_line(original_text, terminating_characters)
|
max_length=0,
|
||||||
if max_length > 0:
|
terminating_characters=None,
|
||||||
result = result[:max_length]
|
timed_input_routine=None,
|
||||||
|
timed_input_interval=0,
|
||||||
|
):
|
||||||
|
result = _read_line(original_text, terminating_characters)
|
||||||
|
if max_length > 0:
|
||||||
|
result = result[:max_length]
|
||||||
|
|
||||||
# TODO: The value of 'newline_occurred' here is not accurate,
|
# TODO: The value of 'newline_occurred' here is not accurate,
|
||||||
# because terminating_characters may include characters other than
|
# because terminating_characters may include characters other than
|
||||||
# carriage return.
|
# carriage return.
|
||||||
self.__screen.on_input_occurred(newline_occurred=True)
|
self.__screen.on_input_occurred(newline_occurred=True)
|
||||||
|
|
||||||
return str(result)
|
return str(result)
|
||||||
|
|
||||||
|
def read_char(self, timed_input_routine=None, timed_input_interval=0):
|
||||||
|
result = _read_char()
|
||||||
|
self.__screen.on_input_occurred()
|
||||||
|
return ord(result)
|
||||||
|
|
||||||
def read_char(self, timed_input_routine=None,
|
|
||||||
timed_input_interval=0):
|
|
||||||
result = _read_char()
|
|
||||||
self.__screen.on_input_occurred()
|
|
||||||
return ord(result)
|
|
||||||
|
|
||||||
class TrivialFilesystem(zfilesystem.ZFilesystem):
|
class TrivialFilesystem(zfilesystem.ZFilesystem):
|
||||||
def __report_io_error(self, exception):
|
def __report_io_error(self, exception):
|
||||||
sys.stdout.write("FILESYSTEM: An error occurred: %s\n" % exception)
|
sys.stdout.write(f"FILESYSTEM: An error occurred: {exception}\n")
|
||||||
|
|
||||||
def save_game(self, data, suggested_filename=None):
|
def save_game(self, data, suggested_filename=None):
|
||||||
success = False
|
success = False
|
||||||
|
|
||||||
sys.stdout.write("Enter a name for the saved game " \
|
sys.stdout.write("Enter a name for the saved game (hit enter to cancel): ")
|
||||||
"(hit enter to cancel): ")
|
filename = _read_line(suggested_filename)
|
||||||
filename = _read_line(suggested_filename)
|
if filename:
|
||||||
if filename:
|
try:
|
||||||
try:
|
with open(filename, "wb") as file_obj:
|
||||||
file_obj = open(filename, "wb")
|
file_obj.write(data)
|
||||||
file_obj.write(data)
|
success = True
|
||||||
file_obj.close()
|
except OSError as e:
|
||||||
success = True
|
self.__report_io_error(e)
|
||||||
except OSError as e:
|
|
||||||
self.__report_io_error(e)
|
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
||||||
def restore_game(self):
|
def restore_game(self):
|
||||||
data = None
|
data = None
|
||||||
|
|
||||||
sys.stdout.write("Enter the name of the saved game to restore " \
|
sys.stdout.write(
|
||||||
"(hit enter to cancel): ")
|
"Enter the name of the saved game to restore (hit enter to cancel): "
|
||||||
filename = _read_line()
|
)
|
||||||
if filename:
|
filename = _read_line()
|
||||||
try:
|
if filename:
|
||||||
file_obj = open(filename, "rb")
|
try:
|
||||||
data = file_obj.read()
|
with open(filename, "rb") as file_obj:
|
||||||
file_obj.close()
|
data = file_obj.read()
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self.__report_io_error(e)
|
self.__report_io_error(e)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def open_transcript_file_for_writing(self):
|
def open_transcript_file_for_writing(self):
|
||||||
file_obj = None
|
file_obj = None
|
||||||
|
|
||||||
sys.stdout.write("Enter a name for the transcript file " \
|
sys.stdout.write("Enter a name for the transcript file (hit enter to cancel): ")
|
||||||
"(hit enter to cancel): ")
|
filename = _read_line()
|
||||||
filename = _read_line()
|
if filename:
|
||||||
if filename:
|
try:
|
||||||
try:
|
file_obj = open(filename, "w") # noqa: SIM115
|
||||||
file_obj = open(filename, "w")
|
except OSError as e:
|
||||||
except OSError as e:
|
self.__report_io_error(e)
|
||||||
self.__report_io_error(e)
|
|
||||||
|
|
||||||
return file_obj
|
return file_obj
|
||||||
|
|
||||||
def open_transcript_file_for_reading(self):
|
def open_transcript_file_for_reading(self):
|
||||||
file_obj = None
|
file_obj = None
|
||||||
|
|
||||||
sys.stdout.write("Enter the name of the transcript file to read " \
|
sys.stdout.write(
|
||||||
"(hit enter to cancel): ")
|
"Enter the name of the transcript file to read (hit enter to cancel): "
|
||||||
filename = _read_line()
|
)
|
||||||
if filename:
|
filename = _read_line()
|
||||||
try:
|
if filename:
|
||||||
file_obj = open(filename)
|
try:
|
||||||
except OSError as e:
|
file_obj = open(filename) # noqa: SIM115
|
||||||
self.__report_io_error(e)
|
except OSError as e:
|
||||||
|
self.__report_io_error(e)
|
||||||
|
|
||||||
|
return file_obj
|
||||||
|
|
||||||
return file_obj
|
|
||||||
|
|
||||||
def create_zui():
|
def create_zui():
|
||||||
"""Creates and returns a ZUI instance representing a trivial user
|
"""Creates and returns a ZUI instance representing a trivial user
|
||||||
interface."""
|
interface."""
|
||||||
|
|
||||||
audio = TrivialAudio()
|
audio = TrivialAudio()
|
||||||
screen = TrivialScreen()
|
screen = TrivialScreen()
|
||||||
keyboard_input = TrivialKeyboardInputStream(screen)
|
keyboard_input = TrivialKeyboardInputStream(screen)
|
||||||
filesystem = TrivialFilesystem()
|
filesystem = TrivialFilesystem()
|
||||||
|
|
||||||
|
return zui.ZUI(audio, screen, keyboard_input, filesystem)
|
||||||
|
|
||||||
return zui.ZUI(
|
|
||||||
audio,
|
|
||||||
screen,
|
|
||||||
keyboard_input,
|
|
||||||
filesystem
|
|
||||||
)
|
|
||||||
|
|
||||||
# Keyboard input functions
|
# Keyboard input functions
|
||||||
|
|
||||||
|
|
@ -269,110 +271,121 @@ _INTERRUPT_CHAR = chr(3)
|
||||||
_BACKSPACE_CHAR = chr(8)
|
_BACKSPACE_CHAR = chr(8)
|
||||||
_DELETE_CHAR = chr(127)
|
_DELETE_CHAR = chr(127)
|
||||||
|
|
||||||
|
|
||||||
def _win32_read_char():
|
def _win32_read_char():
|
||||||
"""Win32-specific function that reads a character of input from the
|
"""Win32-specific function that reads a character of input from the
|
||||||
keyboard and returns it without printing it to the screen."""
|
keyboard and returns it without printing it to the screen."""
|
||||||
|
|
||||||
import msvcrt
|
import msvcrt
|
||||||
|
|
||||||
|
return str(msvcrt.getch()) # type: ignore[possibly-missing-attribute]
|
||||||
|
|
||||||
return str(msvcrt.getch())
|
|
||||||
|
|
||||||
def _unix_read_char():
|
def _unix_read_char():
|
||||||
"""Unix-specific function that reads a character of input from the
|
"""Unix-specific function that reads a character of input from the
|
||||||
keyboard and returns it without printing it to the screen."""
|
keyboard and returns it without printing it to the screen."""
|
||||||
|
|
||||||
# This code was excised from:
|
# This code was excised from:
|
||||||
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/134892
|
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/134892
|
||||||
|
|
||||||
import termios
|
import termios
|
||||||
import tty
|
import tty
|
||||||
|
|
||||||
|
fd = sys.stdin.fileno()
|
||||||
|
old_settings = termios.tcgetattr(fd)
|
||||||
|
try:
|
||||||
|
tty.setraw(sys.stdin.fileno())
|
||||||
|
ch = sys.stdin.read(1)
|
||||||
|
finally:
|
||||||
|
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
||||||
|
return str(ch)
|
||||||
|
|
||||||
fd = sys.stdin.fileno()
|
|
||||||
old_settings = termios.tcgetattr(fd)
|
|
||||||
try:
|
|
||||||
tty.setraw(sys.stdin.fileno())
|
|
||||||
ch = sys.stdin.read(1)
|
|
||||||
finally:
|
|
||||||
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
||||||
return str(ch)
|
|
||||||
|
|
||||||
def _read_char():
|
def _read_char():
|
||||||
"""Reads a character of input from the keyboard and returns it
|
"""Reads a character of input from the keyboard and returns it
|
||||||
without printing it to the screen."""
|
without printing it to the screen."""
|
||||||
|
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
_platform_read_char = _win32_read_char
|
_platform_read_char = _win32_read_char
|
||||||
else:
|
else:
|
||||||
# We're not running on Windows, so assume we're running on Unix.
|
# We're not running on Windows, so assume we're running on Unix.
|
||||||
_platform_read_char = _unix_read_char
|
_platform_read_char = _unix_read_char
|
||||||
|
|
||||||
|
char = _platform_read_char()
|
||||||
|
if char == _INTERRUPT_CHAR:
|
||||||
|
raise KeyboardInterrupt()
|
||||||
|
else:
|
||||||
|
return char
|
||||||
|
|
||||||
char = _platform_read_char()
|
|
||||||
if char == _INTERRUPT_CHAR:
|
|
||||||
raise KeyboardInterrupt()
|
|
||||||
else:
|
|
||||||
return char
|
|
||||||
|
|
||||||
def _read_line(original_text=None, terminating_characters=None):
|
def _read_line(original_text=None, terminating_characters=None):
|
||||||
"""Reads a line of input with the given unicode string of original
|
"""Reads a line of input with the given unicode string of original
|
||||||
text, which is editable, and the given unicode string of terminating
|
text, which is editable, and the given unicode string of terminating
|
||||||
characters (used to terminate text input). By default,
|
characters (used to terminate text input). By default,
|
||||||
terminating_characters is a string containing the carriage return
|
terminating_characters is a string containing the carriage return
|
||||||
character ('\r')."""
|
character ('\r')."""
|
||||||
|
|
||||||
if original_text == None:
|
|
||||||
original_text = ""
|
|
||||||
if not terminating_characters:
|
|
||||||
terminating_characters = "\r"
|
|
||||||
|
|
||||||
assert isinstance(original_text, str)
|
if original_text is None:
|
||||||
assert isinstance(terminating_characters, str)
|
original_text = ""
|
||||||
|
if not terminating_characters:
|
||||||
|
terminating_characters = "\r"
|
||||||
|
|
||||||
chars_entered = len(original_text)
|
assert isinstance(original_text, str)
|
||||||
sys.stdout.write(original_text)
|
assert isinstance(terminating_characters, str)
|
||||||
string = original_text
|
|
||||||
finished = False
|
|
||||||
while not finished:
|
|
||||||
char = _read_char()
|
|
||||||
|
|
||||||
if char in (_BACKSPACE_CHAR, _DELETE_CHAR):
|
chars_entered = len(original_text)
|
||||||
if chars_entered > 0:
|
sys.stdout.write(original_text)
|
||||||
chars_entered -= 1
|
string = original_text
|
||||||
string = string[:-1]
|
finished = False
|
||||||
else:
|
while not finished:
|
||||||
continue
|
char = _read_char()
|
||||||
elif char in terminating_characters:
|
|
||||||
finished = True
|
|
||||||
else:
|
|
||||||
string += char
|
|
||||||
chars_entered += 1
|
|
||||||
|
|
||||||
if char == "\r":
|
if char in (_BACKSPACE_CHAR, _DELETE_CHAR):
|
||||||
char_to_print = "\n"
|
if chars_entered > 0:
|
||||||
elif char == _BACKSPACE_CHAR:
|
chars_entered -= 1
|
||||||
char_to_print = "%s %s" % (_BACKSPACE_CHAR, _BACKSPACE_CHAR)
|
string = string[:-1]
|
||||||
else:
|
else:
|
||||||
char_to_print = char
|
continue
|
||||||
|
elif char in terminating_characters:
|
||||||
|
finished = True
|
||||||
|
else:
|
||||||
|
string += char
|
||||||
|
chars_entered += 1
|
||||||
|
|
||||||
|
if char == "\r":
|
||||||
|
char_to_print = "\n"
|
||||||
|
elif char == _BACKSPACE_CHAR:
|
||||||
|
char_to_print = f"{_BACKSPACE_CHAR} {_BACKSPACE_CHAR}"
|
||||||
|
else:
|
||||||
|
char_to_print = char
|
||||||
|
|
||||||
|
sys.stdout.write(char_to_print)
|
||||||
|
return string
|
||||||
|
|
||||||
sys.stdout.write(char_to_print)
|
|
||||||
return string
|
|
||||||
|
|
||||||
# Word wrapping helper function
|
# Word wrapping helper function
|
||||||
|
|
||||||
|
|
||||||
def _word_wrap(text, width):
|
def _word_wrap(text, width):
|
||||||
"""
|
"""
|
||||||
A word-wrap function that preserves existing line breaks
|
A word-wrap function that preserves existing line breaks
|
||||||
and most spaces in the text. Expects that existing line
|
and most spaces in the text. Expects that existing line
|
||||||
breaks are posix newlines (\n).
|
breaks are posix newlines (\n).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# This code was taken from:
|
# This code was taken from:
|
||||||
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061
|
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061
|
||||||
|
|
||||||
return reduce(lambda line, word, width=width: '%s%s%s' %
|
return reduce(
|
||||||
(line,
|
lambda line, word, width=width: "{}{}{}".format(
|
||||||
' \n'[(len(line)-line.rfind('\n')-1
|
line,
|
||||||
+ len(word.split('\n',1)[0]
|
" \n"[
|
||||||
) >= width)],
|
(
|
||||||
word),
|
len(line) - line.rfind("\n") - 1 + len(word.split("\n", 1)[0])
|
||||||
text.split(' ')
|
>= width
|
||||||
)
|
)
|
||||||
|
],
|
||||||
|
word,
|
||||||
|
),
|
||||||
|
text.split(" "),
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,55 +22,55 @@ EFFECT_START = 2
|
||||||
EFFECT_STOP = 3
|
EFFECT_STOP = 3
|
||||||
EFFECT_FINISH = 4
|
EFFECT_FINISH = 4
|
||||||
|
|
||||||
|
|
||||||
class ZAudio:
|
class ZAudio:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Constructor of the audio system."""
|
"""Constructor of the audio system."""
|
||||||
|
|
||||||
# Subclasses must define real values for all the features they
|
# Subclasses must define real values for all the features they
|
||||||
# support (or don't support).
|
# support (or don't support).
|
||||||
|
|
||||||
self.features = {
|
self.features = {
|
||||||
"has_more_than_a_bleep": False,
|
"has_more_than_a_bleep": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
def play_bleep(self, bleep_type):
|
def play_bleep(self, bleep_type):
|
||||||
"""Plays a bleep sound of the given type:
|
"""Plays a bleep sound of the given type:
|
||||||
|
|
||||||
BLEEP_HIGH - a high-pitched bleep
|
BLEEP_HIGH - a high-pitched bleep
|
||||||
BLEEP_LOW - a low-pitched bleep
|
BLEEP_LOW - a low-pitched bleep
|
||||||
"""
|
"""
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def play_sound_effect(self, id, effect, volume, repeats,
|
def play_sound_effect(self, id, effect, volume, repeats, routine=None):
|
||||||
routine=None):
|
"""The given effect happens to the given sound number. The id
|
||||||
"""The given effect happens to the given sound number. The id
|
must be 3 or above is supplied by the ZAudio object for the
|
||||||
must be 3 or above is supplied by the ZAudio object for the
|
particular game in question.
|
||||||
particular game in question.
|
|
||||||
|
|
||||||
The effect can be:
|
The effect can be:
|
||||||
|
|
||||||
EFFECT_PREPARE - prepare a sound effect for playing
|
EFFECT_PREPARE - prepare a sound effect for playing
|
||||||
EFFECT_START - start a sound effect
|
EFFECT_START - start a sound effect
|
||||||
EFFECT_STOP - stop a sound effect
|
EFFECT_STOP - stop a sound effect
|
||||||
EFFECT_FINISH - finish a sound effect
|
EFFECT_FINISH - finish a sound effect
|
||||||
|
|
||||||
The volume is an integer from 1 to 8 (8 being loudest of
|
The volume is an integer from 1 to 8 (8 being loudest of
|
||||||
these). The volume level -1 means 'loudest possible'.
|
these). The volume level -1 means 'loudest possible'.
|
||||||
|
|
||||||
The repeats specify how many times for the sound to repeatedly
|
The repeats specify how many times for the sound to repeatedly
|
||||||
play itself, if it is provided.
|
play itself, if it is provided.
|
||||||
|
|
||||||
The routine, if supplied, is a Python function that will be called
|
The routine, if supplied, is a Python function that will be called
|
||||||
once the sound has finished playing. Note that this routine may
|
once the sound has finished playing. Note that this routine may
|
||||||
be called from any thread. The routine should have the following
|
be called from any thread. The routine should have the following
|
||||||
form:
|
form:
|
||||||
|
|
||||||
def on_sound_finished(id)
|
def on_sound_finished(id)
|
||||||
|
|
||||||
where 'id' is the id of the sound that finished playing.
|
where 'id' is the id of the sound that finished playing.
|
||||||
|
|
||||||
This method should only be implemented if the
|
This method should only be implemented if the
|
||||||
has_more_than_a_bleep feature is enabled."""
|
has_more_than_a_bleep feature is enabled."""
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
|
||||||
|
|
@ -10,56 +10,54 @@
|
||||||
# root directory of this distribution.
|
# root directory of this distribution.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class ZFilesystem:
|
class ZFilesystem:
|
||||||
"""Encapsulates the interactions that the end-user has with the
|
"""Encapsulates the interactions that the end-user has with the
|
||||||
filesystem."""
|
filesystem."""
|
||||||
|
|
||||||
def save_game(self, data, suggested_filename=None):
|
def save_game(self, data, suggested_filename=None):
|
||||||
"""Prompt for a filename (possibly using suggested_filename), and
|
"""Prompt for a filename (possibly using suggested_filename), and
|
||||||
attempt to write DATA as a saved-game file. Return True on
|
attempt to write DATA as a saved-game file. Return True on
|
||||||
success, False on failure.
|
success, False on failure.
|
||||||
|
|
||||||
Note that file-handling errors such as 'disc corrupt' and 'disc
|
Note that file-handling errors such as 'disc corrupt' and 'disc
|
||||||
full' should be reported directly to the player by the method in
|
full' should be reported directly to the player by the method in
|
||||||
question method, and they should also cause this function to
|
question method, and they should also cause this function to
|
||||||
return False. If the user clicks 'cancel' or its equivalent,
|
return False. If the user clicks 'cancel' or its equivalent,
|
||||||
this function should return False."""
|
this function should return False."""
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def restore_game(self):
|
||||||
|
"""Prompt for a filename, and return file's contents. (Presumably
|
||||||
|
the interpreter will attempt to use those contents to restore a
|
||||||
|
saved game.) Returns None on failure.
|
||||||
|
|
||||||
def restore_game(self):
|
Note that file-handling errors such as 'disc corrupt' and 'disc
|
||||||
"""Prompt for a filename, and return file's contents. (Presumably
|
full' should be reported directly to the player by the method in
|
||||||
the interpreter will attempt to use those contents to restore a
|
question method, and they should also cause this function to
|
||||||
saved game.) Returns None on failure.
|
return None. The error 'file not found' should cause this function
|
||||||
|
to return None. If the user clicks 'cancel' or its equivalent,
|
||||||
|
this function should return None."""
|
||||||
|
|
||||||
Note that file-handling errors such as 'disc corrupt' and 'disc
|
raise NotImplementedError()
|
||||||
full' should be reported directly to the player by the method in
|
|
||||||
question method, and they should also cause this function to
|
|
||||||
return None. The error 'file not found' should cause this function
|
|
||||||
to return None. If the user clicks 'cancel' or its equivalent,
|
|
||||||
this function should return None."""
|
|
||||||
|
|
||||||
raise NotImplementedError()
|
def open_transcript_file_for_writing(self):
|
||||||
|
"""Prompt for a filename in which to save either a full game
|
||||||
|
transcript or just a list of the user's commands. Return standard
|
||||||
|
python file object that can be written to.
|
||||||
|
|
||||||
|
If an error occurs, or if the user clicks 'cancel' or its
|
||||||
|
equivalent, return None."""
|
||||||
|
|
||||||
def open_transcript_file_for_writing(self):
|
raise NotImplementedError()
|
||||||
"""Prompt for a filename in which to save either a full game
|
|
||||||
transcript or just a list of the user's commands. Return standard
|
|
||||||
python file object that can be written to.
|
|
||||||
|
|
||||||
If an error occurs, or if the user clicks 'cancel' or its
|
def open_transcript_file_for_reading(self):
|
||||||
equivalent, return None."""
|
"""Prompt for a filename contain user commands, which can be used
|
||||||
|
to drive the interpreter. Return standard python file object that
|
||||||
|
can be read from.
|
||||||
|
|
||||||
raise NotImplementedError()
|
If an error occurs, or if the user clicks 'cancel' or its
|
||||||
|
equivalent, return None."""
|
||||||
|
|
||||||
|
raise NotImplementedError()
|
||||||
def open_transcript_file_for_reading(self):
|
|
||||||
"""Prompt for a filename contain user commands, which can be used
|
|
||||||
to drive the interpreter. Return standard python file object that
|
|
||||||
can be read from.
|
|
||||||
|
|
||||||
If an error occurs, or if the user clicks 'cancel' or its
|
|
||||||
equivalent, return None."""
|
|
||||||
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ from .zstring import ZsciiTranslator, ZStringFactory
|
||||||
|
|
||||||
|
|
||||||
class ZLexerError(Exception):
|
class ZLexerError(Exception):
|
||||||
"General exception for ZLexer class"
|
"General exception for ZLexer class"
|
||||||
|
|
||||||
|
|
||||||
# Note that the specification describes tokenisation as a process
|
# Note that the specification describes tokenisation as a process
|
||||||
# whereby the user's input is divided into words, each word converted
|
# whereby the user's input is divided into words, each word converted
|
||||||
|
|
@ -26,117 +27,106 @@ class ZLexerError(Exception):
|
||||||
# Note that the main API here (tokenise_input()) can work with any
|
# Note that the main API here (tokenise_input()) can work with any
|
||||||
# dictionary, not just the standard one.
|
# dictionary, not just the standard one.
|
||||||
|
|
||||||
|
|
||||||
class ZLexer:
|
class ZLexer:
|
||||||
|
def __init__(self, mem):
|
||||||
|
self._memory = mem
|
||||||
|
self._stringfactory = ZStringFactory(self._memory)
|
||||||
|
self._zsciitranslator = ZsciiTranslator(self._memory)
|
||||||
|
|
||||||
def __init__(self, mem):
|
# Load and parse game's 'standard' dictionary from static memory.
|
||||||
|
dict_addr = self._memory.read_word(0x08)
|
||||||
|
self._num_entries, self._entry_length, self._separators, entries_addr = (
|
||||||
|
self._parse_dict_header(dict_addr)
|
||||||
|
)
|
||||||
|
self._dict = self.get_dictionary(dict_addr)
|
||||||
|
|
||||||
self._memory = mem
|
def _parse_dict_header(self, address):
|
||||||
self._stringfactory = ZStringFactory(self._memory)
|
"""Parse the header of the dictionary at ADDRESS. Return the
|
||||||
self._zsciitranslator = ZsciiTranslator(self._memory)
|
number of entries, the length of each entry, a list of zscii
|
||||||
|
word separators, and an address of the beginning the entries."""
|
||||||
|
|
||||||
# Load and parse game's 'standard' dictionary from static memory.
|
addr = address
|
||||||
dict_addr = self._memory.read_word(0x08)
|
num_separators = self._memory[addr]
|
||||||
self._num_entries, self._entry_length, self._separators, entries_addr = \
|
separators = self._memory[(addr + 1) : (addr + num_separators)]
|
||||||
self._parse_dict_header(dict_addr)
|
addr += 1 + num_separators
|
||||||
self._dict = self.get_dictionary(dict_addr)
|
entry_length = self._memory[addr]
|
||||||
|
addr += 1
|
||||||
|
num_entries = self._memory.read_word(addr)
|
||||||
|
addr += 2
|
||||||
|
|
||||||
|
return num_entries, entry_length, separators, addr
|
||||||
|
|
||||||
def _parse_dict_header(self, address):
|
def _tokenise_string(self, string, separators):
|
||||||
"""Parse the header of the dictionary at ADDRESS. Return the
|
"""Split unicode STRING into a list of words, and return the list.
|
||||||
number of entries, the length of each entry, a list of zscii
|
Whitespace always counts as a word separator, but so do any
|
||||||
word separators, and an address of the beginning the entries."""
|
unicode characters provided in the list of SEPARATORS. Note,
|
||||||
|
however, that instances of these separators caunt as words
|
||||||
|
themselves."""
|
||||||
|
|
||||||
addr = address
|
# re.findall(r'[,.;]|\w+', 'abc, def')
|
||||||
num_separators = self._memory[addr]
|
sep_string = ""
|
||||||
separators = self._memory[(addr + 1):(addr + num_separators)]
|
for sep in separators:
|
||||||
addr += (1 + num_separators)
|
sep_string += sep
|
||||||
entry_length = self._memory[addr]
|
regex = r"\w+" if sep_string == "" else rf"[{sep_string}]|\w+"
|
||||||
addr += 1
|
|
||||||
num_entries = self._memory.read_word(addr)
|
|
||||||
addr += 2
|
|
||||||
|
|
||||||
return num_entries, entry_length, separators, addr
|
return re.findall(regex, string)
|
||||||
|
|
||||||
|
# --------- Public APIs -----------
|
||||||
|
|
||||||
def _tokenise_string(self, string, separators):
|
def get_dictionary(self, address):
|
||||||
"""Split unicode STRING into a list of words, and return the list.
|
"""Load a z-machine-format dictionary at ADDRESS -- which maps
|
||||||
Whitespace always counts as a word separator, but so do any
|
zstrings to bytestrings -- into a python dictionary which maps
|
||||||
unicode characters provided in the list of SEPARATORS. Note,
|
unicode strings to the address of the word in the original
|
||||||
however, that instances of these separators caunt as words
|
dictionary. Return the new dictionary."""
|
||||||
themselves."""
|
|
||||||
|
|
||||||
# re.findall(r'[,.;]|\w+', 'abc, def')
|
dict = {}
|
||||||
sep_string = ""
|
|
||||||
for sep in separators:
|
|
||||||
sep_string += sep
|
|
||||||
if sep_string == "":
|
|
||||||
regex = r"\w+"
|
|
||||||
else:
|
|
||||||
regex = r"[%s]|\w+" % sep_string
|
|
||||||
|
|
||||||
return re.findall(regex, string)
|
num_entries, entry_length, separators, addr = self._parse_dict_header(address)
|
||||||
|
|
||||||
|
for _i in range(0, num_entries):
|
||||||
|
text_key = self._stringfactory.get(addr)
|
||||||
|
dict[text_key] = addr
|
||||||
|
addr += entry_length
|
||||||
|
|
||||||
#--------- Public APIs -----------
|
return dict
|
||||||
|
|
||||||
|
def parse_input(self, string, dict_addr=None):
|
||||||
|
"""Given a unicode string, parse it into words based on a dictionary.
|
||||||
|
|
||||||
def get_dictionary(self, address):
|
if DICT_ADDR is provided, use the custom dictionary at that
|
||||||
"""Load a z-machine-format dictionary at ADDRESS -- which maps
|
address to do the analysis, otherwise default to using the game's
|
||||||
zstrings to bytestrings -- into a python dictionary which maps
|
'standard' dictionary.
|
||||||
unicode strings to the address of the word in the original
|
|
||||||
dictionary. Return the new dictionary."""
|
|
||||||
|
|
||||||
dict = {}
|
The dictionary plays two roles: first, it specifies separator
|
||||||
|
characters beyond the usual space character. Second, we need to
|
||||||
|
look up each word in the dictionary and return the address.
|
||||||
|
|
||||||
num_entries, entry_length, separators, addr = \
|
Return a list of lists, each list being of the form
|
||||||
self._parse_dict_header(address)
|
|
||||||
|
|
||||||
for i in range(0, num_entries):
|
[word, byte_address_of_word_in_dictionary (or 0 if not in dictionary)]
|
||||||
text_key = self._stringfactory.get(addr)
|
"""
|
||||||
dict[text_key] = addr
|
|
||||||
addr += entry_length
|
|
||||||
|
|
||||||
return dict
|
if dict_addr is None:
|
||||||
|
zseparators = self._separators
|
||||||
|
dict = self._dict
|
||||||
|
else:
|
||||||
|
num_entries, entry_length, zseparators, addr = self._parse_dict_header(
|
||||||
|
dict_addr
|
||||||
|
)
|
||||||
|
dict = self.get_dictionary(dict_addr)
|
||||||
|
|
||||||
|
# Our list of word separators are actually zscii codes that must
|
||||||
|
# be converted to unicode before we can use them.
|
||||||
|
separators = []
|
||||||
|
for code in zseparators:
|
||||||
|
separators.append(self._zsciitranslator.ztou(code))
|
||||||
|
|
||||||
def parse_input(self, string, dict_addr=None):
|
token_list = self._tokenise_string(string, separators)
|
||||||
"""Given a unicode string, parse it into words based on a dictionary.
|
|
||||||
|
|
||||||
if DICT_ADDR is provided, use the custom dictionary at that
|
final_list = []
|
||||||
address to do the analysis, otherwise default to using the game's
|
for word in token_list:
|
||||||
'standard' dictionary.
|
byte_addr = dict.get(word, 0)
|
||||||
|
final_list.append([word, byte_addr])
|
||||||
|
|
||||||
The dictionary plays two roles: first, it specifies separator
|
return final_list
|
||||||
characters beyond the usual space character. Second, we need to
|
|
||||||
look up each word in the dictionary and return the address.
|
|
||||||
|
|
||||||
Return a list of lists, each list being of the form
|
|
||||||
|
|
||||||
[word, byte_address_of_word_in_dictionary (or 0 if not in dictionary)]
|
|
||||||
"""
|
|
||||||
|
|
||||||
if dict_addr is None:
|
|
||||||
zseparators = self._separators
|
|
||||||
dict = self._dict
|
|
||||||
else:
|
|
||||||
num_entries, entry_length, zseparators, addr = \
|
|
||||||
self._parse_dict_header(dict_addr)
|
|
||||||
dict = self.get_dictionary(dict_addr)
|
|
||||||
|
|
||||||
# Our list of word separators are actually zscii codes that must
|
|
||||||
# be converted to unicode before we can use them.
|
|
||||||
separators = []
|
|
||||||
for code in zseparators:
|
|
||||||
separators.append(self._zsciitranslator.ztou(code))
|
|
||||||
|
|
||||||
token_list = self._tokenise_string(string, separators)
|
|
||||||
|
|
||||||
final_list = []
|
|
||||||
for word in token_list:
|
|
||||||
if word in dict:
|
|
||||||
byte_addr = dict[word]
|
|
||||||
else:
|
|
||||||
byte_addr = 0
|
|
||||||
final_list.append([word, byte_addr])
|
|
||||||
|
|
||||||
return final_list
|
|
||||||
|
|
|
||||||
|
|
@ -12,33 +12,35 @@ logging.getLogger().setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# Create the logging objects regardless. If debugmode is False, then
|
# Create the logging objects regardless. If debugmode is False, then
|
||||||
# they won't actually do anything when used.
|
# they won't actually do anything when used.
|
||||||
mainlog = logging.FileHandler('debug.log', 'a')
|
mainlog_handler = logging.FileHandler("debug.log", "a")
|
||||||
mainlog.setLevel(logging.DEBUG)
|
mainlog_handler.setLevel(logging.DEBUG)
|
||||||
mainlog.setFormatter(logging.Formatter('%(asctime)s: %(message)s'))
|
mainlog_handler.setFormatter(logging.Formatter("%(asctime)s: %(message)s"))
|
||||||
logging.getLogger('mainlog').addHandler(mainlog)
|
logging.getLogger("mainlog").addHandler(mainlog_handler)
|
||||||
|
|
||||||
# We'll store the disassembly in a separate file, for better
|
# We'll store the disassembly in a separate file, for better
|
||||||
# readability.
|
# readability.
|
||||||
disasm = logging.FileHandler('disasm.log', 'a')
|
disasm_handler = logging.FileHandler("disasm.log", "a")
|
||||||
disasm.setLevel(logging.DEBUG)
|
disasm_handler.setLevel(logging.DEBUG)
|
||||||
disasm.setFormatter(logging.Formatter('%(message)s'))
|
disasm_handler.setFormatter(logging.Formatter("%(message)s"))
|
||||||
logging.getLogger('disasm').addHandler(disasm)
|
logging.getLogger("disasm").addHandler(disasm_handler)
|
||||||
|
|
||||||
|
mainlog = logging.getLogger("mainlog")
|
||||||
|
mainlog.info("*** Log reopened ***")
|
||||||
|
disasm = logging.getLogger("disasm")
|
||||||
|
disasm.info("*** Log reopened ***")
|
||||||
|
|
||||||
mainlog = logging.getLogger('mainlog')
|
|
||||||
mainlog.info('*** Log reopened ***')
|
|
||||||
disasm = logging.getLogger('disasm')
|
|
||||||
disasm.info('*** Log reopened ***')
|
|
||||||
|
|
||||||
# Pubilc routines used by other modules
|
# Pubilc routines used by other modules
|
||||||
def set_debug(state):
|
def set_debug(state):
|
||||||
if state:
|
if state:
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
logging.getLogger().setLevel(logging.DEBUG)
|
||||||
else:
|
else:
|
||||||
logging.getLogger().setLevel(logging.CRITICAL)
|
logging.getLogger().setLevel(logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
def log(msg):
|
def log(msg):
|
||||||
mainlog.debug(msg)
|
mainlog.debug(msg)
|
||||||
|
|
||||||
|
|
||||||
def log_disasm(pc, opcode_type, opcode_num, opcode_name, args):
|
def log_disasm(pc, opcode_type, opcode_num, opcode_name, args):
|
||||||
disasm.debug("%06x %s:%02x %s %s" % (pc, opcode_type, opcode_num,
|
disasm.debug(f"{pc:06x} {opcode_type}:{opcode_num:02x} {opcode_name} {args}")
|
||||||
opcode_name, args))
|
|
||||||
|
|
|
||||||
|
|
@ -16,27 +16,34 @@ from .zstring import ZStringFactory
|
||||||
|
|
||||||
|
|
||||||
class ZMachineError(Exception):
|
class ZMachineError(Exception):
|
||||||
"""General exception for ZMachine class"""
|
"""General exception for ZMachine class"""
|
||||||
|
|
||||||
|
|
||||||
class ZMachine:
|
class ZMachine:
|
||||||
"""The Z-Machine black box."""
|
"""The Z-Machine black box."""
|
||||||
|
|
||||||
def __init__(self, story, ui, debugmode=False):
|
def __init__(self, story, ui, debugmode=False):
|
||||||
zlogging.set_debug(debugmode)
|
zlogging.set_debug(debugmode)
|
||||||
self._pristine_mem = ZMemory(story) # the original memory image
|
self._pristine_mem = ZMemory(story) # the original memory image
|
||||||
self._mem = ZMemory(story) # the memory image which changes during play
|
self._mem = ZMemory(story) # the memory image which changes during play
|
||||||
self._stringfactory = ZStringFactory(self._mem)
|
self._stringfactory = ZStringFactory(self._mem)
|
||||||
self._objectparser = ZObjectParser(self._mem)
|
self._objectparser = ZObjectParser(self._mem)
|
||||||
self._stackmanager = ZStackManager(self._mem)
|
self._stackmanager = ZStackManager(self._mem)
|
||||||
self._opdecoder = ZOpDecoder(self._mem, self._stackmanager)
|
self._opdecoder = ZOpDecoder(self._mem, self._stackmanager)
|
||||||
self._opdecoder.program_counter = self._mem.read_word(0x06)
|
self._opdecoder.program_counter = self._mem.read_word(0x06)
|
||||||
self._ui = ui
|
self._ui = ui
|
||||||
self._stream_manager = ZStreamManager(self._mem, self._ui)
|
self._stream_manager = ZStreamManager(self._mem, self._ui)
|
||||||
self._cpu = ZCpu(self._mem, self._opdecoder, self._stackmanager,
|
self._cpu = ZCpu(
|
||||||
self._objectparser, self._stringfactory,
|
self._mem,
|
||||||
self._stream_manager, self._ui)
|
self._opdecoder,
|
||||||
|
self._stackmanager,
|
||||||
|
self._objectparser,
|
||||||
|
self._stringfactory,
|
||||||
|
self._stream_manager,
|
||||||
|
self._ui,
|
||||||
|
)
|
||||||
|
|
||||||
#--------- Public APIs -----------
|
# --------- Public APIs -----------
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
return self._cpu.run()
|
return self._cpu.run()
|
||||||
|
|
|
||||||
|
|
@ -17,262 +17,324 @@ from .zlogging import log
|
||||||
|
|
||||||
|
|
||||||
class ZMemoryError(Exception):
|
class ZMemoryError(Exception):
|
||||||
"General exception for ZMemory class"
|
"General exception for ZMemory class"
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZMemoryIllegalWrite(ZMemoryError):
|
class ZMemoryIllegalWrite(ZMemoryError):
|
||||||
"Tried to write to a read-only part of memory"
|
"Tried to write to a read-only part of memory"
|
||||||
def __init__(self, address):
|
|
||||||
super().__init__(
|
def __init__(self, address):
|
||||||
"Illegal write to address %d" % address)
|
super().__init__(f"Illegal write to address {address}")
|
||||||
|
|
||||||
|
|
||||||
class ZMemoryBadInitialization(ZMemoryError):
|
class ZMemoryBadInitialization(ZMemoryError):
|
||||||
"Failure to initialize ZMemory class"
|
"Failure to initialize ZMemory class"
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZMemoryOutOfBounds(ZMemoryError):
|
class ZMemoryOutOfBounds(ZMemoryError):
|
||||||
"Accessed an address beyond the bounds of memory."
|
"Accessed an address beyond the bounds of memory."
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZMemoryBadMemoryLayout(ZMemoryError):
|
class ZMemoryBadMemoryLayout(ZMemoryError):
|
||||||
"Static plus dynamic memory exceeds 64k"
|
"Static plus dynamic memory exceeds 64k"
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZMemoryBadStoryfileSize(ZMemoryError):
|
class ZMemoryBadStoryfileSize(ZMemoryError):
|
||||||
"Story is too large for Z-machine version."
|
"Story is too large for Z-machine version."
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZMemoryUnsupportedVersion(ZMemoryError):
|
class ZMemoryUnsupportedVersion(ZMemoryError):
|
||||||
"Unsupported version of Z-story file."
|
"Unsupported version of Z-story file."
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZMemory:
|
class ZMemory:
|
||||||
|
# A list of 64 tuples describing who's allowed to tweak header-bytes.
|
||||||
|
# Index into the list is the header-byte being tweaked.
|
||||||
|
# List value is a tuple of the form
|
||||||
|
#
|
||||||
|
# [minimum_z_version, game_allowed, interpreter_allowed]
|
||||||
|
#
|
||||||
|
# Note: in section 11.1 of the spec, we should technically be
|
||||||
|
# enforcing authorization by *bit*, not by byte. Maybe do this
|
||||||
|
# someday.
|
||||||
|
|
||||||
# A list of 64 tuples describing who's allowed to tweak header-bytes.
|
HEADER_PERMS = (
|
||||||
# Index into the list is the header-byte being tweaked.
|
[1, 0, 0],
|
||||||
# List value is a tuple of the form
|
[3, 0, 1],
|
||||||
#
|
None,
|
||||||
# [minimum_z_version, game_allowed, interpreter_allowed]
|
None,
|
||||||
#
|
[1, 0, 0],
|
||||||
# Note: in section 11.1 of the spec, we should technically be
|
None,
|
||||||
# enforcing authorization by *bit*, not by byte. Maybe do this
|
[1, 0, 0],
|
||||||
# someday.
|
None,
|
||||||
|
[1, 0, 0],
|
||||||
|
None,
|
||||||
|
[1, 0, 0],
|
||||||
|
None,
|
||||||
|
[1, 0, 0],
|
||||||
|
None,
|
||||||
|
[1, 0, 0],
|
||||||
|
None,
|
||||||
|
[1, 1, 1],
|
||||||
|
[1, 1, 1],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
[2, 0, 0],
|
||||||
|
None,
|
||||||
|
[3, 0, 0],
|
||||||
|
None,
|
||||||
|
[3, 0, 0],
|
||||||
|
None,
|
||||||
|
[4, 1, 1],
|
||||||
|
[4, 1, 1],
|
||||||
|
[4, 0, 1],
|
||||||
|
[4, 0, 1],
|
||||||
|
[5, 0, 1],
|
||||||
|
None,
|
||||||
|
[5, 0, 1],
|
||||||
|
None,
|
||||||
|
[5, 0, 1],
|
||||||
|
[5, 0, 1],
|
||||||
|
[6, 0, 0],
|
||||||
|
None,
|
||||||
|
[6, 0, 0],
|
||||||
|
None,
|
||||||
|
[5, 0, 1],
|
||||||
|
[5, 0, 1],
|
||||||
|
[5, 0, 0],
|
||||||
|
None,
|
||||||
|
[6, 0, 1],
|
||||||
|
None,
|
||||||
|
[1, 0, 1],
|
||||||
|
None,
|
||||||
|
[5, 0, 0],
|
||||||
|
None,
|
||||||
|
[5, 0, 0],
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
HEADER_PERMS = ([1,0,0], [3,0,1], None, None,
|
def __init__(self, initial_string):
|
||||||
[1,0,0], None, [1,0,0], None,
|
"""Construct class based on a string that represents an initial
|
||||||
[1,0,0], None, [1,0,0], None,
|
'snapshot' of main memory."""
|
||||||
[1,0,0], None, [1,0,0], None,
|
if initial_string is None:
|
||||||
[1,1,1], [1,1,1], None, None,
|
raise ZMemoryBadInitialization
|
||||||
None, None, None, None,
|
|
||||||
[2,0,0], None, [3,0,0], None,
|
|
||||||
[3,0,0], None, [4,1,1], [4,1,1],
|
|
||||||
[4,0,1], [4,0,1], [5,0,1], None,
|
|
||||||
[5,0,1], None, [5,0,1], [5,0,1],
|
|
||||||
[6,0,0], None, [6,0,0], None,
|
|
||||||
[5,0,1], [5,0,1], [5,0,0], None,
|
|
||||||
[6,0,1], None, [1,0,1], None,
|
|
||||||
[5,0,0], None, [5,0,0], None,
|
|
||||||
None, None, None, None,
|
|
||||||
None, None, None, None)
|
|
||||||
|
|
||||||
def __init__(self, initial_string):
|
# Copy string into a _memory sequence that represents main memory.
|
||||||
"""Construct class based on a string that represents an initial
|
self._total_size = len(initial_string)
|
||||||
'snapshot' of main memory."""
|
self._memory = bytearray(initial_string)
|
||||||
if initial_string is None:
|
|
||||||
raise ZMemoryBadInitialization
|
|
||||||
|
|
||||||
# Copy string into a _memory sequence that represents main memory.
|
# Figure out the different sections of memory
|
||||||
self._total_size = len(initial_string)
|
self._static_start = self.read_word(0x0E)
|
||||||
self._memory = bytearray(initial_string)
|
self._static_end = min(0x0FFFF, self._total_size)
|
||||||
|
self._dynamic_start = 0
|
||||||
|
self._dynamic_end = self._static_start - 1
|
||||||
|
self._high_start = self.read_word(0x04)
|
||||||
|
self._high_end = self._total_size
|
||||||
|
self._global_variable_start = self.read_word(0x0C)
|
||||||
|
|
||||||
# Figure out the different sections of memory
|
# Dynamic + static must not exceed 64k
|
||||||
self._static_start = self.read_word(0x0e)
|
dynamic_plus_static = (self._dynamic_end - self._dynamic_start) + (
|
||||||
self._static_end = min(0x0ffff, self._total_size)
|
self._static_end - self._static_start
|
||||||
self._dynamic_start = 0
|
)
|
||||||
self._dynamic_end = self._static_start - 1
|
if dynamic_plus_static > 65534:
|
||||||
self._high_start = self.read_word(0x04)
|
raise ZMemoryBadMemoryLayout
|
||||||
self._high_end = self._total_size
|
|
||||||
self._global_variable_start = self.read_word(0x0c)
|
|
||||||
|
|
||||||
# Dynamic + static must not exceed 64k
|
# What z-machine version is this story file?
|
||||||
dynamic_plus_static = ((self._dynamic_end - self._dynamic_start)
|
self.version = self._memory[0]
|
||||||
+ (self._static_end - self._static_start))
|
|
||||||
if dynamic_plus_static > 65534:
|
|
||||||
raise ZMemoryBadMemoryLayout
|
|
||||||
|
|
||||||
# What z-machine version is this story file?
|
# Validate game size
|
||||||
self.version = self._memory[0]
|
if 1 <= self.version <= 3:
|
||||||
|
if self._total_size > 131072:
|
||||||
|
raise ZMemoryBadStoryfileSize
|
||||||
|
elif 4 <= self.version <= 5:
|
||||||
|
if self._total_size > 262144:
|
||||||
|
raise ZMemoryBadStoryfileSize
|
||||||
|
else:
|
||||||
|
raise ZMemoryUnsupportedVersion
|
||||||
|
|
||||||
# Validate game size
|
log("Memory system initialized, map follows")
|
||||||
if 1 <= self.version <= 3:
|
log(f" Dynamic memory: {self._dynamic_start:x} - {self._dynamic_end:x}")
|
||||||
if self._total_size > 131072:
|
log(f" Static memory: {self._static_start:x} - {self._static_end:x}")
|
||||||
raise ZMemoryBadStoryfileSize
|
log(f" High memory: {self._high_start:x} - {self._high_end:x}")
|
||||||
elif 4 <= self.version <= 5:
|
log(f" Global variable start: {self._global_variable_start:x}")
|
||||||
if self._total_size > 262144:
|
|
||||||
raise ZMemoryBadStoryfileSize
|
|
||||||
else:
|
|
||||||
raise ZMemoryUnsupportedVersion
|
|
||||||
|
|
||||||
log("Memory system initialized, map follows")
|
def _check_bounds(self, index):
|
||||||
log(" Dynamic memory: %x - %x" % (self._dynamic_start, self._dynamic_end))
|
if isinstance(index, slice):
|
||||||
log(" Static memory: %x - %x" % (self._static_start, self._static_end))
|
start, stop = index.start, index.stop
|
||||||
log(" High memory: %x - %x" % (self._high_start, self._high_end))
|
else:
|
||||||
log(" Global variable start: %x" % self._global_variable_start)
|
start, stop = index, index
|
||||||
|
if not ((0 <= start < self._total_size) and (0 <= stop < self._total_size)):
|
||||||
|
raise ZMemoryOutOfBounds
|
||||||
|
|
||||||
def _check_bounds(self, index):
|
def _check_static(self, index):
|
||||||
if isinstance(index, slice):
|
"""Throw error if INDEX is within the static-memory area."""
|
||||||
start, stop = index.start, index.stop
|
if isinstance(index, slice):
|
||||||
else:
|
start, stop = index.start, index.stop
|
||||||
start, stop = index, index
|
else:
|
||||||
if not ((0 <= start < self._total_size) and (0 <= stop < self._total_size)):
|
start, stop = index, index
|
||||||
raise ZMemoryOutOfBounds
|
if (
|
||||||
|
self._static_start <= start <= self._static_end
|
||||||
|
and self._static_start <= stop <= self._static_end
|
||||||
|
):
|
||||||
|
raise ZMemoryIllegalWrite(index)
|
||||||
|
|
||||||
def _check_static(self, index):
|
def print_map(self):
|
||||||
"""Throw error if INDEX is within the static-memory area."""
|
"""Pretty-print a description of the memory map."""
|
||||||
if isinstance(index, slice):
|
print("Dynamic memory: ", self._dynamic_start, "-", self._dynamic_end)
|
||||||
start, stop = index.start, index.stop
|
print(" Static memory: ", self._static_start, "-", self._static_end)
|
||||||
else:
|
print(" High memory: ", self._high_start, "-", self._high_end)
|
||||||
start, stop = index, index
|
|
||||||
if (
|
|
||||||
self._static_start <= start <= self._static_end
|
|
||||||
and self._static_start <= stop <= self._static_end
|
|
||||||
):
|
|
||||||
raise ZMemoryIllegalWrite(index)
|
|
||||||
|
|
||||||
def print_map(self):
|
def __getitem__(self, index):
|
||||||
"""Pretty-print a description of the memory map."""
|
"""Return the byte value stored at address INDEX.."""
|
||||||
print("Dynamic memory: ", self._dynamic_start, "-", self._dynamic_end)
|
self._check_bounds(index)
|
||||||
print(" Static memory: ", self._static_start, "-", self._static_end)
|
return self._memory[index]
|
||||||
print(" High memory: ", self._high_start, "-", self._high_end)
|
|
||||||
|
|
||||||
def __getitem__(self, index):
|
def __setitem__(self, index, value):
|
||||||
"""Return the byte value stored at address INDEX.."""
|
"""Set VALUE in memory address INDEX."""
|
||||||
self._check_bounds(index)
|
self._check_bounds(index)
|
||||||
return self._memory[index]
|
self._check_static(index)
|
||||||
|
self._memory[index] = value
|
||||||
|
|
||||||
def __setitem__(self, index, value):
|
def __getslice__(self, start, end):
|
||||||
"""Set VALUE in memory address INDEX."""
|
"""Return a sequence of bytes from memory."""
|
||||||
self._check_bounds(index)
|
self._check_bounds(start)
|
||||||
self._check_static(index)
|
self._check_bounds(end)
|
||||||
self._memory[index] = value
|
return self._memory[start:end]
|
||||||
|
|
||||||
def __getslice__(self, start, end):
|
def __setslice__(self, start, end, sequence):
|
||||||
"""Return a sequence of bytes from memory."""
|
"""Set a range of memory addresses to SEQUENCE."""
|
||||||
self._check_bounds(start)
|
self._check_bounds(start)
|
||||||
self._check_bounds(end)
|
self._check_bounds(end - 1)
|
||||||
return self._memory[start:end]
|
self._check_static(start)
|
||||||
|
self._check_static(end - 1)
|
||||||
|
self._memory[start:end] = sequence
|
||||||
|
|
||||||
def __setslice__(self, start, end, sequence):
|
def word_address(self, address):
|
||||||
"""Set a range of memory addresses to SEQUENCE."""
|
"""Return the 'actual' address of word address ADDRESS."""
|
||||||
self._check_bounds(start)
|
if address < 0 or address > (self._total_size / 2):
|
||||||
self._check_bounds(end - 1)
|
raise ZMemoryOutOfBounds
|
||||||
self._check_static(start)
|
return address * 2
|
||||||
self._check_static(end - 1)
|
|
||||||
self._memory[start:end] = sequence
|
|
||||||
|
|
||||||
def word_address(self, address):
|
def packed_address(self, address):
|
||||||
"""Return the 'actual' address of word address ADDRESS."""
|
"""Return the 'actual' address of packed address ADDRESS."""
|
||||||
if address < 0 or address > (self._total_size / 2):
|
if 1 <= self.version <= 3:
|
||||||
raise ZMemoryOutOfBounds
|
if address < 0 or address > (self._total_size / 2):
|
||||||
return address*2
|
raise ZMemoryOutOfBounds
|
||||||
|
return address * 2
|
||||||
|
elif 4 <= self.version <= 5:
|
||||||
|
if address < 0 or address > (self._total_size / 4):
|
||||||
|
raise ZMemoryOutOfBounds
|
||||||
|
return address * 4
|
||||||
|
else:
|
||||||
|
raise ZMemoryUnsupportedVersion
|
||||||
|
|
||||||
def packed_address(self, address):
|
def read_word(self, address):
|
||||||
"""Return the 'actual' address of packed address ADDRESS."""
|
"""Return the 16-bit value stored at ADDRESS, ADDRESS+1."""
|
||||||
if 1 <= self.version <= 3:
|
if address < 0 or address >= (self._total_size - 1):
|
||||||
if address < 0 or address > (self._total_size / 2):
|
raise ZMemoryOutOfBounds
|
||||||
raise ZMemoryOutOfBounds
|
return (self._memory[address] << 8) + self._memory[(address + 1)]
|
||||||
return address*2
|
|
||||||
elif 4 <= self.version <= 5:
|
|
||||||
if address < 0 or address > (self._total_size / 4):
|
|
||||||
raise ZMemoryOutOfBounds
|
|
||||||
return address*4
|
|
||||||
else:
|
|
||||||
raise ZMemoryUnsupportedVersion
|
|
||||||
|
|
||||||
def read_word(self, address):
|
def write_word(self, address, value):
|
||||||
"""Return the 16-bit value stored at ADDRESS, ADDRESS+1."""
|
"""Write the given 16-bit value at ADDRESS, ADDRESS+1."""
|
||||||
if address < 0 or address >= (self._total_size - 1):
|
if address < 0 or address >= (self._total_size - 1):
|
||||||
raise ZMemoryOutOfBounds
|
raise ZMemoryOutOfBounds
|
||||||
return (self._memory[address] << 8) + self._memory[(address + 1)]
|
# Handle writing of a word to the game headers. If write_word is
|
||||||
|
# used for this, we assume that it's the game that is setting the
|
||||||
|
# header. The interpreter should use the specialized method.
|
||||||
|
value_msb = (value >> 8) & 0xFF
|
||||||
|
value_lsb = value & 0xFF
|
||||||
|
if 0 <= address < 64:
|
||||||
|
self.game_set_header(address, value_msb)
|
||||||
|
self.game_set_header(address + 1, value_lsb)
|
||||||
|
else:
|
||||||
|
self._memory[address] = value_msb
|
||||||
|
self._memory[address + 1] = value_lsb
|
||||||
|
|
||||||
def write_word(self, address, value):
|
# Normal sequence syntax cannot be used to set bytes in the 64-byte
|
||||||
"""Write the given 16-bit value at ADDRESS, ADDRESS+1."""
|
# header. Instead, the interpreter or game must call one of the
|
||||||
if address < 0 or address >= (self._total_size - 1):
|
# following APIs.
|
||||||
raise ZMemoryOutOfBounds
|
|
||||||
# Handle writing of a word to the game headers. If write_word is
|
|
||||||
# used for this, we assume that it's the game that is setting the
|
|
||||||
# header. The interpreter should use the specialized method.
|
|
||||||
value_msb = (value >> 8) & 0xFF
|
|
||||||
value_lsb = value & 0xFF
|
|
||||||
if 0 <= address < 64:
|
|
||||||
self.game_set_header(address, value_msb)
|
|
||||||
self.game_set_header(address+1, value_lsb)
|
|
||||||
else:
|
|
||||||
self._memory[address] = value_msb
|
|
||||||
self._memory[address+1] = value_lsb
|
|
||||||
|
|
||||||
# Normal sequence syntax cannot be used to set bytes in the 64-byte
|
def interpreter_set_header(self, address, value):
|
||||||
# header. Instead, the interpreter or game must call one of the
|
"""Possibly allow the interpreter to set header ADDRESS to VALUE."""
|
||||||
# following APIs.
|
if address < 0 or address > 63:
|
||||||
|
raise ZMemoryOutOfBounds
|
||||||
|
perm_tuple = self.HEADER_PERMS[address]
|
||||||
|
if perm_tuple is None:
|
||||||
|
raise ZMemoryIllegalWrite(address)
|
||||||
|
if self.version >= perm_tuple[0] and perm_tuple[2]:
|
||||||
|
self._memory[address] = value
|
||||||
|
else:
|
||||||
|
raise ZMemoryIllegalWrite(address)
|
||||||
|
|
||||||
def interpreter_set_header(self, address, value):
|
def game_set_header(self, address, value):
|
||||||
"""Possibly allow the interpreter to set header ADDRESS to VALUE."""
|
"""Possibly allow the game code to set header ADDRESS to VALUE."""
|
||||||
if address < 0 or address > 63:
|
if address < 0 or address > 63:
|
||||||
raise ZMemoryOutOfBounds
|
raise ZMemoryOutOfBounds
|
||||||
perm_tuple = self.HEADER_PERMS[address]
|
perm_tuple = self.HEADER_PERMS[address]
|
||||||
if perm_tuple is None:
|
if perm_tuple is None:
|
||||||
raise ZMemoryIllegalWrite(address)
|
raise ZMemoryIllegalWrite(address)
|
||||||
if self.version >= perm_tuple[0] and perm_tuple[2]:
|
if self.version >= perm_tuple[0] and perm_tuple[1]:
|
||||||
self._memory[address] = value
|
self._memory[address] = value
|
||||||
else:
|
else:
|
||||||
raise ZMemoryIllegalWrite(address)
|
raise ZMemoryIllegalWrite(address)
|
||||||
|
|
||||||
def game_set_header(self, address, value):
|
# The ZPU will need to read and write global variables. The 240
|
||||||
"""Possibly allow the game code to set header ADDRESS to VALUE."""
|
# global variables are located at a place determined by the header.
|
||||||
if address < 0 or address > 63:
|
|
||||||
raise ZMemoryOutOfBounds
|
|
||||||
perm_tuple = self.HEADER_PERMS[address]
|
|
||||||
if perm_tuple is None:
|
|
||||||
raise ZMemoryIllegalWrite(address)
|
|
||||||
if self.version >= perm_tuple[0] and perm_tuple[1]:
|
|
||||||
self._memory[address] = value
|
|
||||||
else:
|
|
||||||
raise ZMemoryIllegalWrite(address)
|
|
||||||
|
|
||||||
# The ZPU will need to read and write global variables. The 240
|
def read_global(self, varnum):
|
||||||
# global variables are located at a place determined by the header.
|
"""Return 16-bit value of global variable VARNUM. Incoming VARNUM
|
||||||
|
must be between 0x10 and 0xFF."""
|
||||||
|
if not (0x10 <= varnum <= 0xFF):
|
||||||
|
raise ZMemoryOutOfBounds
|
||||||
|
actual_address = self._global_variable_start + ((varnum - 0x10) * 2)
|
||||||
|
return self.read_word(actual_address)
|
||||||
|
|
||||||
def read_global(self, varnum):
|
def write_global(self, varnum, value):
|
||||||
"""Return 16-bit value of global variable VARNUM. Incoming VARNUM
|
"""Write 16-bit VALUE to global variable VARNUM. Incoming VARNUM
|
||||||
must be between 0x10 and 0xFF."""
|
must be between 0x10 and 0xFF."""
|
||||||
if not (0x10 <= varnum <= 0xFF):
|
if not (0x10 <= varnum <= 0xFF):
|
||||||
raise ZMemoryOutOfBounds
|
raise ZMemoryOutOfBounds
|
||||||
actual_address = self._global_variable_start + ((varnum - 0x10) * 2)
|
if not (0x00 <= value <= 0xFFFF):
|
||||||
return self.read_word(actual_address)
|
raise ZMemoryIllegalWrite(value)
|
||||||
|
log(f"Write {value} to global variable {varnum}")
|
||||||
|
actual_address = self._global_variable_start + ((varnum - 0x10) * 2)
|
||||||
|
bf = bitfield.BitField(value)
|
||||||
|
self._memory[actual_address] = bf[8:15]
|
||||||
|
self._memory[actual_address + 1] = bf[0:7]
|
||||||
|
|
||||||
def write_global(self, varnum, value):
|
# The 'verify' opcode and the QueztalWriter class both need to have
|
||||||
"""Write 16-bit VALUE to global variable VARNUM. Incoming VARNUM
|
# a checksum of memory generated.
|
||||||
must be between 0x10 and 0xFF."""
|
|
||||||
if not (0x10 <= varnum <= 0xFF):
|
|
||||||
raise ZMemoryOutOfBounds
|
|
||||||
if not (0x00 <= value <= 0xFFFF):
|
|
||||||
raise ZMemoryIllegalWrite(value)
|
|
||||||
log("Write %d to global variable %d" % (value, varnum))
|
|
||||||
actual_address = self._global_variable_start + ((varnum - 0x10) * 2)
|
|
||||||
bf = bitfield.BitField(value)
|
|
||||||
self._memory[actual_address] = bf[8:15]
|
|
||||||
self._memory[actual_address + 1] = bf[0:7]
|
|
||||||
|
|
||||||
# The 'verify' opcode and the QueztalWriter class both need to have
|
def generate_checksum(self):
|
||||||
# a checksum of memory generated.
|
"""Return a checksum value which represents all the bytes of
|
||||||
|
memory added from $0040 upwards, modulo $10000."""
|
||||||
def generate_checksum(self):
|
count = 0x40
|
||||||
"""Return a checksum value which represents all the bytes of
|
total = 0
|
||||||
memory added from $0040 upwards, modulo $10000."""
|
while count < self._total_size:
|
||||||
count = 0x40
|
total += self._memory[count]
|
||||||
total = 0
|
count += 1
|
||||||
while count < self._total_size:
|
return total % 0x10000
|
||||||
total += self._memory[count]
|
|
||||||
count += 1
|
|
||||||
return (total % 0x10000)
|
|
||||||
|
|
|
||||||
|
|
@ -19,414 +19,405 @@ from .zstring import ZStringFactory
|
||||||
|
|
||||||
|
|
||||||
class ZObjectError(Exception):
|
class ZObjectError(Exception):
|
||||||
"General exception for ZObject class"
|
"General exception for ZObject class"
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZObjectIllegalObjectNumber(ZObjectError):
|
class ZObjectIllegalObjectNumber(ZObjectError):
|
||||||
"Illegal object number given."
|
"Illegal object number given."
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZObjectIllegalAttributeNumber(ZObjectError):
|
class ZObjectIllegalAttributeNumber(ZObjectError):
|
||||||
"Illegal attribute number given."
|
"Illegal attribute number given."
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZObjectIllegalPropertyNumber(ZObjectError):
|
class ZObjectIllegalPropertyNumber(ZObjectError):
|
||||||
"Illegal property number given."
|
"Illegal property number given."
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZObjectIllegalPropertySet(ZObjectError):
|
class ZObjectIllegalPropertySet(ZObjectError):
|
||||||
"Illegal set of a property whose size is not 1 or 2."
|
"Illegal set of a property whose size is not 1 or 2."
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZObjectIllegalVersion(ZObjectError):
|
class ZObjectIllegalVersion(ZObjectError):
|
||||||
"Unsupported z-machine version."
|
"Unsupported z-machine version."
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZObjectIllegalPropLength(ZObjectError):
|
class ZObjectIllegalPropLength(ZObjectError):
|
||||||
"Illegal property length."
|
"Illegal property length."
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZObjectMalformedTree(ZObjectError):
|
class ZObjectMalformedTree(ZObjectError):
|
||||||
"Object tree is malformed."
|
"Object tree is malformed."
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# The interpreter should only need exactly one instance of this class.
|
# The interpreter should only need exactly one instance of this class.
|
||||||
|
|
||||||
|
|
||||||
class ZObjectParser:
|
class ZObjectParser:
|
||||||
|
def __init__(self, zmem):
|
||||||
|
self._memory = zmem
|
||||||
|
self._propdefaults_addr = zmem.read_word(0x0A)
|
||||||
|
self._stringfactory = ZStringFactory(self._memory)
|
||||||
|
|
||||||
def __init__(self, zmem):
|
if 1 <= self._memory.version <= 3:
|
||||||
|
self._objecttree_addr = self._propdefaults_addr + 62
|
||||||
|
elif 4 <= self._memory.version <= 5:
|
||||||
|
self._objecttree_addr = self._propdefaults_addr + 126
|
||||||
|
else:
|
||||||
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
self._memory = zmem
|
def _get_object_addr(self, objectnum):
|
||||||
self._propdefaults_addr = zmem.read_word(0x0a)
|
"""Return address of object number OBJECTNUM."""
|
||||||
self._stringfactory = ZStringFactory(self._memory)
|
|
||||||
|
|
||||||
if 1 <= self._memory.version <= 3:
|
result = 0
|
||||||
self._objecttree_addr = self._propdefaults_addr + 62
|
if 1 <= self._memory.version <= 3:
|
||||||
elif 4 <= self._memory.version <= 5:
|
if not (1 <= objectnum <= 255):
|
||||||
self._objecttree_addr = self._propdefaults_addr + 126
|
raise ZObjectIllegalObjectNumber
|
||||||
else:
|
result = self._objecttree_addr + (9 * (objectnum - 1))
|
||||||
raise ZObjectIllegalVersion
|
elif 4 <= self._memory.version <= 5:
|
||||||
|
if not (1 <= objectnum <= 65535):
|
||||||
|
log(f"error: there is no object {objectnum}")
|
||||||
|
raise ZObjectIllegalObjectNumber
|
||||||
|
result = self._objecttree_addr + (14 * (objectnum - 1))
|
||||||
|
else:
|
||||||
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
|
log(f"address of object {objectnum} is {result}")
|
||||||
|
return result
|
||||||
|
|
||||||
def _get_object_addr(self, objectnum):
|
def _get_parent_sibling_child(self, objectnum):
|
||||||
"""Return address of object number OBJECTNUM."""
|
"""Return [parent, sibling, child] object numbers of object OBJECTNUM."""
|
||||||
|
|
||||||
result = 0
|
addr = self._get_object_addr(objectnum)
|
||||||
if 1 <= self._memory.version <= 3:
|
|
||||||
if not (1 <= objectnum <= 255):
|
|
||||||
raise ZObjectIllegalObjectNumber
|
|
||||||
result = self._objecttree_addr + (9 * (objectnum - 1))
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
|
||||||
if not (1 <= objectnum <= 65535):
|
|
||||||
log("error: there is no object %d" % objectnum)
|
|
||||||
raise ZObjectIllegalObjectNumber
|
|
||||||
result = self._objecttree_addr + (14 * (objectnum - 1))
|
|
||||||
else:
|
|
||||||
raise ZObjectIllegalVersion
|
|
||||||
|
|
||||||
log("address of object %d is %d" % (objectnum, result))
|
result = 0
|
||||||
return result
|
if 1 <= self._memory.version <= 3:
|
||||||
|
addr += 4 # skip past attributes
|
||||||
|
result = self._memory[addr : addr + 3]
|
||||||
|
|
||||||
|
elif 4 <= self._memory.version <= 5:
|
||||||
def _get_parent_sibling_child(self, objectnum):
|
addr += 6 # skip past attributes
|
||||||
"""Return [parent, sibling, child] object numbers of object OBJECTNUM."""
|
result = [
|
||||||
|
self._memory.read_word(addr),
|
||||||
addr = self._get_object_addr(objectnum)
|
|
||||||
|
|
||||||
result = 0
|
|
||||||
if 1 <= self._memory.version <= 3:
|
|
||||||
addr += 4 # skip past attributes
|
|
||||||
result = self._memory[addr:addr+3]
|
|
||||||
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
|
||||||
addr += 6 # skip past attributes
|
|
||||||
result = [self._memory.read_word(addr),
|
|
||||||
self._memory.read_word(addr + 2),
|
self._memory.read_word(addr + 2),
|
||||||
self._memory.read_word(addr + 4)]
|
self._memory.read_word(addr + 4),
|
||||||
else:
|
]
|
||||||
raise ZObjectIllegalVersion
|
|
||||||
|
|
||||||
log ("parent/sibling/child of object %d is %d, %d, %d" %
|
|
||||||
(objectnum, result[0], result[1], result[2]))
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _get_proptable_addr(self, objectnum):
|
|
||||||
"""Return address of property table of object OBJECTNUM."""
|
|
||||||
|
|
||||||
addr = self._get_object_addr(objectnum)
|
|
||||||
|
|
||||||
# skip past attributes and relatives
|
|
||||||
if 1 <= self._memory.version <= 3:
|
|
||||||
addr += 7
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
|
||||||
addr += 12
|
|
||||||
else:
|
|
||||||
raise ZObjectIllegalVersion
|
|
||||||
|
|
||||||
return self._memory.read_word(addr)
|
|
||||||
|
|
||||||
def _get_default_property_addr(self, propnum):
|
|
||||||
"""Return address of default value for property PROPNUM."""
|
|
||||||
|
|
||||||
addr = self._propdefaults_addr
|
|
||||||
|
|
||||||
if 1 <= self._memory.version <= 3:
|
|
||||||
if not (1 <= propnum <= 31):
|
|
||||||
raise ZObjectIllegalPropertyNumber
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
|
||||||
if not (1 <= propnum <= 63):
|
|
||||||
raise ZObjectIllegalPropertyNumber
|
|
||||||
else:
|
|
||||||
raise ZObjectIllegalVersion
|
|
||||||
|
|
||||||
return (addr + (2 * (propnum - 1)))
|
|
||||||
|
|
||||||
|
|
||||||
#--------- Public APIs -----------
|
|
||||||
|
|
||||||
def get_attribute(self, objectnum, attrnum):
|
|
||||||
"""Return value (0 or 1) of attribute number ATTRNUM of object
|
|
||||||
number OBJECTNUM."""
|
|
||||||
|
|
||||||
object_addr = self._get_object_addr(objectnum)
|
|
||||||
|
|
||||||
if 1 <= self._memory.version <= 3:
|
|
||||||
if not (0 <= attrnum <= 31):
|
|
||||||
raise ZObjectIllegalAttributeNumber
|
|
||||||
bf = BitField(self._memory[object_addr + (attrnum / 8)])
|
|
||||||
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
|
||||||
if not (0 <= attrnum <= 47):
|
|
||||||
raise ZObjectIllegalAttributeNumber
|
|
||||||
bf = BitField(self._memory[object_addr + (attrnum / 8)])
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ZObjectIllegalVersion
|
|
||||||
|
|
||||||
return bf[7 - (attrnum % 8)]
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_attributes(self, objectnum):
|
|
||||||
"""Return a list of all attribute numbers that are set on object
|
|
||||||
OBJECTNUM"""
|
|
||||||
|
|
||||||
if 1 <= self._memory.version <= 3:
|
|
||||||
max = 32
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
|
||||||
max = 48
|
|
||||||
else:
|
|
||||||
raise ZObjectIllegalVersion
|
|
||||||
|
|
||||||
# really inefficient, but who cares?
|
|
||||||
attrs = []
|
|
||||||
for i in range (0, max):
|
|
||||||
if self.get_attribute(objectnum, i):
|
|
||||||
attrs.append(i)
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
|
|
||||||
def get_parent(self, objectnum):
|
|
||||||
"""Return object number of parent of object number OBJECTNUM."""
|
|
||||||
|
|
||||||
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
|
|
||||||
return parent
|
|
||||||
|
|
||||||
|
|
||||||
def get_child(self, objectnum):
|
|
||||||
"""Return object number of child of object number OBJECTNUM."""
|
|
||||||
|
|
||||||
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
|
|
||||||
return child
|
|
||||||
|
|
||||||
|
|
||||||
def get_sibling(self, objectnum):
|
|
||||||
"""Return object number of sibling of object number OBJECTNUM."""
|
|
||||||
|
|
||||||
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
|
|
||||||
return sibling
|
|
||||||
|
|
||||||
|
|
||||||
def set_parent(self, objectnum, new_parent_num):
|
|
||||||
"""Make OBJECTNUM's parent pointer point to NEW_PARENT_NUM."""
|
|
||||||
|
|
||||||
addr = self._get_object_addr(objectnum)
|
|
||||||
if 1 <= self._memory.version <= 3:
|
|
||||||
self._memory[addr + 4] = new_parent_num
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
|
||||||
self._memory.write_word(addr + 6, new_parent_num)
|
|
||||||
else:
|
|
||||||
raise ZObjectIllegalVersion
|
|
||||||
|
|
||||||
|
|
||||||
def set_child(self, objectnum, new_child_num):
|
|
||||||
"""Make OBJECTNUM's child pointer point to NEW_PARENT_NUM."""
|
|
||||||
|
|
||||||
addr = self._get_object_addr(objectnum)
|
|
||||||
if 1 <= self._memory.version <= 3:
|
|
||||||
self._memory[addr + 6] = new_child_num
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
|
||||||
self._memory.write_word(addr + 10, new_child_num)
|
|
||||||
else:
|
|
||||||
raise ZObjectIllegalVersion
|
|
||||||
|
|
||||||
|
|
||||||
def set_sibling(self, objectnum, new_sibling_num):
|
|
||||||
"""Make OBJECTNUM's sibling pointer point to NEW_PARENT_NUM."""
|
|
||||||
|
|
||||||
addr = self._get_object_addr(objectnum)
|
|
||||||
if 1 <= self._memory.version <= 3:
|
|
||||||
self._memory[addr + 5] = new_sibling_num
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
|
||||||
self._memory.write_word(addr + 8, new_sibling_num)
|
|
||||||
else:
|
|
||||||
raise ZObjectIllegalVersion
|
|
||||||
|
|
||||||
|
|
||||||
def insert_object(self, parent_object, new_child):
|
|
||||||
"""Prepend object NEW_CHILD to the list of PARENT_OBJECT's children."""
|
|
||||||
|
|
||||||
# Remember all the original pointers within the new_child
|
|
||||||
[p, s, c] = self._get_parent_sibling_child(new_child)
|
|
||||||
|
|
||||||
# First insert new_child intto the parent_object
|
|
||||||
original_child = self.get_child(parent_object)
|
|
||||||
self.set_sibling(new_child, original_child)
|
|
||||||
self.set_parent(new_child, parent_object)
|
|
||||||
self.set_child(parent_object, new_child)
|
|
||||||
|
|
||||||
if p == 0: # no need to 'remove' new_child, since it wasn't in a tree
|
|
||||||
return
|
|
||||||
|
|
||||||
# Hunt down and remove the new_child from its old location
|
|
||||||
item = self.get_child(p)
|
|
||||||
if item == 0:
|
|
||||||
# new_object claimed to have parent p, but p has no children!?
|
|
||||||
raise ZObjectMalformedTree
|
|
||||||
elif item == new_child: # done! new_object was head of list
|
|
||||||
self.set_child(p, s) # note that s might be 0, that's fine.
|
|
||||||
else: # walk across list of sibling links
|
|
||||||
prev = item
|
|
||||||
current = self.get_sibling(item)
|
|
||||||
while current != 0:
|
|
||||||
if current == new_child:
|
|
||||||
self.set_sibling(prev, s) # s might be 0, that's fine.
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# we reached the end of the list, never got a match
|
|
||||||
raise ZObjectMalformedTree
|
|
||||||
|
|
||||||
|
|
||||||
def get_shortname(self, objectnum):
|
|
||||||
"""Return 'short name' of object number OBJECTNUM as ascii string."""
|
|
||||||
|
|
||||||
addr = self._get_proptable_addr(objectnum)
|
|
||||||
return self._stringfactory.get(addr+1)
|
|
||||||
|
|
||||||
|
|
||||||
def get_prop(self, objectnum, propnum):
|
|
||||||
"""Return either a byte or word value of property PROPNUM of
|
|
||||||
object OBJECTNUM."""
|
|
||||||
(addr, size) = self.get_prop_addr_len(objectnum, propnum)
|
|
||||||
if size == 1:
|
|
||||||
return self._memory[addr]
|
|
||||||
elif size == 2:
|
|
||||||
return self._memory.read_word(addr)
|
|
||||||
else:
|
|
||||||
raise ZObjectIllegalPropLength
|
|
||||||
|
|
||||||
|
|
||||||
def get_prop_addr_len(self, objectnum, propnum):
|
|
||||||
"""Return address & length of value for property number PROPNUM of
|
|
||||||
object number OBJECTNUM. If object has no such property, then
|
|
||||||
return the address & length of the 'default' value for the property."""
|
|
||||||
|
|
||||||
# start at the beginning of the object's proptable
|
|
||||||
addr = self._get_proptable_addr(objectnum)
|
|
||||||
# skip past the shortname of the object
|
|
||||||
addr += (2 * self._memory[addr])
|
|
||||||
pnum = 0
|
|
||||||
|
|
||||||
if 1 <= self._memory.version <= 3:
|
|
||||||
|
|
||||||
while self._memory[addr] != 0:
|
|
||||||
bf = BitField(self._memory[addr])
|
|
||||||
addr += 1
|
|
||||||
pnum = bf[4:0]
|
|
||||||
size = bf[7:5] + 1
|
|
||||||
if pnum == propnum:
|
|
||||||
return (addr, size)
|
|
||||||
addr += size
|
|
||||||
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
|
||||||
|
|
||||||
while self._memory[addr] != 0:
|
|
||||||
bf = BitField(self._memory[addr])
|
|
||||||
addr += 1
|
|
||||||
pnum = bf[5:0]
|
|
||||||
if bf[7]:
|
|
||||||
bf2 = BitField(self._memory[addr])
|
|
||||||
addr += 1
|
|
||||||
size = bf2[5:0]
|
|
||||||
else:
|
else:
|
||||||
if bf[6]:
|
raise ZObjectIllegalVersion
|
||||||
size = 2
|
|
||||||
else:
|
|
||||||
size = 1
|
|
||||||
if pnum == propnum:
|
|
||||||
return (addr, size)
|
|
||||||
addr += size
|
|
||||||
|
|
||||||
else:
|
log(
|
||||||
raise ZObjectIllegalVersion
|
f"parent/sibling/child of object {objectnum} is "
|
||||||
|
f"{result[0]}, {result[1]}, {result[2]}"
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
# property list ran out, so return default propval instead.
|
def _get_proptable_addr(self, objectnum):
|
||||||
default_value_addr = self._get_default_property_addr(propnum)
|
"""Return address of property table of object OBJECTNUM."""
|
||||||
return (default_value_addr, 2)
|
|
||||||
|
|
||||||
|
addr = self._get_object_addr(objectnum)
|
||||||
|
|
||||||
def get_all_properties(self, objectnum):
|
# skip past attributes and relatives
|
||||||
"""Return a dictionary of all properties listed in the property
|
if 1 <= self._memory.version <= 3:
|
||||||
table of object OBJECTNUM. (Obviously, this discounts 'default'
|
addr += 7
|
||||||
property values.). The dictionary maps property numbers to (addr,
|
elif 4 <= self._memory.version <= 5:
|
||||||
len) propval tuples."""
|
addr += 12
|
||||||
|
|
||||||
proplist = {}
|
|
||||||
|
|
||||||
# start at the beginning of the object's proptable
|
|
||||||
addr = self._get_proptable_addr(objectnum)
|
|
||||||
# skip past the shortname of the object
|
|
||||||
shortname_length = self._memory[addr]
|
|
||||||
addr += 1
|
|
||||||
addr += (2*shortname_length)
|
|
||||||
|
|
||||||
if 1 <= self._memory.version <= 3:
|
|
||||||
while self._memory[addr] != 0:
|
|
||||||
bf = BitField(self._memory[addr])
|
|
||||||
addr += 1
|
|
||||||
pnum = bf[4:0]
|
|
||||||
size = bf[7:5] + 1
|
|
||||||
proplist[pnum] = (addr, size)
|
|
||||||
addr += size
|
|
||||||
|
|
||||||
elif 4 <= self._memory.version <= 5:
|
|
||||||
while self._memory[addr] != 0:
|
|
||||||
bf = BitField(self._memory[addr])
|
|
||||||
addr += 1
|
|
||||||
pnum = bf[0:6]
|
|
||||||
if bf[7]:
|
|
||||||
bf2 = BitField(self._memory[addr])
|
|
||||||
addr += 1
|
|
||||||
size = bf2[0:6]
|
|
||||||
if size == 0:
|
|
||||||
size = 64
|
|
||||||
else:
|
else:
|
||||||
if bf[6]:
|
raise ZObjectIllegalVersion
|
||||||
size = 2
|
|
||||||
else:
|
|
||||||
size = 1
|
|
||||||
proplist[pnum] = (addr, size)
|
|
||||||
addr += size
|
|
||||||
|
|
||||||
else:
|
return self._memory.read_word(addr)
|
||||||
raise ZObjectIllegalVersion
|
|
||||||
|
|
||||||
return proplist
|
def _get_default_property_addr(self, propnum):
|
||||||
|
"""Return address of default value for property PROPNUM."""
|
||||||
|
|
||||||
|
addr = self._propdefaults_addr
|
||||||
|
|
||||||
def set_property(self, objectnum, propnum, value):
|
if 1 <= self._memory.version <= 3:
|
||||||
"""Set a property on an object."""
|
if not (1 <= propnum <= 31):
|
||||||
proplist = self.get_all_properties(objectnum)
|
raise ZObjectIllegalPropertyNumber
|
||||||
if propnum not in proplist:
|
elif 4 <= self._memory.version <= 5:
|
||||||
raise ZObjectIllegalPropertyNumber
|
if not (1 <= propnum <= 63):
|
||||||
|
raise ZObjectIllegalPropertyNumber
|
||||||
|
else:
|
||||||
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
addr, size = proplist[propnum]
|
return addr + (2 * (propnum - 1))
|
||||||
if size == 1:
|
|
||||||
self._memory[addr] = (value & 0xFF)
|
|
||||||
elif size == 2:
|
|
||||||
self._memory.write_word(addr, value)
|
|
||||||
else:
|
|
||||||
raise ZObjectIllegalPropertySet
|
|
||||||
|
|
||||||
|
# --------- Public APIs -----------
|
||||||
|
|
||||||
def describe_object(self, objectnum):
|
def get_attribute(self, objectnum, attrnum):
|
||||||
"""For debugging purposes, pretty-print everything known about
|
"""Return value (0 or 1) of attribute number ATTRNUM of object
|
||||||
object OBJECTNUM."""
|
number OBJECTNUM."""
|
||||||
|
|
||||||
print("Object number:", objectnum)
|
object_addr = self._get_object_addr(objectnum)
|
||||||
print(" Short name:", self.get_shortname(objectnum))
|
|
||||||
print(" Parent:", self.get_parent(objectnum), end=' ')
|
|
||||||
print(" Sibling:", self.get_sibling(objectnum), end=' ')
|
|
||||||
print(" Child:", self.get_child(objectnum))
|
|
||||||
print(" Attributes:", self.get_all_attributes(objectnum))
|
|
||||||
print(" Properties:")
|
|
||||||
|
|
||||||
proplist = self.get_all_properties(objectnum)
|
if 1 <= self._memory.version <= 3:
|
||||||
for key in list(proplist.keys()):
|
if not (0 <= attrnum <= 31):
|
||||||
(addr, len) = proplist[key]
|
raise ZObjectIllegalAttributeNumber
|
||||||
print(" [%2d] :" % key, end=' ')
|
bf = BitField(self._memory[object_addr + (attrnum / 8)])
|
||||||
for i in range(0, len):
|
|
||||||
print("%02X" % self._memory[addr+i], end=' ')
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
elif 4 <= self._memory.version <= 5:
|
||||||
|
if not (0 <= attrnum <= 47):
|
||||||
|
raise ZObjectIllegalAttributeNumber
|
||||||
|
bf = BitField(self._memory[object_addr + (attrnum / 8)])
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
|
return bf[7 - (attrnum % 8)]
|
||||||
|
|
||||||
|
def get_all_attributes(self, objectnum):
|
||||||
|
"""Return a list of all attribute numbers that are set on object
|
||||||
|
OBJECTNUM"""
|
||||||
|
|
||||||
|
if 1 <= self._memory.version <= 3:
|
||||||
|
max = 32
|
||||||
|
elif 4 <= self._memory.version <= 5:
|
||||||
|
max = 48
|
||||||
|
else:
|
||||||
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
|
# really inefficient, but who cares?
|
||||||
|
attrs = []
|
||||||
|
for i in range(0, max):
|
||||||
|
if self.get_attribute(objectnum, i):
|
||||||
|
attrs.append(i)
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
def get_parent(self, objectnum):
|
||||||
|
"""Return object number of parent of object number OBJECTNUM."""
|
||||||
|
|
||||||
|
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
|
||||||
|
return parent
|
||||||
|
|
||||||
|
def get_child(self, objectnum):
|
||||||
|
"""Return object number of child of object number OBJECTNUM."""
|
||||||
|
|
||||||
|
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
|
||||||
|
return child
|
||||||
|
|
||||||
|
def get_sibling(self, objectnum):
|
||||||
|
"""Return object number of sibling of object number OBJECTNUM."""
|
||||||
|
|
||||||
|
[parent, sibling, child] = self._get_parent_sibling_child(objectnum)
|
||||||
|
return sibling
|
||||||
|
|
||||||
|
def set_parent(self, objectnum, new_parent_num):
|
||||||
|
"""Make OBJECTNUM's parent pointer point to NEW_PARENT_NUM."""
|
||||||
|
|
||||||
|
addr = self._get_object_addr(objectnum)
|
||||||
|
if 1 <= self._memory.version <= 3:
|
||||||
|
self._memory[addr + 4] = new_parent_num
|
||||||
|
elif 4 <= self._memory.version <= 5:
|
||||||
|
self._memory.write_word(addr + 6, new_parent_num)
|
||||||
|
else:
|
||||||
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
|
def set_child(self, objectnum, new_child_num):
|
||||||
|
"""Make OBJECTNUM's child pointer point to NEW_PARENT_NUM."""
|
||||||
|
|
||||||
|
addr = self._get_object_addr(objectnum)
|
||||||
|
if 1 <= self._memory.version <= 3:
|
||||||
|
self._memory[addr + 6] = new_child_num
|
||||||
|
elif 4 <= self._memory.version <= 5:
|
||||||
|
self._memory.write_word(addr + 10, new_child_num)
|
||||||
|
else:
|
||||||
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
|
def set_sibling(self, objectnum, new_sibling_num):
|
||||||
|
"""Make OBJECTNUM's sibling pointer point to NEW_PARENT_NUM."""
|
||||||
|
|
||||||
|
addr = self._get_object_addr(objectnum)
|
||||||
|
if 1 <= self._memory.version <= 3:
|
||||||
|
self._memory[addr + 5] = new_sibling_num
|
||||||
|
elif 4 <= self._memory.version <= 5:
|
||||||
|
self._memory.write_word(addr + 8, new_sibling_num)
|
||||||
|
else:
|
||||||
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
|
def insert_object(self, parent_object, new_child):
|
||||||
|
"""Prepend object NEW_CHILD to the list of PARENT_OBJECT's children."""
|
||||||
|
|
||||||
|
# Remember all the original pointers within the new_child
|
||||||
|
[p, s, c] = self._get_parent_sibling_child(new_child)
|
||||||
|
|
||||||
|
# First insert new_child intto the parent_object
|
||||||
|
original_child = self.get_child(parent_object)
|
||||||
|
self.set_sibling(new_child, original_child)
|
||||||
|
self.set_parent(new_child, parent_object)
|
||||||
|
self.set_child(parent_object, new_child)
|
||||||
|
|
||||||
|
if p == 0: # no need to 'remove' new_child, since it wasn't in a tree
|
||||||
|
return
|
||||||
|
|
||||||
|
# Hunt down and remove the new_child from its old location
|
||||||
|
item = self.get_child(p)
|
||||||
|
if item == 0:
|
||||||
|
# new_object claimed to have parent p, but p has no children!?
|
||||||
|
raise ZObjectMalformedTree
|
||||||
|
elif item == new_child: # done! new_object was head of list
|
||||||
|
self.set_child(p, s) # note that s might be 0, that's fine.
|
||||||
|
else: # walk across list of sibling links
|
||||||
|
prev = item
|
||||||
|
current = self.get_sibling(item)
|
||||||
|
while current != 0:
|
||||||
|
if current == new_child:
|
||||||
|
self.set_sibling(prev, s) # s might be 0, that's fine.
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# we reached the end of the list, never got a match
|
||||||
|
raise ZObjectMalformedTree
|
||||||
|
|
||||||
|
def get_shortname(self, objectnum):
|
||||||
|
"""Return 'short name' of object number OBJECTNUM as ascii string."""
|
||||||
|
|
||||||
|
addr = self._get_proptable_addr(objectnum)
|
||||||
|
return self._stringfactory.get(addr + 1)
|
||||||
|
|
||||||
|
def get_prop(self, objectnum, propnum):
|
||||||
|
"""Return either a byte or word value of property PROPNUM of
|
||||||
|
object OBJECTNUM."""
|
||||||
|
(addr, size) = self.get_prop_addr_len(objectnum, propnum)
|
||||||
|
if size == 1:
|
||||||
|
return self._memory[addr]
|
||||||
|
elif size == 2:
|
||||||
|
return self._memory.read_word(addr)
|
||||||
|
else:
|
||||||
|
raise ZObjectIllegalPropLength
|
||||||
|
|
||||||
|
def get_prop_addr_len(self, objectnum, propnum):
|
||||||
|
"""Return address & length of value for property number PROPNUM of
|
||||||
|
object number OBJECTNUM. If object has no such property, then
|
||||||
|
return the address & length of the 'default' value for the property."""
|
||||||
|
|
||||||
|
# start at the beginning of the object's proptable
|
||||||
|
addr = self._get_proptable_addr(objectnum)
|
||||||
|
# skip past the shortname of the object
|
||||||
|
addr += 2 * self._memory[addr]
|
||||||
|
pnum = 0
|
||||||
|
|
||||||
|
if 1 <= self._memory.version <= 3:
|
||||||
|
while self._memory[addr] != 0:
|
||||||
|
bf = BitField(self._memory[addr])
|
||||||
|
addr += 1
|
||||||
|
pnum = bf[4:0]
|
||||||
|
size = bf[7:5] + 1
|
||||||
|
if pnum == propnum:
|
||||||
|
return (addr, size)
|
||||||
|
addr += size
|
||||||
|
|
||||||
|
elif 4 <= self._memory.version <= 5:
|
||||||
|
while self._memory[addr] != 0:
|
||||||
|
bf = BitField(self._memory[addr])
|
||||||
|
addr += 1
|
||||||
|
pnum = bf[5:0]
|
||||||
|
if bf[7]:
|
||||||
|
bf2 = BitField(self._memory[addr])
|
||||||
|
addr += 1
|
||||||
|
size = bf2[5:0]
|
||||||
|
else:
|
||||||
|
size = 2 if bf[6] else 1
|
||||||
|
if pnum == propnum:
|
||||||
|
return (addr, size)
|
||||||
|
addr += size
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
|
# property list ran out, so return default propval instead.
|
||||||
|
default_value_addr = self._get_default_property_addr(propnum)
|
||||||
|
return (default_value_addr, 2)
|
||||||
|
|
||||||
|
def get_all_properties(self, objectnum):
|
||||||
|
"""Return a dictionary of all properties listed in the property
|
||||||
|
table of object OBJECTNUM. (Obviously, this discounts 'default'
|
||||||
|
property values.). The dictionary maps property numbers to (addr,
|
||||||
|
len) propval tuples."""
|
||||||
|
|
||||||
|
proplist = {}
|
||||||
|
|
||||||
|
# start at the beginning of the object's proptable
|
||||||
|
addr = self._get_proptable_addr(objectnum)
|
||||||
|
# skip past the shortname of the object
|
||||||
|
shortname_length = self._memory[addr]
|
||||||
|
addr += 1
|
||||||
|
addr += 2 * shortname_length
|
||||||
|
|
||||||
|
if 1 <= self._memory.version <= 3:
|
||||||
|
while self._memory[addr] != 0:
|
||||||
|
bf = BitField(self._memory[addr])
|
||||||
|
addr += 1
|
||||||
|
pnum = bf[4:0]
|
||||||
|
size = bf[7:5] + 1
|
||||||
|
proplist[pnum] = (addr, size)
|
||||||
|
addr += size
|
||||||
|
|
||||||
|
elif 4 <= self._memory.version <= 5:
|
||||||
|
while self._memory[addr] != 0:
|
||||||
|
bf = BitField(self._memory[addr])
|
||||||
|
addr += 1
|
||||||
|
pnum = bf[0:6]
|
||||||
|
if bf[7]:
|
||||||
|
bf2 = BitField(self._memory[addr])
|
||||||
|
addr += 1
|
||||||
|
size = bf2[0:6]
|
||||||
|
if size == 0:
|
||||||
|
size = 64
|
||||||
|
else:
|
||||||
|
size = 2 if bf[6] else 1
|
||||||
|
proplist[pnum] = (addr, size)
|
||||||
|
addr += size
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ZObjectIllegalVersion
|
||||||
|
|
||||||
|
return proplist
|
||||||
|
|
||||||
|
def set_property(self, objectnum, propnum, value):
|
||||||
|
"""Set a property on an object."""
|
||||||
|
proplist = self.get_all_properties(objectnum)
|
||||||
|
if propnum not in proplist:
|
||||||
|
raise ZObjectIllegalPropertyNumber
|
||||||
|
|
||||||
|
addr, size = proplist[propnum]
|
||||||
|
if size == 1:
|
||||||
|
self._memory[addr] = value & 0xFF
|
||||||
|
elif size == 2:
|
||||||
|
self._memory.write_word(addr, value)
|
||||||
|
else:
|
||||||
|
raise ZObjectIllegalPropertySet
|
||||||
|
|
||||||
|
def describe_object(self, objectnum):
|
||||||
|
"""For debugging purposes, pretty-print everything known about
|
||||||
|
object OBJECTNUM."""
|
||||||
|
|
||||||
|
print("Object number:", objectnum)
|
||||||
|
print(" Short name:", self.get_shortname(objectnum))
|
||||||
|
print(" Parent:", self.get_parent(objectnum), end=" ")
|
||||||
|
print(" Sibling:", self.get_sibling(objectnum), end=" ")
|
||||||
|
print(" Child:", self.get_child(objectnum))
|
||||||
|
print(" Attributes:", self.get_all_attributes(objectnum))
|
||||||
|
print(" Properties:")
|
||||||
|
|
||||||
|
proplist = self.get_all_properties(objectnum)
|
||||||
|
for key in list(proplist.keys()):
|
||||||
|
(addr, len) = proplist[key]
|
||||||
|
print(f" [{key:2d}] :", end=" ")
|
||||||
|
for i in range(0, len):
|
||||||
|
print(f"{self._memory[addr + i]:02X}", end=" ")
|
||||||
|
print()
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,10 @@ from .zlogging import log
|
||||||
|
|
||||||
|
|
||||||
class ZOperationError(Exception):
|
class ZOperationError(Exception):
|
||||||
"General exception for ZOperation class"
|
"General exception for ZOperation class"
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Constants defining the known instruction types. These types are
|
# Constants defining the known instruction types. These types are
|
||||||
# related to the number of operands the opcode has: for each operand
|
# related to the number of operands the opcode has: for each operand
|
||||||
|
|
@ -27,12 +29,12 @@ OPCODE_EXT = 4
|
||||||
# Mapping of those constants to strings describing the opcode
|
# Mapping of those constants to strings describing the opcode
|
||||||
# classes. Used for pretty-printing only.
|
# classes. Used for pretty-printing only.
|
||||||
OPCODE_STRINGS = {
|
OPCODE_STRINGS = {
|
||||||
OPCODE_0OP: '0OP',
|
OPCODE_0OP: "0OP",
|
||||||
OPCODE_1OP: '1OP',
|
OPCODE_1OP: "1OP",
|
||||||
OPCODE_2OP: '2OP',
|
OPCODE_2OP: "2OP",
|
||||||
OPCODE_VAR: 'VAR',
|
OPCODE_VAR: "VAR",
|
||||||
OPCODE_EXT: 'EXT',
|
OPCODE_EXT: "EXT",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Constants defining the possible operand types.
|
# Constants defining the possible operand types.
|
||||||
LARGE_CONSTANT = 0x0
|
LARGE_CONSTANT = 0x0
|
||||||
|
|
@ -40,195 +42,203 @@ SMALL_CONSTANT = 0x1
|
||||||
VARIABLE = 0x2
|
VARIABLE = 0x2
|
||||||
ABSENT = 0x3
|
ABSENT = 0x3
|
||||||
|
|
||||||
|
|
||||||
class ZOpDecoder:
|
class ZOpDecoder:
|
||||||
def __init__(self, zmem, zstack):
|
def __init__(self, zmem, zstack):
|
||||||
""
|
""
|
||||||
self._memory = zmem
|
self._memory = zmem
|
||||||
self._stack = zstack
|
self._stack = zstack
|
||||||
self._parse_map = {}
|
self._parse_map = {}
|
||||||
self.program_counter = self._memory.read_word(0x6)
|
self.program_counter = self._memory.read_word(0x6)
|
||||||
|
|
||||||
def _get_pc(self):
|
def _get_pc(self):
|
||||||
byte = self._memory[self.program_counter]
|
byte = self._memory[self.program_counter]
|
||||||
self.program_counter += 1
|
self.program_counter += 1
|
||||||
return byte
|
return byte
|
||||||
|
|
||||||
def get_next_instruction(self):
|
def get_next_instruction(self):
|
||||||
"""Decode the opcode & operands currently pointed to by the
|
"""Decode the opcode & operands currently pointed to by the
|
||||||
program counter, and appropriately increment the program counter
|
program counter, and appropriately increment the program counter
|
||||||
afterwards. A decoded operation is returned to the caller in the form:
|
afterwards. A decoded operation is returned to the caller in the form:
|
||||||
|
|
||||||
[opcode-class, opcode-number, [operand, operand, operand, ...]]
|
[opcode-class, opcode-number, [operand, operand, operand, ...]]
|
||||||
|
|
||||||
If the opcode has no operands, the operand list is present but empty."""
|
If the opcode has no operands, the operand list is present but empty."""
|
||||||
|
|
||||||
opcode = self._get_pc()
|
opcode = self._get_pc()
|
||||||
|
|
||||||
log("Decode opcode %x" % opcode)
|
log(f"Decode opcode {opcode:x}")
|
||||||
|
|
||||||
# Determine the opcode type, and hand off further parsing.
|
# Determine the opcode type, and hand off further parsing.
|
||||||
if self._memory.version == 5 and opcode == 0xBE:
|
if self._memory.version == 5 and opcode == 0xBE:
|
||||||
# Extended opcode
|
# Extended opcode
|
||||||
return self._parse_opcode_extended()
|
return self._parse_opcode_extended()
|
||||||
|
|
||||||
opcode = BitField(opcode)
|
opcode = BitField(opcode)
|
||||||
if opcode[7] == 0:
|
if opcode[7] == 0:
|
||||||
# Long opcode
|
# Long opcode
|
||||||
return self._parse_opcode_long(opcode)
|
return self._parse_opcode_long(opcode)
|
||||||
elif opcode[6] == 0:
|
elif opcode[6] == 0:
|
||||||
# Short opcode
|
# Short opcode
|
||||||
return self._parse_opcode_short(opcode)
|
return self._parse_opcode_short(opcode)
|
||||||
else:
|
else:
|
||||||
# Variable opcode
|
# Variable opcode
|
||||||
return self._parse_opcode_variable(opcode)
|
return self._parse_opcode_variable(opcode)
|
||||||
|
|
||||||
def _parse_opcode_long(self, opcode):
|
def _parse_opcode_long(self, opcode):
|
||||||
"""Parse an opcode of the long form."""
|
"""Parse an opcode of the long form."""
|
||||||
# Long opcodes are always 2OP. The types of the two operands are
|
# Long opcodes are always 2OP. The types of the two operands are
|
||||||
# encoded in bits 5 and 6 of the opcode.
|
# encoded in bits 5 and 6 of the opcode.
|
||||||
log("Opcode is long")
|
log("Opcode is long")
|
||||||
LONG_OPERAND_TYPES = [SMALL_CONSTANT, VARIABLE]
|
LONG_OPERAND_TYPES = [SMALL_CONSTANT, VARIABLE]
|
||||||
operands = [self._parse_operand(LONG_OPERAND_TYPES[opcode[6]]),
|
operands = [
|
||||||
self._parse_operand(LONG_OPERAND_TYPES[opcode[5]])]
|
self._parse_operand(LONG_OPERAND_TYPES[opcode[6]]),
|
||||||
return (OPCODE_2OP, opcode[0:5], operands)
|
self._parse_operand(LONG_OPERAND_TYPES[opcode[5]]),
|
||||||
|
]
|
||||||
|
return (OPCODE_2OP, opcode[0:5], operands)
|
||||||
|
|
||||||
def _parse_opcode_short(self, opcode):
|
def _parse_opcode_short(self, opcode):
|
||||||
"""Parse an opcode of the short form."""
|
"""Parse an opcode of the short form."""
|
||||||
# Short opcodes can have either 1 operand, or no operand.
|
# Short opcodes can have either 1 operand, or no operand.
|
||||||
log("Opcode is short")
|
log("Opcode is short")
|
||||||
operand_type = opcode[4:6]
|
operand_type = opcode[4:6]
|
||||||
operand = self._parse_operand(operand_type)
|
operand = self._parse_operand(operand_type)
|
||||||
if operand is None: # 0OP variant
|
if operand is None: # 0OP variant
|
||||||
log("Opcode is 0OP variant")
|
log("Opcode is 0OP variant")
|
||||||
return (OPCODE_0OP, opcode[0:4], [])
|
return (OPCODE_0OP, opcode[0:4], [])
|
||||||
else:
|
else:
|
||||||
log("Opcode is 1OP variant")
|
log("Opcode is 1OP variant")
|
||||||
return (OPCODE_1OP, opcode[0:4], [operand])
|
return (OPCODE_1OP, opcode[0:4], [operand])
|
||||||
|
|
||||||
def _parse_opcode_variable(self, opcode):
|
def _parse_opcode_variable(self, opcode):
|
||||||
"""Parse an opcode of the variable form."""
|
"""Parse an opcode of the variable form."""
|
||||||
log("Opcode is variable")
|
log("Opcode is variable")
|
||||||
if opcode[5]:
|
if opcode[5]:
|
||||||
log("Variable opcode of VAR kind")
|
log("Variable opcode of VAR kind")
|
||||||
opcode_type = OPCODE_VAR
|
opcode_type = OPCODE_VAR
|
||||||
else:
|
else:
|
||||||
log("Variable opcode of 2OP kind")
|
log("Variable opcode of 2OP kind")
|
||||||
opcode_type = OPCODE_2OP
|
opcode_type = OPCODE_2OP
|
||||||
|
|
||||||
opcode_num = opcode[0:5]
|
opcode_num = opcode[0:5]
|
||||||
|
|
||||||
# Parse the types byte to retrieve the operands.
|
# Parse the types byte to retrieve the operands.
|
||||||
operands = self._parse_operands_byte()
|
operands = self._parse_operands_byte()
|
||||||
|
|
||||||
# Special case: opcodes 12 and 26 have a second operands byte.
|
# Special case: opcodes 12 and 26 have a second operands byte.
|
||||||
if opcode[0:7] == 0xC or opcode[0:7] == 0x1A:
|
if opcode[0:7] == 0xC or opcode[0:7] == 0x1A:
|
||||||
log("Opcode has second operand byte")
|
log("Opcode has second operand byte")
|
||||||
operands += self._parse_operands_byte()
|
operands += self._parse_operands_byte()
|
||||||
|
|
||||||
return (opcode_type, opcode_num, operands)
|
return (opcode_type, opcode_num, operands)
|
||||||
|
|
||||||
def _parse_operand(self, operand_type):
|
def _parse_opcode_extended(self):
|
||||||
"""Read and return an operand of the given type.
|
"""Parse an extended opcode (v5+ feature)."""
|
||||||
|
raise NotImplementedError("Extended opcodes (v5+) not yet implemented")
|
||||||
|
|
||||||
This assumes that the operand is in memory, at the address pointed
|
def _parse_operand(self, operand_type):
|
||||||
by the Program Counter."""
|
"""Read and return an operand of the given type.
|
||||||
assert operand_type <= 0x3
|
|
||||||
|
|
||||||
if operand_type == LARGE_CONSTANT:
|
This assumes that the operand is in memory, at the address pointed
|
||||||
log("Operand is large constant")
|
by the Program Counter."""
|
||||||
operand = self._memory.read_word(self.program_counter)
|
assert operand_type <= 0x3
|
||||||
self.program_counter += 2
|
|
||||||
elif operand_type == SMALL_CONSTANT:
|
|
||||||
log("Operand is small constant")
|
|
||||||
operand = self._get_pc()
|
|
||||||
elif operand_type == VARIABLE:
|
|
||||||
variable_number = self._get_pc()
|
|
||||||
log("Operand is variable %d" % variable_number)
|
|
||||||
if variable_number == 0:
|
|
||||||
log("Operand value comes from stack")
|
|
||||||
operand = self._stack.pop_stack() # TODO: make sure this is right.
|
|
||||||
elif variable_number < 16:
|
|
||||||
log("Operand value comes from local variable")
|
|
||||||
operand = self._stack.get_local_variable(variable_number - 1)
|
|
||||||
else:
|
|
||||||
log("Operand value comes from global variable")
|
|
||||||
operand = self._memory.read_global(variable_number)
|
|
||||||
elif operand_type == ABSENT:
|
|
||||||
log("Operand is absent")
|
|
||||||
operand = None
|
|
||||||
if operand is not None:
|
|
||||||
log("Operand value: %d" % operand)
|
|
||||||
|
|
||||||
return operand
|
if operand_type == LARGE_CONSTANT:
|
||||||
|
log("Operand is large constant")
|
||||||
|
operand = self._memory.read_word(self.program_counter)
|
||||||
|
self.program_counter += 2
|
||||||
|
elif operand_type == SMALL_CONSTANT:
|
||||||
|
log("Operand is small constant")
|
||||||
|
operand = self._get_pc()
|
||||||
|
elif operand_type == VARIABLE:
|
||||||
|
variable_number = self._get_pc()
|
||||||
|
log(f"Operand is variable {variable_number}")
|
||||||
|
if variable_number == 0:
|
||||||
|
log("Operand value comes from stack")
|
||||||
|
operand = self._stack.pop_stack() # TODO: make sure this is right.
|
||||||
|
elif variable_number < 16:
|
||||||
|
log("Operand value comes from local variable")
|
||||||
|
operand = self._stack.get_local_variable(variable_number - 1)
|
||||||
|
else:
|
||||||
|
log("Operand value comes from global variable")
|
||||||
|
operand = self._memory.read_global(variable_number)
|
||||||
|
elif operand_type == ABSENT:
|
||||||
|
log("Operand is absent")
|
||||||
|
operand = None
|
||||||
|
if operand is not None:
|
||||||
|
log(f"Operand value: {operand}")
|
||||||
|
|
||||||
def _parse_operands_byte(self):
|
return operand
|
||||||
"""Parse operands given by the operand byte and return a list of
|
|
||||||
values.
|
|
||||||
"""
|
|
||||||
operand_byte = BitField(self._get_pc())
|
|
||||||
operands = []
|
|
||||||
for operand_type in [operand_byte[6:8], operand_byte[4:6],
|
|
||||||
operand_byte[2:4], operand_byte[0:2]]:
|
|
||||||
operand = self._parse_operand(operand_type)
|
|
||||||
if operand is None:
|
|
||||||
break
|
|
||||||
operands.append(operand)
|
|
||||||
|
|
||||||
return operands
|
def _parse_operands_byte(self):
|
||||||
|
"""Parse operands given by the operand byte and return a list of
|
||||||
|
values.
|
||||||
|
"""
|
||||||
|
operand_byte = BitField(self._get_pc())
|
||||||
|
operands = []
|
||||||
|
for operand_type in [
|
||||||
|
operand_byte[6:8],
|
||||||
|
operand_byte[4:6],
|
||||||
|
operand_byte[2:4],
|
||||||
|
operand_byte[0:2],
|
||||||
|
]:
|
||||||
|
operand = self._parse_operand(operand_type)
|
||||||
|
if operand is None:
|
||||||
|
break
|
||||||
|
operands.append(operand)
|
||||||
|
|
||||||
|
return operands
|
||||||
|
|
||||||
# Public funcs that the ZPU may also need to call, depending on the
|
# Public funcs that the ZPU may also need to call, depending on the
|
||||||
# opcode being executed:
|
# opcode being executed:
|
||||||
|
|
||||||
def get_zstring(self):
|
def get_zstring(self):
|
||||||
"""For string opcodes, return the address of the zstring pointed
|
"""For string opcodes, return the address of the zstring pointed
|
||||||
to by the PC. Increment PC just past the text."""
|
to by the PC. Increment PC just past the text."""
|
||||||
|
|
||||||
start_addr = self.program_counter
|
start_addr = self.program_counter
|
||||||
bf = BitField(0)
|
bf = BitField(0)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
bf.__init__(self._memory[self.program_counter])
|
bf.__init__(self._memory[self.program_counter])
|
||||||
self.program_counter += 2
|
self.program_counter += 2
|
||||||
if bf[7] == 1:
|
if bf[7] == 1:
|
||||||
break
|
break
|
||||||
|
|
||||||
return start_addr
|
return start_addr
|
||||||
|
|
||||||
|
def get_store_address(self):
|
||||||
|
"""For store opcodes, read byte pointed to by PC and return the
|
||||||
|
variable number in which the operation result should be stored.
|
||||||
|
Increment the PC as necessary."""
|
||||||
|
return self._get_pc()
|
||||||
|
|
||||||
def get_store_address(self):
|
def get_branch_offset(self):
|
||||||
"""For store opcodes, read byte pointed to by PC and return the
|
"""For branching opcodes, examine address pointed to by PC, and
|
||||||
variable number in which the operation result should be stored.
|
return two values: first, either True or False (indicating whether
|
||||||
Increment the PC as necessary."""
|
to branch if true or branch if false), and second, the address to
|
||||||
return self._get_pc()
|
jump to. Increment the PC as necessary."""
|
||||||
|
|
||||||
|
bf = BitField(self._get_pc())
|
||||||
|
branch_if_true = bool(bf[7])
|
||||||
|
if bf[6]:
|
||||||
|
branch_offset = bf[0:6]
|
||||||
|
else:
|
||||||
|
# We need to do a little magic here. The branch offset is
|
||||||
|
# written as a signed 14-bit number, with signed meaning '-n' is
|
||||||
|
# written as '65536-n'. Or in this case, as we have 14 bits,
|
||||||
|
# '16384-n'.
|
||||||
|
#
|
||||||
|
# So, if the MSB (ie. bit 13) is set, we have a negative
|
||||||
|
# number. We take the value, and substract 16384 to get the
|
||||||
|
# actual offset as a negative integer.
|
||||||
|
#
|
||||||
|
# If the MSB is not set, we just extract the value and return it.
|
||||||
|
#
|
||||||
|
# Can you spell "Weird" ?
|
||||||
|
branch_offset = self._get_pc() + (bf[0:5] << 8)
|
||||||
|
if bf[5]:
|
||||||
|
branch_offset -= 8192
|
||||||
|
|
||||||
def get_branch_offset(self):
|
log(f"Branch if {branch_if_true} to offset {branch_offset:+d}")
|
||||||
"""For branching opcodes, examine address pointed to by PC, and
|
return branch_if_true, branch_offset
|
||||||
return two values: first, either True or False (indicating whether
|
|
||||||
to branch if true or branch if false), and second, the address to
|
|
||||||
jump to. Increment the PC as necessary."""
|
|
||||||
|
|
||||||
bf = BitField(self._get_pc())
|
|
||||||
branch_if_true = bool(bf[7])
|
|
||||||
if bf[6]:
|
|
||||||
branch_offset = bf[0:6]
|
|
||||||
else:
|
|
||||||
# We need to do a little magic here. The branch offset is
|
|
||||||
# written as a signed 14-bit number, with signed meaning '-n' is
|
|
||||||
# written as '65536-n'. Or in this case, as we have 14 bits,
|
|
||||||
# '16384-n'.
|
|
||||||
#
|
|
||||||
# So, if the MSB (ie. bit 13) is set, we have a negative
|
|
||||||
# number. We take the value, and substract 16384 to get the
|
|
||||||
# actual offset as a negative integer.
|
|
||||||
#
|
|
||||||
# If the MSB is not set, we just extract the value and return it.
|
|
||||||
#
|
|
||||||
# Can you spell "Weird" ?
|
|
||||||
branch_offset = self._get_pc() + (bf[0:5] << 8)
|
|
||||||
if bf[5]:
|
|
||||||
branch_offset -= 8192
|
|
||||||
|
|
||||||
log('Branch if %s to offset %+d' % (branch_if_true, branch_offset))
|
|
||||||
return branch_if_true, branch_offset
|
|
||||||
|
|
|
||||||
|
|
@ -60,244 +60,231 @@ INFINITE_ROWS = 255
|
||||||
|
|
||||||
|
|
||||||
class ZScreenObserver:
|
class ZScreenObserver:
|
||||||
"""Observer that is notified of changes in the state of a ZScreen
|
"""Observer that is notified of changes in the state of a ZScreen
|
||||||
object.
|
object.
|
||||||
|
|
||||||
Note that all methods in this class may be called by any thread at
|
Note that all methods in this class may be called by any thread at
|
||||||
any time, so they should take any necessary precautions to ensure
|
any time, so they should take any necessary precautions to ensure
|
||||||
the integrity of any data they modify."""
|
the integrity of any data they modify."""
|
||||||
|
|
||||||
def on_screen_size_change(self, zscreen):
|
def on_screen_size_change(self, zscreen):
|
||||||
"""Called when the screen size of a ZScreen changes."""
|
"""Called when the screen size of a ZScreen changes."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def on_font_size_change(self, zscreen):
|
def on_font_size_change(self, zscreen):
|
||||||
"""Called when the font size of a ZScreen changes."""
|
"""Called when the font size of a ZScreen changes."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZScreen(zstream.ZBufferableOutputStream):
|
class ZScreen(zstream.ZBufferableOutputStream):
|
||||||
"""Subclass of zstream.ZBufferableOutputStream that provides an
|
"""Subclass of zstream.ZBufferableOutputStream that provides an
|
||||||
abstraction of a computer screen."""
|
abstraction of a computer screen."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"Constructor for the screen."
|
"Constructor for the screen."
|
||||||
|
|
||||||
zstream.ZBufferableOutputStream.__init__(self)
|
zstream.ZBufferableOutputStream.__init__(self)
|
||||||
|
|
||||||
# The size of the screen.
|
# The size of the screen.
|
||||||
self._columns = 79
|
self._columns = 79
|
||||||
self._rows = 24
|
self._rows = 24
|
||||||
|
|
||||||
# The size of the current font, in characters
|
# The size of the current font, in characters
|
||||||
self._fontheight = 1
|
self._fontheight = 1
|
||||||
self._fontwidth = 1
|
self._fontwidth = 1
|
||||||
|
|
||||||
# List of our observers; clients can directly append to and remove
|
# List of our observers; clients can directly append to and remove
|
||||||
# from this.
|
# from this.
|
||||||
self.observers = []
|
self.observers = []
|
||||||
|
|
||||||
# Subclasses must define real values for all the features they
|
# Subclasses must define real values for all the features they
|
||||||
# support (or don't support).
|
# support (or don't support).
|
||||||
|
|
||||||
self.features = {
|
self.features = {
|
||||||
"has_status_line" : False,
|
"has_status_line": False,
|
||||||
"has_upper_window" : False,
|
"has_upper_window": False,
|
||||||
"has_graphics_font" : False,
|
"has_graphics_font": False,
|
||||||
"has_text_colors": False,
|
"has_text_colors": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Window Management
|
# Window Management
|
||||||
#
|
#
|
||||||
# The z-machine has 2 windows for displaying text, "upper" and
|
# The z-machine has 2 windows for displaying text, "upper" and
|
||||||
# "lower". (The upper window has an inital height of 0.)
|
# "lower". (The upper window has an inital height of 0.)
|
||||||
#
|
#
|
||||||
# The upper window is not necessarily where the "status line"
|
# The upper window is not necessarily where the "status line"
|
||||||
# appears; see section 8.6.1.1 of the Z-Machine Standards Document.
|
# appears; see section 8.6.1.1 of the Z-Machine Standards Document.
|
||||||
#
|
#
|
||||||
# The UI is responsible for making the lower window scroll properly,
|
# The UI is responsible for making the lower window scroll properly,
|
||||||
# as well as wrapping words ("buffering"). The upper window,
|
# as well as wrapping words ("buffering"). The upper window,
|
||||||
# however, should *never* scroll or wrap words.
|
# however, should *never* scroll or wrap words.
|
||||||
#
|
#
|
||||||
# The UI is also responsible for displaying [MORE] prompts when
|
# The UI is also responsible for displaying [MORE] prompts when
|
||||||
# printing more text than the screen's rows can display. (Note: if
|
# printing more text than the screen's rows can display. (Note: if
|
||||||
# the number of screen rows is INFINITE_ROWS, then it should never
|
# the number of screen rows is INFINITE_ROWS, then it should never
|
||||||
# prompt [MORE].)
|
# prompt [MORE].)
|
||||||
|
|
||||||
def get_screen_size(self):
|
def get_screen_size(self):
|
||||||
"""Return the current size of the screen as [rows, columns]."""
|
"""Return the current size of the screen as [rows, columns]."""
|
||||||
|
|
||||||
return [self._rows, self._columns]
|
return [self._rows, self._columns]
|
||||||
|
|
||||||
|
|
||||||
def select_window(self, window):
|
def select_window(self, window):
|
||||||
"""Select a window to be the 'active' window, and move that
|
"""Select a window to be the 'active' window, and move that
|
||||||
window's cursor to the upper left.
|
window's cursor to the upper left.
|
||||||
|
|
||||||
WINDOW should be one of WINDOW_UPPER or WINDOW_LOWER.
|
WINDOW should be one of WINDOW_UPPER or WINDOW_LOWER.
|
||||||
|
|
||||||
This method should only be implemented if the
|
This method should only be implemented if the
|
||||||
has_upper_window feature is enabled."""
|
has_upper_window feature is enabled."""
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def split_window(self, height):
|
||||||
|
"""Make the upper window appear and be HEIGHT lines tall. To
|
||||||
|
'unsplit' a window, call with a height of 0 lines.
|
||||||
|
|
||||||
def split_window(self, height):
|
This method should only be implemented if the has_upper_window
|
||||||
"""Make the upper window appear and be HEIGHT lines tall. To
|
feature is enabled."""
|
||||||
'unsplit' a window, call with a height of 0 lines.
|
|
||||||
|
|
||||||
This method should only be implemented if the has_upper_window
|
raise NotImplementedError()
|
||||||
feature is enabled."""
|
|
||||||
|
|
||||||
raise NotImplementedError()
|
def set_cursor_position(self, x, y):
|
||||||
|
"""Set the cursor to (row, column) coordinates (X,Y) in the
|
||||||
|
current window, where (1,1) is the upper-left corner.
|
||||||
|
|
||||||
|
This function only does something if the current window is the
|
||||||
|
upper window; if the current window is the lower window, this
|
||||||
|
function has no effect.
|
||||||
|
|
||||||
def set_cursor_position(self, x, y):
|
This method should only be implemented if the has_upper_window
|
||||||
"""Set the cursor to (row, column) coordinates (X,Y) in the
|
feature is enabled, as the upper window is the only window that
|
||||||
current window, where (1,1) is the upper-left corner.
|
supports cursor positioning."""
|
||||||
|
|
||||||
This function only does something if the current window is the
|
raise NotImplementedError()
|
||||||
upper window; if the current window is the lower window, this
|
|
||||||
function has no effect.
|
|
||||||
|
|
||||||
This method should only be implemented if the has_upper_window
|
|
||||||
feature is enabled, as the upper window is the only window that
|
|
||||||
supports cursor positioning."""
|
|
||||||
|
|
||||||
raise NotImplementedError()
|
def erase_window(self, window=WINDOW_LOWER, color=COLOR_CURRENT):
|
||||||
|
"""Erase WINDOW to background COLOR.
|
||||||
|
|
||||||
|
WINDOW should be one of WINDOW_UPPER or WINDOW_LOWER.
|
||||||
|
|
||||||
def erase_window(self, window=WINDOW_LOWER,
|
If the has_upper_window feature is not supported, WINDOW is
|
||||||
color=COLOR_CURRENT):
|
ignored (in such a case, this function should clear the entire
|
||||||
"""Erase WINDOW to background COLOR.
|
screen).
|
||||||
|
|
||||||
WINDOW should be one of WINDOW_UPPER or WINDOW_LOWER.
|
COLOR should be one of the COLOR_* constants.
|
||||||
|
|
||||||
If the has_upper_window feature is not supported, WINDOW is
|
If the has_text_colors feature is not supported, COLOR is ignored."""
|
||||||
ignored (in such a case, this function should clear the entire
|
|
||||||
screen).
|
|
||||||
|
|
||||||
COLOR should be one of the COLOR_* constants.
|
raise NotImplementedError()
|
||||||
|
|
||||||
If the has_text_colors feature is not supported, COLOR is ignored."""
|
def erase_line(self):
|
||||||
|
"""Erase from the current cursor position to the end of its line
|
||||||
|
in the current window.
|
||||||
|
|
||||||
raise NotImplementedError()
|
This method should only be implemented if the has_upper_window
|
||||||
|
feature is enabled, as the upper window is the only window that
|
||||||
|
supports cursor positioning."""
|
||||||
|
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
def erase_line(self):
|
# Status Line
|
||||||
"""Erase from the current cursor position to the end of its line
|
#
|
||||||
in the current window.
|
# These routines are only called if the has_status_line capability
|
||||||
|
# is set. Specifically, one of them is called whenever the
|
||||||
|
# show_status opcode is executed, and just before input is read from
|
||||||
|
# the user.
|
||||||
|
|
||||||
This method should only be implemented if the has_upper_window
|
def print_status_score_turns(self, text, score, turns):
|
||||||
feature is enabled, as the upper window is the only window that
|
"""Print a status line in the upper window, as follows:
|
||||||
supports cursor positioning."""
|
|
||||||
|
|
||||||
raise NotImplementedError()
|
On the left side of the status line, print TEXT.
|
||||||
|
On the right side of the status line, print SCORE/TURNS.
|
||||||
|
|
||||||
|
This method should only be implemented if the has_status_line
|
||||||
|
feature is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
# Status Line
|
raise NotImplementedError()
|
||||||
#
|
|
||||||
# These routines are only called if the has_status_line capability
|
|
||||||
# is set. Specifically, one of them is called whenever the
|
|
||||||
# show_status opcode is executed, and just before input is read from
|
|
||||||
# the user.
|
|
||||||
|
|
||||||
def print_status_score_turns(self, text, score, turns):
|
def print_status_time(self, hours, minutes):
|
||||||
"""Print a status line in the upper window, as follows:
|
"""Print a status line in the upper window, as follows:
|
||||||
|
|
||||||
On the left side of the status line, print TEXT.
|
On the left side of the status line, print TEXT.
|
||||||
On the right side of the status line, print SCORE/TURNS.
|
On the right side of the status line, print HOURS:MINUTES.
|
||||||
|
|
||||||
This method should only be implemented if the has_status_line
|
This method should only be implemented if the has_status_line
|
||||||
feature is enabled.
|
feature is enabled.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
# Text Appearances
|
||||||
|
#
|
||||||
|
|
||||||
def print_status_time(self, hours, minutes):
|
def get_font_size(self):
|
||||||
"""Print a status line in the upper window, as follows:
|
"""Return the current font's size as [width, height]."""
|
||||||
|
|
||||||
On the left side of the status line, print TEXT.
|
return [self._fontwidth, self._fontheight]
|
||||||
On the right side of the status line, print HOURS:MINUTES.
|
|
||||||
|
|
||||||
This method should only be implemented if the has_status_line
|
def set_font(self, font_number):
|
||||||
feature is enabled.
|
"""Set the current window's font to one of
|
||||||
"""
|
|
||||||
|
|
||||||
raise NotImplementedError()
|
FONT_NORMAL - normal font
|
||||||
|
FONT_PICTURE - picture font (IGNORE, this means nothing)
|
||||||
|
FONT_CHARACTER_GRAPHICS - character graphics font
|
||||||
|
FONT_FIXED_WIDTH - fixed-width font
|
||||||
|
|
||||||
|
If a font is not available, return None. Otherwise, set the
|
||||||
|
new font, and return the number of the *previous* font.
|
||||||
|
|
||||||
# Text Appearances
|
The only font that must be supported is FONT_NORMAL; all others
|
||||||
#
|
are optional, as per section 8.1.3 of the Z-Machine Standards
|
||||||
|
Document."""
|
||||||
|
|
||||||
def get_font_size(self):
|
raise NotImplementedError()
|
||||||
"""Return the current font's size as [width, height]."""
|
|
||||||
|
|
||||||
return [self._fontwidth, self._fontheight]
|
def set_text_style(self, style):
|
||||||
|
"""Set the current text style to the given text style.
|
||||||
|
|
||||||
|
STYLE is a sequence, each element of which should be one of the
|
||||||
|
following values:
|
||||||
|
|
||||||
def set_font(self, font_number):
|
STYLE_ROMAN - Roman
|
||||||
"""Set the current window's font to one of
|
STYLE_REVERSE_VIDEO - Reverse video
|
||||||
|
STYLE_BOLD - Bold
|
||||||
|
STYLE_ITALIC - Italic
|
||||||
|
STYLE_FIXED_PITCH - Fixed-width
|
||||||
|
|
||||||
FONT_NORMAL - normal font
|
It is not a requirement that the screen implementation support
|
||||||
FONT_PICTURE - picture font (IGNORE, this means nothing)
|
every combination of style; if no combinations are possible, it is
|
||||||
FONT_CHARACTER_GRAPHICS - character graphics font
|
acceptable to simply use the first style in the sequence and ignore
|
||||||
FONT_FIXED_WIDTH - fixed-width font
|
the rest.
|
||||||
|
|
||||||
If a font is not available, return None. Otherwise, set the
|
As per section 8.7.1.1 of the Z-Machine Standards Document, the
|
||||||
new font, and return the number of the *previous* font.
|
implementation need not provide bold or italic, and is free to
|
||||||
|
interpret them broadly.
|
||||||
|
"""
|
||||||
|
|
||||||
The only font that must be supported is FONT_NORMAL; all others
|
raise NotImplementedError()
|
||||||
are optional, as per section 8.1.3 of the Z-Machine Standards
|
|
||||||
Document."""
|
|
||||||
|
|
||||||
raise NotImplementedError()
|
def set_text_color(self, foreground_color, background_color):
|
||||||
|
"""Set current text foreground and background color. Each color
|
||||||
|
should correspond to one of the COLOR_* constants.
|
||||||
|
|
||||||
|
This method should only be implemented if the has_text_colors
|
||||||
|
feature is enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
def set_text_style(self, style):
|
raise NotImplementedError()
|
||||||
"""Set the current text style to the given text style.
|
|
||||||
|
|
||||||
STYLE is a sequence, each element of which should be one of the
|
# Standard output
|
||||||
following values:
|
|
||||||
|
|
||||||
STYLE_ROMAN - Roman
|
def write(self, string):
|
||||||
STYLE_REVERSE_VIDEO - Reverse video
|
"""Implementation of the ZOutputStream method. Prints the given
|
||||||
STYLE_BOLD - Bold
|
unicode string to the currently active window, using the current
|
||||||
STYLE_ITALIC - Italic
|
text style settings."""
|
||||||
STYLE_FIXED_PITCH - Fixed-width
|
|
||||||
|
|
||||||
It is not a requirement that the screen implementation support
|
raise NotImplementedError()
|
||||||
every combination of style; if no combinations are possible, it is
|
|
||||||
acceptable to simply use the first style in the sequence and ignore
|
|
||||||
the rest.
|
|
||||||
|
|
||||||
As per section 8.7.1.1 of the Z-Machine Standards Document, the
|
|
||||||
implementation need not provide bold or italic, and is free to
|
|
||||||
interpret them broadly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
def set_text_color(self, foreground_color, background_color):
|
|
||||||
"""Set current text foreground and background color. Each color
|
|
||||||
should correspond to one of the COLOR_* constants.
|
|
||||||
|
|
||||||
This method should only be implemented if the has_text_colors
|
|
||||||
feature is enabled.
|
|
||||||
"""
|
|
||||||
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
# Standard output
|
|
||||||
|
|
||||||
def write(self, string):
|
|
||||||
"""Implementation of the ZOutputStream method. Prints the given
|
|
||||||
unicode string to the currently active window, using the current
|
|
||||||
text style settings."""
|
|
||||||
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
|
||||||
|
|
@ -12,193 +12,189 @@ from .zlogging import log
|
||||||
|
|
||||||
|
|
||||||
class ZStackError(Exception):
|
class ZStackError(Exception):
|
||||||
"General exception for stack or routine-related errors"
|
"General exception for stack or routine-related errors"
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZStackUnsupportedVersion(ZStackError):
|
class ZStackUnsupportedVersion(ZStackError):
|
||||||
"Unsupported version of Z-story file."
|
"Unsupported version of Z-story file."
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZStackNoRoutine(ZStackError):
|
class ZStackNoRoutine(ZStackError):
|
||||||
"No routine is being executed."
|
"No routine is being executed."
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZStackNoSuchVariable(ZStackError):
|
class ZStackNoSuchVariable(ZStackError):
|
||||||
"Trying to access non-existent local variable."
|
"Trying to access non-existent local variable."
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ZStackPopError(ZStackError):
|
class ZStackPopError(ZStackError):
|
||||||
"Nothing to pop from stack!"
|
"Nothing to pop from stack!"
|
||||||
pass
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Helper class used by ZStackManager; a 'routine' object which
|
# Helper class used by ZStackManager; a 'routine' object which
|
||||||
# includes its own private stack of data.
|
# includes its own private stack of data.
|
||||||
class ZRoutine:
|
class ZRoutine:
|
||||||
|
def __init__(
|
||||||
|
self, start_addr, return_addr, zmem, args, local_vars=None, stack=None
|
||||||
|
):
|
||||||
|
"""Initialize a routine object beginning at START_ADDR in ZMEM,
|
||||||
|
with initial argument values in list ARGS. If LOCAL_VARS is None,
|
||||||
|
then parse them from START_ADDR."""
|
||||||
|
|
||||||
def __init__(self, start_addr, return_addr, zmem, args,
|
self.start_addr = start_addr
|
||||||
local_vars=None, stack=None):
|
self.return_addr = return_addr
|
||||||
"""Initialize a routine object beginning at START_ADDR in ZMEM,
|
self.program_counter = 0 # used when execution interrupted
|
||||||
with initial argument values in list ARGS. If LOCAL_VARS is None,
|
|
||||||
then parse them from START_ADDR."""
|
|
||||||
|
|
||||||
self.start_addr = start_addr
|
if stack is None:
|
||||||
self.return_addr = return_addr
|
self.stack = []
|
||||||
self.program_counter = 0 # used when execution interrupted
|
else:
|
||||||
|
self.stack = stack[:]
|
||||||
|
|
||||||
if stack is None:
|
if local_vars is not None:
|
||||||
self.stack = []
|
self.local_vars = local_vars[:]
|
||||||
else:
|
else:
|
||||||
self.stack = stack[:]
|
num_local_vars = zmem[self.start_addr]
|
||||||
|
if not (0 <= num_local_vars <= 15):
|
||||||
|
log(f"num local vars is {num_local_vars}")
|
||||||
|
raise ZStackError
|
||||||
|
self.start_addr += 1
|
||||||
|
|
||||||
if local_vars is not None:
|
# Initialize the local vars in the ZRoutine's dictionary. This is
|
||||||
self.local_vars = local_vars[:]
|
# only needed on machines v1 through v4. In v5 machines, all local
|
||||||
else:
|
# variables are preinitialized to zero.
|
||||||
num_local_vars = zmem[self.start_addr]
|
self.local_vars = [0 for _ in range(15)]
|
||||||
if not (0 <= num_local_vars <= 15):
|
if 1 <= zmem.version <= 4:
|
||||||
log("num local vars is %d" % num_local_vars)
|
for i in range(num_local_vars):
|
||||||
raise ZStackError
|
self.local_vars[i] = zmem.read_word(self.start_addr)
|
||||||
self.start_addr += 1
|
self.start_addr += 2
|
||||||
|
elif zmem.version != 5:
|
||||||
|
raise ZStackUnsupportedVersion
|
||||||
|
|
||||||
# Initialize the local vars in the ZRoutine's dictionary. This is
|
# Place call arguments into local vars, if available
|
||||||
# only needed on machines v1 through v4. In v5 machines, all local
|
for i in range(0, len(args)):
|
||||||
# variables are preinitialized to zero.
|
self.local_vars[i] = args[i]
|
||||||
self.local_vars = [0 for _ in range(15)]
|
|
||||||
if 1 <= zmem.version <= 4:
|
|
||||||
for i in range(num_local_vars):
|
|
||||||
self.local_vars[i] = zmem.read_word(self.start_addr)
|
|
||||||
self.start_addr += 2
|
|
||||||
elif zmem.version != 5:
|
|
||||||
raise ZStackUnsupportedVersion
|
|
||||||
|
|
||||||
# Place call arguments into local vars, if available
|
def pretty_print(self):
|
||||||
for i in range(0, len(args)):
|
"Display a ZRoutine nicely, for debugging purposes."
|
||||||
self.local_vars[i] = args[i]
|
|
||||||
|
|
||||||
|
log(f"ZRoutine: start address: {self.start_addr}")
|
||||||
def pretty_print(self):
|
log(f"ZRoutine: return value address: {self.return_addr}")
|
||||||
"Display a ZRoutine nicely, for debugging purposes."
|
log(f"ZRoutine: program counter: {self.program_counter}")
|
||||||
|
log(f"ZRoutine: local variables: {self.local_vars}")
|
||||||
log("ZRoutine: start address: %d" % self.start_addr)
|
|
||||||
log("ZRoutine: return value address: %d" % self.return_addr)
|
|
||||||
log("ZRoutine: program counter: %d" % self.program_counter)
|
|
||||||
log("ZRoutine: local variables: %d" % self.local_vars)
|
|
||||||
|
|
||||||
|
|
||||||
class ZStackBottom:
|
class ZStackBottom:
|
||||||
|
def __init__(self):
|
||||||
def __init__(self):
|
self.program_counter = 0 # used as a cache only
|
||||||
self.program_counter = 0 # used as a cache only
|
|
||||||
|
|
||||||
|
|
||||||
class ZStackManager:
|
class ZStackManager:
|
||||||
|
def __init__(self, zmem):
|
||||||
|
self._memory = zmem
|
||||||
|
self._stackbottom = ZStackBottom()
|
||||||
|
self._call_stack = [self._stackbottom]
|
||||||
|
|
||||||
def __init__(self, zmem):
|
def get_local_variable(self, varnum):
|
||||||
|
"""Return value of local variable VARNUM from currently-running
|
||||||
|
routine. VARNUM must be a value between 0 and 15, and must
|
||||||
|
exist."""
|
||||||
|
|
||||||
self._memory = zmem
|
if self._call_stack[-1] == self._stackbottom:
|
||||||
self._stackbottom = ZStackBottom()
|
raise ZStackNoRoutine
|
||||||
self._call_stack = [self._stackbottom]
|
|
||||||
|
|
||||||
|
if not 0 <= varnum <= 15:
|
||||||
|
raise ZStackNoSuchVariable
|
||||||
|
|
||||||
def get_local_variable(self, varnum):
|
current_routine = self._call_stack[-1]
|
||||||
"""Return value of local variable VARNUM from currently-running
|
|
||||||
routine. VARNUM must be a value between 0 and 15, and must
|
|
||||||
exist."""
|
|
||||||
|
|
||||||
if self._call_stack[-1] == self._stackbottom:
|
return current_routine.local_vars[varnum] # type: ignore[possibly-missing-attribute]
|
||||||
raise ZStackNoRoutine
|
|
||||||
|
|
||||||
if not 0 <= varnum <= 15:
|
def set_local_variable(self, varnum, value):
|
||||||
raise ZStackNoSuchVariable
|
"""Set value of local variable VARNUM to VALUE in
|
||||||
|
currently-running routine. VARNUM must be a value between 0 and
|
||||||
|
15, and must exist."""
|
||||||
|
|
||||||
current_routine = self._call_stack[-1]
|
if self._call_stack[1] == self._stackbottom:
|
||||||
|
raise ZStackNoRoutine
|
||||||
|
|
||||||
return current_routine.local_vars[varnum]
|
if not 0 <= varnum <= 15:
|
||||||
|
raise ZStackNoSuchVariable
|
||||||
|
|
||||||
|
current_routine = self._call_stack[-1]
|
||||||
|
|
||||||
def set_local_variable(self, varnum, value):
|
current_routine.local_vars[varnum] = value # type: ignore[possibly-missing-attribute]
|
||||||
"""Set value of local variable VARNUM to VALUE in
|
|
||||||
currently-running routine. VARNUM must be a value between 0 and
|
|
||||||
15, and must exist."""
|
|
||||||
|
|
||||||
if self._call_stack[1] == self._stackbottom:
|
def push_stack(self, value):
|
||||||
raise ZStackNoRoutine
|
"Push VALUE onto the top of the current routine's data stack."
|
||||||
|
|
||||||
if not 0 <= varnum <= 15:
|
current_routine = self._call_stack[-1]
|
||||||
raise ZStackNoSuchVariable
|
current_routine.stack.append(value) # type: ignore[possibly-missing-attribute]
|
||||||
|
|
||||||
current_routine = self._call_stack[-1]
|
def pop_stack(self):
|
||||||
|
"Remove and return value from the top of the data stack."
|
||||||
|
|
||||||
current_routine.local_vars[varnum] = value
|
current_routine = self._call_stack[-1]
|
||||||
|
return current_routine.stack.pop() # type: ignore[possibly-missing-attribute]
|
||||||
|
|
||||||
|
def get_stack_frame_index(self):
|
||||||
|
"Return current stack frame number. For use by 'catch' opcode."
|
||||||
|
|
||||||
def push_stack(self, value):
|
return len(self._call_stack) - 1
|
||||||
"Push VALUE onto the top of the current routine's data stack."
|
|
||||||
|
|
||||||
current_routine = self._call_stack[-1]
|
# Used by quetzal save-file parser to reconstruct stack-frames.
|
||||||
current_routine.stack.append(value)
|
def push_routine(self, routine):
|
||||||
|
"""Blindly push a ZRoutine object to the call stack.
|
||||||
|
WARNING: do not use this unless you know what you're doing; you
|
||||||
|
probably want the more full-featured start_routine() belowe
|
||||||
|
instead."""
|
||||||
|
|
||||||
|
self._call_stack.append(routine)
|
||||||
|
|
||||||
def pop_stack(self):
|
# ZPU should call this whenever it decides to call a new routine.
|
||||||
"Remove and return value from the top of the data stack."
|
def start_routine(self, routine_addr, return_addr, program_counter, args):
|
||||||
|
"""Save the state of the currenly running routine (by examining
|
||||||
|
the current value of the PROGRAM_COUNTER), and prepare for
|
||||||
|
execution of a new routine at ROUTINE_ADDR with list of initial
|
||||||
|
arguments ARGS."""
|
||||||
|
|
||||||
current_routine = self._call_stack[-1]
|
new_routine = ZRoutine(routine_addr, return_addr, self._memory, args)
|
||||||
return current_routine.stack.pop()
|
current_routine = self._call_stack[-1]
|
||||||
|
current_routine.program_counter = program_counter
|
||||||
|
self._call_stack.append(new_routine)
|
||||||
|
|
||||||
|
return new_routine.start_addr
|
||||||
|
|
||||||
def get_stack_frame_index(self):
|
# ZPU should call this whenever it decides to return from current
|
||||||
"Return current stack frame number. For use by 'catch' opcode."
|
# routine.
|
||||||
|
def finish_routine(self, return_value):
|
||||||
|
"""Toss the currently running routine from the call stack, and
|
||||||
|
toss any leftover values pushed to the data stack by said routine.
|
||||||
|
Return the previous routine's program counter address, so that
|
||||||
|
execution can resume where from it left off."""
|
||||||
|
|
||||||
return len(self._call_stack) - 1
|
exiting_routine = self._call_stack.pop()
|
||||||
|
current_routine = self._call_stack[-1]
|
||||||
|
|
||||||
|
# Depending on many things, return stuff.
|
||||||
|
if exiting_routine.return_addr is not None: # type: ignore[possibly-missing-attribute]
|
||||||
|
if exiting_routine.return_addr == 0: # type: ignore[possibly-missing-attribute]
|
||||||
|
# Push to stack
|
||||||
|
self.push_stack(return_value)
|
||||||
|
elif 0 < exiting_routine.return_addr < 10: # type: ignore[possibly-missing-attribute]
|
||||||
|
# Store in local var
|
||||||
|
self.set_local_variable(exiting_routine.return_addr, return_value) # type: ignore[possibly-missing-attribute]
|
||||||
|
else:
|
||||||
|
# Store in global var
|
||||||
|
self._memory.write_global(exiting_routine.return_addr, return_value) # type: ignore[possibly-missing-attribute]
|
||||||
|
|
||||||
# Used by quetzal save-file parser to reconstruct stack-frames.
|
return current_routine.program_counter
|
||||||
def push_routine(self, routine):
|
|
||||||
"""Blindly push a ZRoutine object to the call stack.
|
|
||||||
WARNING: do not use this unless you know what you're doing; you
|
|
||||||
probably want the more full-featured start_routine() belowe
|
|
||||||
instead."""
|
|
||||||
|
|
||||||
self._call_stack.append(routine)
|
|
||||||
|
|
||||||
|
|
||||||
# ZPU should call this whenever it decides to call a new routine.
|
|
||||||
def start_routine(self, routine_addr, return_addr,
|
|
||||||
program_counter, args):
|
|
||||||
"""Save the state of the currenly running routine (by examining
|
|
||||||
the current value of the PROGRAM_COUNTER), and prepare for
|
|
||||||
execution of a new routine at ROUTINE_ADDR with list of initial
|
|
||||||
arguments ARGS."""
|
|
||||||
|
|
||||||
new_routine = ZRoutine(routine_addr, return_addr,
|
|
||||||
self._memory, args)
|
|
||||||
current_routine = self._call_stack[-1]
|
|
||||||
current_routine.program_counter = program_counter
|
|
||||||
self._call_stack.append(new_routine)
|
|
||||||
|
|
||||||
return new_routine.start_addr
|
|
||||||
|
|
||||||
|
|
||||||
# ZPU should call this whenever it decides to return from current
|
|
||||||
# routine.
|
|
||||||
def finish_routine(self, return_value):
|
|
||||||
"""Toss the currently running routine from the call stack, and
|
|
||||||
toss any leftover values pushed to the data stack by said routine.
|
|
||||||
Return the previous routine's program counter address, so that
|
|
||||||
execution can resume where from it left off."""
|
|
||||||
|
|
||||||
exiting_routine = self._call_stack.pop()
|
|
||||||
current_routine = self._call_stack[-1]
|
|
||||||
|
|
||||||
# Depending on many things, return stuff.
|
|
||||||
if exiting_routine.return_addr != None:
|
|
||||||
if exiting_routine.return_addr == 0: # Push to stack
|
|
||||||
self.push_stack(return_value)
|
|
||||||
elif 0 < exiting_routine.return_addr < 10: # Store in local var
|
|
||||||
self.set_local_variable(exiting_routine.return_addr,
|
|
||||||
return_value)
|
|
||||||
else: # Store in global var
|
|
||||||
self._memory.write_global(exiting_routine.return_addr,
|
|
||||||
return_value)
|
|
||||||
|
|
||||||
return current_routine.program_counter
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,94 +5,99 @@
|
||||||
# root directory of this distribution.
|
# root directory of this distribution.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class ZOutputStream:
|
class ZOutputStream:
|
||||||
"""Abstract class representing an output stream for a z-machine."""
|
"""Abstract class representing an output stream for a z-machine."""
|
||||||
|
|
||||||
def write(self, string):
|
def write(self, string):
|
||||||
"""Prints the given unicode string to the output stream."""
|
"""Prints the given unicode string to the output stream."""
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
class ZBufferableOutputStream(ZOutputStream):
|
class ZBufferableOutputStream(ZOutputStream):
|
||||||
"""Abstract class representing a buffered output stream for a
|
"""Abstract class representing a buffered output stream for a
|
||||||
z-machine, which can be optionally configured at run-time to provide
|
z-machine, which can be optionally configured at run-time to provide
|
||||||
'buffering', also known as word-wrap."""
|
'buffering', also known as word-wrap."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# This is a public variable that determines whether buffering is
|
# This is a public variable that determines whether buffering is
|
||||||
# enabled for this stream or not. Subclasses can make it a
|
# enabled for this stream or not. Subclasses can make it a
|
||||||
# Python property if necessary.
|
# Python property if necessary.
|
||||||
self.buffer_mode = False
|
self.buffer_mode = False
|
||||||
|
|
||||||
|
|
||||||
class ZInputStream:
|
class ZInputStream:
|
||||||
"""Abstract class representing an input stream for a z-machine."""
|
"""Abstract class representing an input stream for a z-machine."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Constructor for the input stream."""
|
"""Constructor for the input stream."""
|
||||||
# Subclasses must define real values for all the features they
|
# Subclasses must define real values for all the features they
|
||||||
# support (or don't support).
|
# support (or don't support).
|
||||||
|
|
||||||
self.features = {
|
self.features = {
|
||||||
"has_timed_input" : False,
|
"has_timed_input": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
def read_line(self, original_text=None, max_length=0,
|
def read_line(
|
||||||
terminating_characters=None,
|
self,
|
||||||
timed_input_routine=None, timed_input_interval=0):
|
original_text=None,
|
||||||
"""Reads from the input stream and returns a unicode string
|
max_length=0,
|
||||||
representing the characters the end-user entered. The characters
|
terminating_characters=None,
|
||||||
are displayed to the screen as the user types them.
|
timed_input_routine=None,
|
||||||
|
timed_input_interval=0,
|
||||||
|
):
|
||||||
|
"""Reads from the input stream and returns a unicode string
|
||||||
|
representing the characters the end-user entered. The characters
|
||||||
|
are displayed to the screen as the user types them.
|
||||||
|
|
||||||
original_text, if provided, is pre-filled-in unicode text that the
|
original_text, if provided, is pre-filled-in unicode text that the
|
||||||
end-user may delete or otherwise modify if they so choose.
|
end-user may delete or otherwise modify if they so choose.
|
||||||
|
|
||||||
max_length is the maximum length, in characters, of the text that
|
max_length is the maximum length, in characters, of the text that
|
||||||
the end-user may enter. Any typing the end-user does after these
|
the end-user may enter. Any typing the end-user does after these
|
||||||
many characters have been entered is ignored. 0 means that there
|
many characters have been entered is ignored. 0 means that there
|
||||||
is no practical limit to the number of characters the end-user can
|
is no practical limit to the number of characters the end-user can
|
||||||
enter.
|
enter.
|
||||||
|
|
||||||
terminating_characters is a string of unicode characters
|
terminating_characters is a string of unicode characters
|
||||||
representing the characters that can signify the end of a line of
|
representing the characters that can signify the end of a line of
|
||||||
input. If not provided, it defaults to a string containing a
|
input. If not provided, it defaults to a string containing a
|
||||||
carriage return character ('\r'). The terminating character is
|
carriage return character ('\r'). The terminating character is
|
||||||
not contained in the returned string.
|
not contained in the returned string.
|
||||||
|
|
||||||
timed_input_routine is a function that will be called every
|
timed_input_routine is a function that will be called every
|
||||||
time_input_interval milliseconds. This function should be of the
|
time_input_interval milliseconds. This function should be of the
|
||||||
form:
|
form:
|
||||||
|
|
||||||
def timed_input_routine(interval)
|
def timed_input_routine(interval)
|
||||||
|
|
||||||
where interval is simply the value of timed_input_interval that
|
where interval is simply the value of timed_input_interval that
|
||||||
was passed in to read_line(). The function should also return
|
was passed in to read_line(). The function should also return
|
||||||
True if input should continue to be collected, or False if input
|
True if input should continue to be collected, or False if input
|
||||||
should stop being collected; if False is returned, then
|
should stop being collected; if False is returned, then
|
||||||
read_line() will return a unicode string representing the
|
read_line() will return a unicode string representing the
|
||||||
characters typed so far.
|
characters typed so far.
|
||||||
|
|
||||||
The timed input routine will be called from the same thread that
|
The timed input routine will be called from the same thread that
|
||||||
called read_line().
|
called read_line().
|
||||||
|
|
||||||
Note, however, that supplying a timed input routine is only useful
|
Note, however, that supplying a timed input routine is only useful
|
||||||
if the has_timed_input feature is supported by the input stream.
|
if the has_timed_input feature is supported by the input stream.
|
||||||
If it is unsupported, then the timed input routine will not be
|
If it is unsupported, then the timed input routine will not be
|
||||||
called."""
|
called."""
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def read_char(self, timed_input_routine=None,
|
def read_char(self, timed_input_routine=None, timed_input_interval=0):
|
||||||
timed_input_interval=0):
|
"""Reads a single character from the stream and returns it as a
|
||||||
"""Reads a single character from the stream and returns it as a
|
unicode character.
|
||||||
unicode character.
|
|
||||||
|
|
||||||
timed_input_routine and timed_input_interval are the same as
|
timed_input_routine and timed_input_interval are the same as
|
||||||
described in the documentation for read_line().
|
described in the documentation for read_line().
|
||||||
|
|
||||||
TODO: Should the character be automatically printed to the screen?
|
TODO: Should the character be automatically printed to the screen?
|
||||||
The Z-Machine documentation for the read_char opcode, which this
|
The Z-Machine documentation for the read_char opcode, which this
|
||||||
function is meant to ultimately implement, doesn't specify."""
|
function is meant to ultimately implement, doesn't specify."""
|
||||||
|
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,9 @@
|
||||||
# Constants for output streams. These are human-readable names for
|
# Constants for output streams. These are human-readable names for
|
||||||
# the stream ID numbers as described in sections 7.1.1 and 7.1.2
|
# the stream ID numbers as described in sections 7.1.1 and 7.1.2
|
||||||
# of the Z-Machine Standards Document.
|
# of the Z-Machine Standards Document.
|
||||||
OUTPUT_SCREEN = 1 # spews text to the the screen
|
OUTPUT_SCREEN = 1 # spews text to the the screen
|
||||||
OUTPUT_TRANSCRIPT = 2 # contains everything player typed, plus our responses
|
OUTPUT_TRANSCRIPT = 2 # contains everything player typed, plus our responses
|
||||||
OUTPUT_MEMORY = 3 # if the z-machine wants to write to memory
|
OUTPUT_MEMORY = 3 # if the z-machine wants to write to memory
|
||||||
OUTPUT_PLAYER_INPUT = 4 # contains *only* the player's typed commands
|
OUTPUT_PLAYER_INPUT = 4 # contains *only* the player's typed commands
|
||||||
|
|
||||||
# Constants for input streams. These are human-readable names for the
|
# Constants for input streams. These are human-readable names for the
|
||||||
|
|
@ -20,78 +20,81 @@ OUTPUT_PLAYER_INPUT = 4 # contains *only* the player's typed commands
|
||||||
INPUT_KEYBOARD = 0
|
INPUT_KEYBOARD = 0
|
||||||
INPUT_FILE = 1
|
INPUT_FILE = 1
|
||||||
|
|
||||||
|
|
||||||
class ZOutputStreamManager:
|
class ZOutputStreamManager:
|
||||||
"""Manages output streams for a Z-Machine."""
|
"""Manages output streams for a Z-Machine."""
|
||||||
|
|
||||||
def __init__(self, zmem, zui):
|
def __init__(self, zmem, zui):
|
||||||
# TODO: Actually set/create the streams as necessary.
|
# TODO: Actually set/create the streams as necessary.
|
||||||
|
|
||||||
self._selectedStreams = []
|
self._selectedStreams = []
|
||||||
self._streams = {}
|
self._streams = {}
|
||||||
|
|
||||||
def select(self, stream):
|
def select(self, stream):
|
||||||
"""Selects the given stream ID for output."""
|
"""Selects the given stream ID for output."""
|
||||||
|
|
||||||
if stream not in self._selectedStreams:
|
if stream not in self._selectedStreams:
|
||||||
self._selectedStreams.append(stream)
|
self._selectedStreams.append(stream)
|
||||||
|
|
||||||
def unselect(self, stream):
|
def unselect(self, stream):
|
||||||
"""Unselects the given stream ID for output."""
|
"""Unselects the given stream ID for output."""
|
||||||
|
|
||||||
if stream in self._selectedStreams:
|
if stream in self._selectedStreams:
|
||||||
self._selectedStreams.remove(stream)
|
self._selectedStreams.remove(stream)
|
||||||
|
|
||||||
def get(self, stream):
|
def get(self, stream):
|
||||||
"""Retrieves the given stream ID."""
|
"""Retrieves the given stream ID."""
|
||||||
|
|
||||||
return self._streams[stream]
|
return self._streams[stream]
|
||||||
|
|
||||||
def write(self, string):
|
def write(self, string):
|
||||||
"""Writes the given unicode string to all currently selected output
|
"""Writes the given unicode string to all currently selected output
|
||||||
streams."""
|
streams."""
|
||||||
|
|
||||||
# TODO: Implement section 7.1.2.2 of the Z-Machine Standards
|
# TODO: Implement section 7.1.2.2 of the Z-Machine Standards
|
||||||
# Document, so that while stream 3 is selected, no text is
|
# Document, so that while stream 3 is selected, no text is
|
||||||
# sent to any other output streams which are selected. (However,
|
# sent to any other output streams which are selected. (However,
|
||||||
# they remain selected.).
|
# they remain selected.).
|
||||||
|
|
||||||
# TODO: Implement section 7.1.2.2.1, so that newlines are written to
|
# TODO: Implement section 7.1.2.2.1, so that newlines are written to
|
||||||
# output stream 3 as ZSCII 13.
|
# output stream 3 as ZSCII 13.
|
||||||
|
|
||||||
# TODO: Implement section 7.1.2.3, so that whiles stream 4 is
|
# TODO: Implement section 7.1.2.3, so that whiles stream 4 is
|
||||||
# selected, the only text printed to it is that of the player's
|
# selected, the only text printed to it is that of the player's
|
||||||
# commands and keypresses (as read by read_char). This may not
|
# commands and keypresses (as read by read_char). This may not
|
||||||
# ultimately happen via this method.
|
# ultimately happen via this method.
|
||||||
|
|
||||||
|
for stream in self._selectedStreams:
|
||||||
|
self._streams[stream].write(string)
|
||||||
|
|
||||||
for stream in self._selectedStreams:
|
|
||||||
self._streams[stream].write(string)
|
|
||||||
|
|
||||||
class ZInputStreamManager:
|
class ZInputStreamManager:
|
||||||
"""Manages input streams for a Z-Machine."""
|
"""Manages input streams for a Z-Machine."""
|
||||||
|
|
||||||
def __init__(self, zui):
|
def __init__(self, zui):
|
||||||
# TODO: Actually set/create the streams as necessary.
|
# TODO: Actually set/create the streams as necessary.
|
||||||
|
|
||||||
self._selectedStream = None
|
self._selectedStream = None
|
||||||
self._streams = {}
|
self._streams = {}
|
||||||
|
|
||||||
def select(self, stream):
|
def select(self, stream):
|
||||||
"""Selects the given stream ID as the currently active input stream."""
|
"""Selects the given stream ID as the currently active input stream."""
|
||||||
|
|
||||||
# TODO: Ensure section 10.2.4, so that while stream 1 is selected,
|
# TODO: Ensure section 10.2.4, so that while stream 1 is selected,
|
||||||
# the only text printed to it is that of the player's commands and
|
# the only text printed to it is that of the player's commands and
|
||||||
# keypresses (as read by read_char). Not sure where this logic
|
# keypresses (as read by read_char). Not sure where this logic
|
||||||
# will ultimately go, however.
|
# will ultimately go, however.
|
||||||
|
|
||||||
self._selectedStream = stream
|
self._selectedStream = stream
|
||||||
|
|
||||||
def getSelected(self):
|
def getSelected(self):
|
||||||
"""Returns the input stream object for the currently active input
|
"""Returns the input stream object for the currently active input
|
||||||
stream."""
|
stream."""
|
||||||
|
|
||||||
|
return self._streams[self._selectedStream]
|
||||||
|
|
||||||
return self._streams[self._selectedStream]
|
|
||||||
|
|
||||||
class ZStreamManager:
|
class ZStreamManager:
|
||||||
def __init__(self, zmem, zui):
|
def __init__(self, zmem, zui):
|
||||||
self.input = ZInputStreamManager(zui)
|
self.input = ZInputStreamManager(zui)
|
||||||
self.output = ZOutputStreamManager(zmem, zui)
|
self.output = ZOutputStreamManager(zmem, zui)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import itertools
|
||||||
class ZStringEndOfString(Exception):
|
class ZStringEndOfString(Exception):
|
||||||
"""No more data left in string."""
|
"""No more data left in string."""
|
||||||
|
|
||||||
|
|
||||||
class ZStringIllegalAbbrevInString(Exception):
|
class ZStringIllegalAbbrevInString(Exception):
|
||||||
"""String abbreviation encountered within a string in a context
|
"""String abbreviation encountered within a string in a context
|
||||||
where it is not allowed."""
|
where it is not allowed."""
|
||||||
|
|
@ -22,6 +23,7 @@ class ZStringTranslator:
|
||||||
|
|
||||||
def get(self, addr):
|
def get(self, addr):
|
||||||
from .bitfield import BitField
|
from .bitfield import BitField
|
||||||
|
|
||||||
pos = (addr, BitField(self._mem.read_word(addr)), 0)
|
pos = (addr, BitField(self._mem.read_word(addr)), 0)
|
||||||
|
|
||||||
s = []
|
s = []
|
||||||
|
|
@ -34,7 +36,7 @@ class ZStringTranslator:
|
||||||
|
|
||||||
def _read_char(self, pos):
|
def _read_char(self, pos):
|
||||||
offset = (2 - pos[2]) * 5
|
offset = (2 - pos[2]) * 5
|
||||||
return pos[1][offset:offset+5]
|
return pos[1][offset : offset + 5]
|
||||||
|
|
||||||
def _is_final(self, pos):
|
def _is_final(self, pos):
|
||||||
return pos[1][15] == 1
|
return pos[1][15] == 1
|
||||||
|
|
@ -50,16 +52,13 @@ class ZStringTranslator:
|
||||||
# Kill processing.
|
# Kill processing.
|
||||||
raise ZStringEndOfString
|
raise ZStringEndOfString
|
||||||
# Get and return the next block.
|
# Get and return the next block.
|
||||||
return (pos[0] + 2,
|
return (pos[0] + 2, BitField(self._mem.read_word(pos[0] + 2)), 0)
|
||||||
BitField(self._mem.read_word(pos[0] + 2)),
|
|
||||||
0)
|
|
||||||
|
|
||||||
# Just increment the intra-block counter.
|
# Just increment the intra-block counter.
|
||||||
return (pos[0], pos[1], offset)
|
return (pos[0], pos[1], offset)
|
||||||
|
|
||||||
|
|
||||||
class ZCharTranslator:
|
class ZCharTranslator:
|
||||||
|
|
||||||
# The default alphabet tables for ZChar translation.
|
# The default alphabet tables for ZChar translation.
|
||||||
# As the codes 0-5 are special, alphabets start with code 0x6.
|
# As the codes 0-5 are special, alphabets start with code 0x6.
|
||||||
DEFAULT_A0 = [ord(x) for x in "abcdefghijklmnopqrstuvwxyz"]
|
DEFAULT_A0 = [ord(x) for x in "abcdefghijklmnopqrstuvwxyz"]
|
||||||
|
|
@ -95,7 +94,7 @@ class ZCharTranslator:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
alph_addr = self._mem.read_word(0x34)
|
alph_addr = self._mem.read_word(0x34)
|
||||||
alphabet = self._mem[alph_addr:alph_addr+78]
|
alphabet = self._mem[alph_addr : alph_addr + 78]
|
||||||
return [alphabet[0:26], alphabet[26:52], alphabet[52:78]]
|
return [alphabet[0:26], alphabet[26:52], alphabet[52:78]]
|
||||||
|
|
||||||
def _load_abbrev_tables(self):
|
def _load_abbrev_tables(self):
|
||||||
|
|
@ -109,8 +108,7 @@ class ZCharTranslator:
|
||||||
xlator = ZStringTranslator(self._mem)
|
xlator = ZStringTranslator(self._mem)
|
||||||
|
|
||||||
def _load_subtable(num, base):
|
def _load_subtable(num, base):
|
||||||
for i,zoff in [(i,base+(num*64)+(i*2))
|
for i, zoff in [(i, base + (num * 64) + (i * 2)) for i in range(0, 32)]:
|
||||||
for i in range(0, 32)]:
|
|
||||||
zaddr = self._mem.read_word(zoff)
|
zaddr = self._mem.read_word(zoff)
|
||||||
zstr = xlator.get(self._mem.word_address(zaddr))
|
zstr = xlator.get(self._mem.word_address(zaddr))
|
||||||
zchr = self.get(zstr, allow_abbreviations=False)
|
zchr = self.get(zstr, allow_abbreviations=False)
|
||||||
|
|
@ -128,11 +126,12 @@ class ZCharTranslator:
|
||||||
"""Load the special character code handlers for the current
|
"""Load the special character code handlers for the current
|
||||||
machine version.
|
machine version.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The following three functions define the three possible
|
# The following three functions define the three possible
|
||||||
# special character code handlers.
|
# special character code handlers.
|
||||||
def newline(state):
|
def newline(state):
|
||||||
"""Append ZSCII 13 (newline) to the output."""
|
"""Append ZSCII 13 (newline) to the output."""
|
||||||
state['zscii'].append(13)
|
state["zscii"].append(13)
|
||||||
|
|
||||||
def shift_alphabet(state, direction, lock):
|
def shift_alphabet(state, direction, lock):
|
||||||
"""Shift the current alphaber up or down. If lock is
|
"""Shift the current alphaber up or down. If lock is
|
||||||
|
|
@ -140,9 +139,9 @@ class ZCharTranslator:
|
||||||
after outputting 1 character. Else, the alphabet will
|
after outputting 1 character. Else, the alphabet will
|
||||||
remain unchanged until the next shift.
|
remain unchanged until the next shift.
|
||||||
"""
|
"""
|
||||||
state['curr_alpha'] = (state['curr_alpha'] + direction) % 3
|
state["curr_alpha"] = (state["curr_alpha"] + direction) % 3
|
||||||
if lock:
|
if lock:
|
||||||
state['prev_alpha'] = state['curr_alpha']
|
state["prev_alpha"] = state["curr_alpha"]
|
||||||
|
|
||||||
def abbreviation(state, abbrev):
|
def abbreviation(state, abbrev):
|
||||||
"""Insert the given abbreviation from the given table into
|
"""Insert the given abbreviation from the given table into
|
||||||
|
|
@ -152,18 +151,18 @@ class ZCharTranslator:
|
||||||
character will be the offset within that table of the
|
character will be the offset within that table of the
|
||||||
abbreviation. Set up a state handler to intercept the next
|
abbreviation. Set up a state handler to intercept the next
|
||||||
character and output the right abbreviation."""
|
character and output the right abbreviation."""
|
||||||
|
|
||||||
def write_abbreviation(state, c, subtable):
|
def write_abbreviation(state, c, subtable):
|
||||||
state['zscii'] += self._abbrevs[(subtable, c)]
|
state["zscii"] += self._abbrevs[(subtable, c)]
|
||||||
del state['state_handler']
|
del state["state_handler"]
|
||||||
|
|
||||||
# If we're parsing an abbreviation, there should be no
|
# If we're parsing an abbreviation, there should be no
|
||||||
# nested abbreviations. So this is just a sanity check for
|
# nested abbreviations. So this is just a sanity check for
|
||||||
# people feeding us bad stories.
|
# people feeding us bad stories.
|
||||||
if not state['allow_abbreviations']:
|
if not state["allow_abbreviations"]:
|
||||||
raise ZStringIllegalAbbrevInString
|
raise ZStringIllegalAbbrevInString
|
||||||
|
|
||||||
state['state_handler'] = lambda s,c: write_abbreviation(s, c,
|
state["state_handler"] = lambda s, c: write_abbreviation(s, c, abbrev)
|
||||||
abbrev)
|
|
||||||
|
|
||||||
# Register the specials handlers depending on machine version.
|
# Register the specials handlers depending on machine version.
|
||||||
if self._mem.version == 1:
|
if self._mem.version == 1:
|
||||||
|
|
@ -173,7 +172,7 @@ class ZCharTranslator:
|
||||||
3: lambda s: shift_alphabet(s, -1, False),
|
3: lambda s: shift_alphabet(s, -1, False),
|
||||||
4: lambda s: shift_alphabet(s, +1, True),
|
4: lambda s: shift_alphabet(s, +1, True),
|
||||||
5: lambda s: shift_alphabet(s, -1, True),
|
5: lambda s: shift_alphabet(s, -1, True),
|
||||||
}
|
}
|
||||||
elif self._mem.version == 2:
|
elif self._mem.version == 2:
|
||||||
self._specials = {
|
self._specials = {
|
||||||
1: lambda s: abbreviation(s, 0),
|
1: lambda s: abbreviation(s, 0),
|
||||||
|
|
@ -181,44 +180,44 @@ class ZCharTranslator:
|
||||||
3: lambda s: shift_alphabet(s, -1, False),
|
3: lambda s: shift_alphabet(s, -1, False),
|
||||||
4: lambda s: shift_alphabet(s, +1, True),
|
4: lambda s: shift_alphabet(s, +1, True),
|
||||||
5: lambda s: shift_alphabet(s, -1, True),
|
5: lambda s: shift_alphabet(s, -1, True),
|
||||||
}
|
}
|
||||||
else: # ZM v3-5
|
else: # ZM v3-5
|
||||||
self._specials = {
|
self._specials = {
|
||||||
1: lambda s: abbreviation(s, 0),
|
1: lambda s: abbreviation(s, 0),
|
||||||
2: lambda s: abbreviation(s, 1),
|
2: lambda s: abbreviation(s, 1),
|
||||||
3: lambda s: abbreviation(s, 2),
|
3: lambda s: abbreviation(s, 2),
|
||||||
4: lambda s: shift_alphabet(s, +1, False),
|
4: lambda s: shift_alphabet(s, +1, False),
|
||||||
5: lambda s: shift_alphabet(s, -1, False),
|
5: lambda s: shift_alphabet(s, -1, False),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _special_zscii(self, state, char):
|
def _special_zscii(self, state, char):
|
||||||
if 'zscii_char' not in list(state.keys()):
|
if "zscii_char" not in list(state.keys()):
|
||||||
state['zscii_char'] = char
|
state["zscii_char"] = char
|
||||||
else:
|
else:
|
||||||
zchar = (state['zscii_char'] << 5) + char
|
zchar = (state["zscii_char"] << 5) + char
|
||||||
state['zscii'].append(zchar)
|
state["zscii"].append(zchar)
|
||||||
del state['zscii_char']
|
del state["zscii_char"]
|
||||||
del state['state_handler']
|
del state["state_handler"]
|
||||||
|
|
||||||
def get(self, zstr, allow_abbreviations=True):
|
def get(self, zstr, allow_abbreviations=True):
|
||||||
state = {
|
state = {
|
||||||
'curr_alpha': 0,
|
"curr_alpha": 0,
|
||||||
'prev_alpha': 0,
|
"prev_alpha": 0,
|
||||||
'zscii': [],
|
"zscii": [],
|
||||||
'allow_abbreviations': allow_abbreviations,
|
"allow_abbreviations": allow_abbreviations,
|
||||||
}
|
}
|
||||||
|
|
||||||
for c in zstr:
|
for c in zstr:
|
||||||
if 'state_handler' in list(state.keys()):
|
if "state_handler" in list(state.keys()):
|
||||||
# If a special handler has registered itself, then hand
|
# If a special handler has registered itself, then hand
|
||||||
# processing over to it.
|
# processing over to it.
|
||||||
state['state_handler'](state, c)
|
state["state_handler"](state, c) # type: ignore[call-non-callable]
|
||||||
elif c in list(self._specials.keys()):
|
elif c in list(self._specials.keys()):
|
||||||
# Hand off per-ZM version special char handling.
|
# Hand off per-ZM version special char handling.
|
||||||
self._specials[c](state)
|
self._specials[c](state)
|
||||||
elif state['curr_alpha'] == 2 and c == 6:
|
elif state["curr_alpha"] == 2 and c == 6:
|
||||||
# Handle the strange A2/6 character
|
# Handle the strange A2/6 character
|
||||||
state['state_handler'] = self._special_zscii
|
state["state_handler"] = self._special_zscii
|
||||||
else:
|
else:
|
||||||
# Do the usual Thing: append a zscii code to the
|
# Do the usual Thing: append a zscii code to the
|
||||||
# decoded sequence and revert to the "previous"
|
# decoded sequence and revert to the "previous"
|
||||||
|
|
@ -227,36 +226,97 @@ class ZCharTranslator:
|
||||||
if c == 0:
|
if c == 0:
|
||||||
# Append a space.
|
# Append a space.
|
||||||
z = 32
|
z = 32
|
||||||
elif state['curr_alpha'] == 2:
|
elif state["curr_alpha"] == 2:
|
||||||
# The symbol alphabet table only has 25 chars
|
# The symbol alphabet table only has 25 chars
|
||||||
# because of the A2/6 special char, so we need to
|
# because of the A2/6 special char, so we need to
|
||||||
# adjust differently.
|
# adjust differently.
|
||||||
z = self._alphabet[state['curr_alpha']][c-7]
|
z = self._alphabet[state["curr_alpha"]][c - 7]
|
||||||
else:
|
else:
|
||||||
z = self._alphabet[state['curr_alpha']][c-6]
|
z = self._alphabet[state["curr_alpha"]][c - 6]
|
||||||
state['zscii'].append(z)
|
state["zscii"].append(z)
|
||||||
state['curr_alpha'] = state['prev_alpha']
|
state["curr_alpha"] = state["prev_alpha"]
|
||||||
|
|
||||||
return state['zscii']
|
return state["zscii"]
|
||||||
|
|
||||||
|
|
||||||
class ZsciiTranslator:
|
class ZsciiTranslator:
|
||||||
# The default Unicode Translation Table that maps to ZSCII codes
|
# The default Unicode Translation Table that maps to ZSCII codes
|
||||||
# 155-251. The codes are unicode codepoints for a host of strange
|
# 155-251. The codes are unicode codepoints for a host of strange
|
||||||
# characters.
|
# characters.
|
||||||
DEFAULT_UTT = [chr(x) for x in
|
DEFAULT_UTT = [
|
||||||
(0xe4, 0xf6, 0xfc, 0xc4, 0xd6, 0xdc,
|
chr(x)
|
||||||
0xdf, 0xbb, 0xab, 0xeb, 0xef, 0xff,
|
for x in (
|
||||||
0xcb, 0xcf, 0xe1, 0xe9, 0xed, 0xf3,
|
0xE4,
|
||||||
0xfa, 0xfd, 0xc1, 0xc9, 0xcd, 0xd3,
|
0xF6,
|
||||||
0xda, 0xdd, 0xe0, 0xe8, 0xec, 0xf2,
|
0xFC,
|
||||||
0xf9, 0xc0, 0xc8, 0xcc, 0xd2, 0xd9,
|
0xC4,
|
||||||
0xe2, 0xea, 0xee, 0xf4, 0xfb, 0xc2,
|
0xD6,
|
||||||
0xca, 0xce, 0xd4, 0xdb, 0xe5, 0xc5,
|
0xDC,
|
||||||
0xf8, 0xd8, 0xe3, 0xf1, 0xf5, 0xc3,
|
0xDF,
|
||||||
0xd1, 0xd5, 0xe6, 0xc6, 0xe7, 0xc7,
|
0xBB,
|
||||||
0xfe, 0xf0, 0xde, 0xd0, 0xa3, 0x153,
|
0xAB,
|
||||||
0x152, 0xa1, 0xbf)]
|
0xEB,
|
||||||
|
0xEF,
|
||||||
|
0xFF,
|
||||||
|
0xCB,
|
||||||
|
0xCF,
|
||||||
|
0xE1,
|
||||||
|
0xE9,
|
||||||
|
0xED,
|
||||||
|
0xF3,
|
||||||
|
0xFA,
|
||||||
|
0xFD,
|
||||||
|
0xC1,
|
||||||
|
0xC9,
|
||||||
|
0xCD,
|
||||||
|
0xD3,
|
||||||
|
0xDA,
|
||||||
|
0xDD,
|
||||||
|
0xE0,
|
||||||
|
0xE8,
|
||||||
|
0xEC,
|
||||||
|
0xF2,
|
||||||
|
0xF9,
|
||||||
|
0xC0,
|
||||||
|
0xC8,
|
||||||
|
0xCC,
|
||||||
|
0xD2,
|
||||||
|
0xD9,
|
||||||
|
0xE2,
|
||||||
|
0xEA,
|
||||||
|
0xEE,
|
||||||
|
0xF4,
|
||||||
|
0xFB,
|
||||||
|
0xC2,
|
||||||
|
0xCA,
|
||||||
|
0xCE,
|
||||||
|
0xD4,
|
||||||
|
0xDB,
|
||||||
|
0xE5,
|
||||||
|
0xC5,
|
||||||
|
0xF8,
|
||||||
|
0xD8,
|
||||||
|
0xE3,
|
||||||
|
0xF1,
|
||||||
|
0xF5,
|
||||||
|
0xC3,
|
||||||
|
0xD1,
|
||||||
|
0xD5,
|
||||||
|
0xE6,
|
||||||
|
0xC6,
|
||||||
|
0xE7,
|
||||||
|
0xC7,
|
||||||
|
0xFE,
|
||||||
|
0xF0,
|
||||||
|
0xDE,
|
||||||
|
0xD0,
|
||||||
|
0xA3,
|
||||||
|
0x153,
|
||||||
|
0x152,
|
||||||
|
0xA1,
|
||||||
|
0xBF,
|
||||||
|
)
|
||||||
|
]
|
||||||
# And here is the offset at which the Unicode Translation Table
|
# And here is the offset at which the Unicode Translation Table
|
||||||
# starts.
|
# starts.
|
||||||
UTT_OFFSET = 155
|
UTT_OFFSET = 155
|
||||||
|
|
@ -299,19 +359,14 @@ class ZsciiTranslator:
|
||||||
|
|
||||||
def __init__(self, zmem):
|
def __init__(self, zmem):
|
||||||
self._mem = zmem
|
self._mem = zmem
|
||||||
self._output_table = {
|
self._output_table = {0: "", 10: "\n"}
|
||||||
0 : "",
|
self._input_table = {"\n": 10}
|
||||||
10: "\n"
|
|
||||||
}
|
|
||||||
self._input_table = {
|
|
||||||
"\n": 10
|
|
||||||
}
|
|
||||||
|
|
||||||
self._load_unicode_table()
|
self._load_unicode_table()
|
||||||
|
|
||||||
# Populate the input and output tables with the ASCII and UTT
|
# Populate the input and output tables with the ASCII and UTT
|
||||||
# characters.
|
# characters.
|
||||||
for code,char in [(x,chr(x)) for x in range(32,127)]:
|
for code, char in [(x, chr(x)) for x in range(32, 127)]:
|
||||||
self._output_table[code] = char
|
self._output_table[code] = char
|
||||||
self._input_table[char] = code
|
self._input_table[char] = code
|
||||||
|
|
||||||
|
|
@ -324,8 +379,11 @@ class ZsciiTranslator:
|
||||||
# Oh and we also pull the items from the subclass into this
|
# Oh and we also pull the items from the subclass into this
|
||||||
# instance, so as to make reference to these special codes
|
# instance, so as to make reference to these special codes
|
||||||
# easier.
|
# easier.
|
||||||
for name,code in [(c,v) for c,v in list(self.Input.__dict__.items())
|
for name, code in [
|
||||||
if not c.startswith('__')]:
|
(c, v)
|
||||||
|
for c, v in list(self.Input.__dict__.items())
|
||||||
|
if not c.startswith("__")
|
||||||
|
]:
|
||||||
self._input_table[code] = code
|
self._input_table[code] = code
|
||||||
setattr(self, name, code)
|
setattr(self, name, code)
|
||||||
|
|
||||||
|
|
@ -350,18 +408,22 @@ class ZsciiTranslator:
|
||||||
#
|
#
|
||||||
# Then there is a unicode translation table other than the
|
# Then there is a unicode translation table other than the
|
||||||
# default that needs loading.
|
# default that needs loading.
|
||||||
if (ext_table_addr != 0 and
|
if (
|
||||||
self._mem.read_word(ext_table_addr) >= 3 and
|
ext_table_addr != 0
|
||||||
self._mem.read_word(ext_table_addr+6) != 0):
|
and self._mem.read_word(ext_table_addr) >= 3
|
||||||
|
and self._mem.read_word(ext_table_addr + 6) != 0
|
||||||
|
):
|
||||||
|
# Get the unicode translation table address
|
||||||
|
utt_addr = self._mem.read_word(ext_table_addr + 6)
|
||||||
|
|
||||||
# The first byte is the number of unicode characters
|
# The first byte is the number of unicode characters
|
||||||
# in the table.
|
# in the table.
|
||||||
utt_len = self._mem[ext_table_addr]
|
utt_len = self._mem[utt_addr]
|
||||||
|
|
||||||
# Build the range of addresses to load from, and build
|
# Build the range of addresses to load from, and build
|
||||||
# the unicode translation table as a list of unicode
|
# the unicode translation table as a list of unicode
|
||||||
# chars.
|
# chars.
|
||||||
utt_range = range(ext_table+1, ext_table+1+(utt_len*2), 2)
|
utt_range = range(utt_addr + 1, utt_addr + 1 + (utt_len * 2), 2)
|
||||||
utt = [chr(self._mem.read_word(i)) for i in utt_range]
|
utt = [chr(self._mem.read_word(i)) for i in utt_range]
|
||||||
else:
|
else:
|
||||||
utt = self.DEFAULT_UTT
|
utt = self.DEFAULT_UTT
|
||||||
|
|
@ -380,7 +442,7 @@ class ZsciiTranslator:
|
||||||
try:
|
try:
|
||||||
return self._output_table[index]
|
return self._output_table[index]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise IndexError("No such ZSCII character")
|
raise IndexError("No such ZSCII character") from None
|
||||||
|
|
||||||
def utoz(self, char):
|
def utoz(self, char):
|
||||||
"""Translate the given Unicode code into the corresponding
|
"""Translate the given Unicode code into the corresponding
|
||||||
|
|
@ -389,10 +451,10 @@ class ZsciiTranslator:
|
||||||
try:
|
try:
|
||||||
return self._input_table[char]
|
return self._input_table[char]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise IndexError("No such input character")
|
raise IndexError("No such input character") from None
|
||||||
|
|
||||||
def get(self, zscii):
|
def get(self, zscii):
|
||||||
return ''.join([self.ztou(c) for c in zscii])
|
return "".join([self.ztou(c) for c in zscii])
|
||||||
|
|
||||||
|
|
||||||
class ZStringFactory:
|
class ZStringFactory:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue