Port 12 trivial opcodes to hybrid z-machine interpreter
This commit is contained in:
parent
677ddac89f
commit
dcc952d4c5
22 changed files with 4925 additions and 0 deletions
27
src/mudlib/zmachine/LICENSE
Normal file
27
src/mudlib/zmachine/LICENSE
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
* Neither the name of the ZVM project nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
9
src/mudlib/zmachine/__init__.py
Normal file
9
src/mudlib/zmachine/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"""Hybrid z-machine interpreter based on sussman/zvm.
|
||||
|
||||
Original: https://github.com/sussman/zvm (BSD license)
|
||||
Extended with opcode implementations ported from DFillmore/viola.
|
||||
"""
|
||||
|
||||
from .zmachine import ZMachine
|
||||
|
||||
__all__ = ["ZMachine"]
|
||||
62
src/mudlib/zmachine/bitfield.py
Normal file
62
src/mudlib/zmachine/bitfield.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
#
|
||||
# A helper class to access individual bits of a bitfield in a Pythonic
|
||||
# way.
|
||||
#
|
||||
# Inspired from a recipe at:
|
||||
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/113799
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
class BitField:
|
||||
"""An bitfield gives read/write access to the individual bits of a
|
||||
value, in array and slice notation.
|
||||
|
||||
Conversion back to an int value is also supported, and a method is
|
||||
provided to print the value in binary for debug purposes.
|
||||
|
||||
For all indexes, 0 is the LSB (Least Significant Bit)."""
|
||||
|
||||
def __init__(self, value=0):
|
||||
"""Initialize a bitfield object from a number or string value."""
|
||||
if isinstance(value, str):
|
||||
self._d = ord(value)
|
||||
else:
|
||||
self._d = value
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""Get the value of a single bit or slice."""
|
||||
if isinstance(index, slice):
|
||||
start, stop = index.start, index.stop
|
||||
if start > stop:
|
||||
(start, stop) = (stop, start)
|
||||
mask = (1<<(stop - start)) -1
|
||||
return (self._d >> start) & mask
|
||||
else:
|
||||
return (self._d >> index) & 1
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
"""Set the value of a single bit or slice."""
|
||||
if isinstance(value, str):
|
||||
value = ord(value)
|
||||
if isinstance(index, slice):
|
||||
start, stop = index.start, index.stop
|
||||
mask = (1<<(stop - start)) -1
|
||||
value = (value & mask) << start
|
||||
mask = mask << start
|
||||
self._d = (self._d & ~mask) | value
|
||||
return (self._d >> start) & mask
|
||||
else:
|
||||
value = (value) << index
|
||||
mask = (1) << index
|
||||
self._d = (self._d & ~mask) | value
|
||||
|
||||
def __int__(self):
|
||||
"""Return the whole bitfield as an integer."""
|
||||
return self._d
|
||||
|
||||
def to_str(self, len):
|
||||
"""Print the binary representation of the bitfield."""
|
||||
return ''.join(["%d" % self[i]
|
||||
for i in range(len-1,-1,-1)])
|
||||
347
src/mudlib/zmachine/glk.py
Normal file
347
src/mudlib/zmachine/glk.py
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
#
|
||||
# This module defines a ctypes foreign function interface to a Glk
|
||||
# library that has been built as a shared library. For more information
|
||||
# on the Glk API, see http://www.eblong.com/zarf/glk/.
|
||||
#
|
||||
# Note that the way this module interfaces with a Glk library is
|
||||
# slightly different from the standard; the standard interface
|
||||
# actually assumes that a Glk library is in fact not a library, but a
|
||||
# "front-end" program that is statically linked to a Glk back-end,
|
||||
# also known as a Glk "program", by calling the back-end's glk_main()
|
||||
# function.
|
||||
#
|
||||
# Instead of this, we assume that the Glk library is actually a shared
|
||||
# library that is initialized by some external source--be it a Python
|
||||
# script or a compiled program--and used by this module. Note that
|
||||
# this is actually the way some Glk libraries, such as WindowsGlk, are
|
||||
# made to function.
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
import ctypes
|
||||
|
||||
# These are ctypes-style declarations that reflect the Glk.h file,
|
||||
# which defines the Glk API, version 0.7.0. The most recent version
|
||||
# of Glk.h can be found here:
|
||||
#
|
||||
# http://www.eblong.com/zarf/glk/glk.h
|
||||
#
|
||||
# Note that there are ctypes extension libraries that can do this kind
|
||||
# of thing for us (that is, take a .h file and automatically generate
|
||||
# a ctypes wrapper from it); however, the only one that exists at the
|
||||
# time of this writing is ctypeslib, which has dependencies that would
|
||||
# make our build process quite complex. Given the relatively small
|
||||
# size of the Glk API and the freedom we get from hand-coding the
|
||||
# interface ourselves, we're not using ctypeslib.
|
||||
|
||||
glsi32 = ctypes.c_int32
|
||||
glui32 = ctypes.c_uint32
|
||||
|
||||
winid_t = ctypes.c_void_p
|
||||
strid_t = ctypes.c_void_p
|
||||
frefid_t = ctypes.c_void_p
|
||||
schanid_t = ctypes.c_void_p
|
||||
|
||||
# TRUE, FALSE, and NULL aren't defined in glk.h, but are mentioned in
|
||||
# Section 1.9 of the Glk spec 0.7.0.
|
||||
TRUE = 1
|
||||
FALSE = 0
|
||||
NULL = ctypes.pointer(glui32(0))
|
||||
|
||||
gestalt_Version = 0
|
||||
gestalt_CharInput = 1
|
||||
gestalt_LineInput = 2
|
||||
gestalt_CharOutput = 3
|
||||
gestalt_CharOutput_CannotPrint = 0
|
||||
gestalt_CharOutput_ApproxPrint = 1
|
||||
gestalt_CharOutput_ExactPrint = 2
|
||||
gestalt_MouseInput = 4
|
||||
gestalt_Timer = 5
|
||||
gestalt_Graphics = 6
|
||||
gestalt_DrawImage = 7
|
||||
gestalt_Sound = 8
|
||||
gestalt_SoundVolume = 9
|
||||
gestalt_SoundNotify = 10
|
||||
gestalt_Hyperlinks = 11
|
||||
gestalt_HyperlinkInput = 12
|
||||
gestalt_SoundMusic = 13
|
||||
gestalt_GraphicsTransparency = 14
|
||||
gestalt_Unicode = 15
|
||||
|
||||
evtype_None = 0
|
||||
evtype_Timer = 1
|
||||
evtype_CharInput = 2
|
||||
evtype_LineInput = 3
|
||||
evtype_MouseInput = 4
|
||||
evtype_Arrange = 5
|
||||
evtype_Redraw = 6
|
||||
evtype_SoundNotify = 7
|
||||
evtype_Hyperlink = 8
|
||||
|
||||
class event_t(ctypes.Structure):
|
||||
_fields_ = [("type", glui32),
|
||||
("win", winid_t),
|
||||
("val1", glui32),
|
||||
("val2", glui32)]
|
||||
|
||||
keycode_Unknown = 0xffffffff
|
||||
keycode_Left = 0xfffffffe
|
||||
keycode_Right = 0xfffffffd
|
||||
keycode_Up = 0xfffffffc
|
||||
keycode_Down = 0xfffffffb
|
||||
keycode_Return = 0xfffffffa
|
||||
keycode_Delete = 0xfffffff9
|
||||
keycode_Escape = 0xfffffff8
|
||||
keycode_Tab = 0xfffffff7
|
||||
keycode_PageUp = 0xfffffff6
|
||||
keycode_PageDown = 0xfffffff5
|
||||
keycode_Home = 0xfffffff4
|
||||
keycode_End = 0xfffffff3
|
||||
keycode_Func1 = 0xffffffef
|
||||
keycode_Func2 = 0xffffffee
|
||||
keycode_Func3 = 0xffffffed
|
||||
keycode_Func4 = 0xffffffec
|
||||
keycode_Func5 = 0xffffffeb
|
||||
keycode_Func6 = 0xffffffea
|
||||
keycode_Func7 = 0xffffffe9
|
||||
keycode_Func8 = 0xffffffe8
|
||||
keycode_Func9 = 0xffffffe7
|
||||
keycode_Func10 = 0xffffffe6
|
||||
keycode_Func11 = 0xffffffe5
|
||||
keycode_Func12 = 0xffffffe4
|
||||
keycode_MAXVAL = 28
|
||||
|
||||
style_Normal = 0
|
||||
style_Emphasized = 1
|
||||
style_Preformatted = 2
|
||||
style_Header = 3
|
||||
style_Subheader = 4
|
||||
style_Alert = 5
|
||||
style_Note = 6
|
||||
style_BlockQuote = 7
|
||||
style_Input = 8
|
||||
style_User1 = 9
|
||||
style_User2 = 10
|
||||
style_NUMSTYLES = 11
|
||||
|
||||
class stream_result_t(ctypes.Structure):
|
||||
_fields_ = [("readcount", glui32),
|
||||
("writecount", glui32)]
|
||||
|
||||
wintype_AllTypes = 0
|
||||
wintype_Pair = 1
|
||||
wintype_Blank = 2
|
||||
wintype_TextBuffer = 3
|
||||
wintype_TextGrid = 4
|
||||
wintype_Graphics = 5
|
||||
|
||||
winmethod_Left = 0x00
|
||||
winmethod_Right = 0x01
|
||||
winmethod_Above = 0x02
|
||||
winmethod_Below = 0x03
|
||||
winmethod_DirMask = 0x0f
|
||||
|
||||
winmethod_Fixed = 0x10
|
||||
winmethod_Proportional = 0x20
|
||||
winmethod_DivisionMask = 0xf0
|
||||
|
||||
fileusage_Data = 0x00
|
||||
fileusage_SavedGame = 0x01
|
||||
fileusage_Transcript = 0x02
|
||||
fileusage_InputRecord = 0x03
|
||||
fileusage_TypeMask = 0x0f
|
||||
|
||||
fileusage_TextMode = 0x100
|
||||
fileusage_BinaryMode = 0x000
|
||||
|
||||
filemode_Write = 0x01
|
||||
filemode_Read = 0x02
|
||||
filemode_ReadWrite = 0x03
|
||||
filemode_WriteAppend = 0x05
|
||||
|
||||
seekmode_Start = 0
|
||||
seekmode_Current = 1
|
||||
seekmode_End = 2
|
||||
|
||||
stylehint_Indentation = 0
|
||||
stylehint_ParaIndentation = 1
|
||||
stylehint_Justification = 2
|
||||
stylehint_Size = 3
|
||||
stylehint_Weight = 4
|
||||
stylehint_Oblique = 5
|
||||
stylehint_Proportional = 6
|
||||
stylehint_TextColor = 7
|
||||
stylehint_BackColor = 8
|
||||
stylehint_ReverseColor = 9
|
||||
stylehint_NUMHINTS = 10
|
||||
|
||||
stylehint_just_LeftFlush = 0
|
||||
stylehint_just_LeftRight = 1
|
||||
stylehint_just_Centered = 2
|
||||
stylehint_just_RightFlush = 3
|
||||
|
||||
# Function prototypes for the core Glk API. It is a list of 3-tuples; each
|
||||
# item in the list represents a function prototype, and each 3-tuple
|
||||
# is in the form (result_type, function_name, arg_types).
|
||||
|
||||
CORE_GLK_LIB_API = [
|
||||
(None, "glk_exit", ()),
|
||||
(None, "glk_tick", ()),
|
||||
(glui32, "glk_gestalt", (glui32, glui32)),
|
||||
(glui32, "glk_gestalt_ext", (glui32, glui32, ctypes.POINTER(glui32),
|
||||
glui32)),
|
||||
(winid_t, "glk_window_get_root", ()),
|
||||
(winid_t, "glk_window_open", (winid_t, glui32, glui32, glui32, glui32)),
|
||||
(None, "glk_window_close", (winid_t, ctypes.POINTER(stream_result_t))),
|
||||
(None, "glk_window_get_size", (winid_t, ctypes.POINTER(glui32),
|
||||
ctypes.POINTER(glui32)) ),
|
||||
(None, "glk_window_set_arrangement", (winid_t, glui32, glui32, winid_t)),
|
||||
(None, "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))),
|
||||
(glui32, "glk_window_get_rock", (winid_t,)),
|
||||
(glui32, "glk_window_get_type", (winid_t,)),
|
||||
(winid_t, "glk_window_get_parent", (winid_t,)),
|
||||
(winid_t, "glk_window_get_sibling", (winid_t,)),
|
||||
(None, "glk_window_clear", (winid_t,)),
|
||||
(None, "glk_window_move_cursor", (winid_t, glui32, glui32)),
|
||||
(strid_t, "glk_window_get_stream", (winid_t,)),
|
||||
(None, "glk_window_set_echo_stream", (winid_t, strid_t)),
|
||||
(strid_t, "glk_window_get_echo_stream", (winid_t,)),
|
||||
(None, "glk_set_window", (winid_t,)),
|
||||
(strid_t, "glk_stream_open_file", (frefid_t, glui32, glui32)),
|
||||
(strid_t, "glk_stream_open_memory", (ctypes.c_char_p,
|
||||
glui32, glui32, glui32)),
|
||||
(None, "glk_stream_close", (strid_t, ctypes.POINTER(stream_result_t))),
|
||||
(strid_t, "glk_stream_iterate", (strid_t, ctypes.POINTER(glui32))),
|
||||
(glui32, "glk_stream_get_rock", (strid_t,)),
|
||||
(None, "glk_stream_set_position", (strid_t, glsi32, glui32)),
|
||||
(glui32, "glk_stream_get_position", (strid_t,)),
|
||||
(None, "glk_stream_set_current", (strid_t,)),
|
||||
(strid_t, "glk_stream_get_current", ()),
|
||||
(None, "glk_put_char", (ctypes.c_ubyte,)),
|
||||
(None, "glk_put_char_stream", (strid_t, ctypes.c_ubyte)),
|
||||
(None, "glk_put_string", (ctypes.c_char_p,)),
|
||||
(None, "glk_put_string_stream", (strid_t, ctypes.c_char_p)),
|
||||
(None, "glk_put_buffer", (ctypes.c_char_p, glui32)),
|
||||
(None, "glk_put_buffer_stream", (strid_t, ctypes.c_char_p, glui32)),
|
||||
(None, "glk_set_style", (glui32,)),
|
||||
(None, "glk_set_style_stream", (strid_t, glui32)),
|
||||
(glsi32, "glk_get_char_stream", (strid_t,)),
|
||||
(glui32, "glk_get_line_stream", (strid_t, ctypes.c_char_p, glui32)),
|
||||
(glui32, "glk_get_buffer_stream", (strid_t, ctypes.c_char_p, glui32)),
|
||||
(None, "glk_stylehint_set", (glui32, glui32, glui32, glsi32)),
|
||||
(None, "glk_stylehint_clear", (glui32, glui32, glui32)),
|
||||
(glui32, "glk_style_distinguish", (winid_t, glui32, glui32)),
|
||||
(glui32, "glk_style_measure", (winid_t, glui32, glui32,
|
||||
ctypes.POINTER(glui32))),
|
||||
(frefid_t, "glk_fileref_create_temp", (glui32, glui32)),
|
||||
(frefid_t, "glk_fileref_create_by_name", (glui32, ctypes.c_char_p,
|
||||
glui32)),
|
||||
(frefid_t, "glk_fileref_create_by_prompt", (glui32, glui32, glui32)),
|
||||
(frefid_t, "glk_fileref_create_from_fileref", (glui32, frefid_t,
|
||||
glui32)),
|
||||
(None, "glk_fileref_destroy", (frefid_t,)),
|
||||
(frefid_t, "glk_fileref_iterate", (frefid_t, ctypes.POINTER(glui32))),
|
||||
(glui32, "glk_fileref_get_rock", (frefid_t,)),
|
||||
(None, "glk_fileref_delete_file", (frefid_t,)),
|
||||
(glui32, "glk_fileref_does_file_exist", (frefid_t,)),
|
||||
(None, "glk_select", (ctypes.POINTER(event_t),)),
|
||||
(None, "glk_select_poll", (ctypes.POINTER(event_t),)),
|
||||
(None, "glk_request_timer_events", (glui32,)),
|
||||
(None, "glk_request_line_event", (winid_t, ctypes.c_char_p, glui32,
|
||||
glui32)),
|
||||
(None, "glk_request_char_event", (winid_t,)),
|
||||
(None, "glk_request_mouse_event", (winid_t,)),
|
||||
(None, "glk_cancel_line_event", (winid_t, ctypes.POINTER(event_t))),
|
||||
(None, "glk_cancel_char_event", (winid_t,)),
|
||||
(None, "glk_cancel_mouse_event", (winid_t,)),
|
||||
]
|
||||
|
||||
# Function prototypes for the optional Unicode extension of the Glk
|
||||
# API.
|
||||
UNICODE_GLK_LIB_API = [
|
||||
(None, "glk_put_char_uni", (glui32,)),
|
||||
(None, "glk_put_string_uni", (ctypes.POINTER(glui32),)),
|
||||
(None, "glk_put_buffer_uni", (ctypes.POINTER(glui32), glui32)),
|
||||
(None, "glk_put_char_stream_uni", (strid_t, glui32)),
|
||||
(None, "glk_put_string_stream_uni", (strid_t, ctypes.POINTER(glui32))),
|
||||
(None, "glk_put_buffer_stream_uni", (strid_t, ctypes.POINTER(glui32),
|
||||
glui32)),
|
||||
(glsi32, "glk_get_char_stream_uni", (strid_t,)),
|
||||
(glui32, "glk_get_buffer_stream_uni", (strid_t, ctypes.POINTER(glui32),
|
||||
glui32)),
|
||||
(glui32, "glk_get_line_stream_uni", (strid_t, ctypes.POINTER(glui32),
|
||||
glui32)),
|
||||
(strid_t, "glk_stream_open_file_uni", (frefid_t, glui32, glui32)),
|
||||
(strid_t, "glk_stream_open_memory_uni", (ctypes.POINTER(glui32),
|
||||
glui32, glui32, glui32)),
|
||||
(None, "glk_request_char_event_uni", (winid_t,)),
|
||||
(None, "glk_request_line_event_uni", (winid_t, ctypes.POINTER(glui32),
|
||||
glui32, glui32))
|
||||
]
|
||||
|
||||
class GlkLib:
|
||||
"""Encapsulates the ctypes interface to a Glk shared library. When
|
||||
instantiated, it wraps the shared library with the appropriate
|
||||
function prototypes from the Glk API to reduce the chance of
|
||||
mis-calls that may result in segfaults (this effectively simulates
|
||||
the strong type-checking a C compiler would perform)."""
|
||||
|
||||
def __init__(self, lib_name):
|
||||
"""Instantiates the instance, binding it to the given shared
|
||||
library (which is referenced by name)."""
|
||||
|
||||
self._dll = ctypes.CDLL(lib_name)
|
||||
|
||||
self.__bind_prototypes(CORE_GLK_LIB_API)
|
||||
|
||||
if self.glk_gestalt(gestalt_Unicode, 0) == 1:
|
||||
self.__bind_prototypes(UNICODE_GLK_LIB_API)
|
||||
else:
|
||||
self.__bind_not_implemented_prototypes(UNICODE_GLK_LIB_API)
|
||||
|
||||
def __bind_prototypes(self, function_prototypes):
|
||||
"""Create function prototypes from the given list of 3-tuples
|
||||
of the form (result_type, function_name, arg_types), bind them
|
||||
to functions in our shared library, and then bind the function
|
||||
instances as methods to this object."""
|
||||
|
||||
for function_prototype in function_prototypes:
|
||||
result_type, function_name, arg_types = function_prototype
|
||||
prototype = ctypes.CFUNCTYPE(result_type, *arg_types)
|
||||
function = prototype((function_name, self._dll))
|
||||
setattr(self, function_name, function)
|
||||
|
||||
def __bind_not_implemented_prototypes(self, function_prototypes):
|
||||
"""Create functions with the names from the given list of
|
||||
3-tuples of the form (result_type, function_name, arg_types)
|
||||
that simply raise NotImplementedError, and bind them to this
|
||||
object. This should be used when a Glk library doesn't
|
||||
support some optional extension of the Glk API."""
|
||||
|
||||
def notImplementedFunction(*args, **kwargs):
|
||||
raise NotImplementedError( "Function not implemented " \
|
||||
"by this Glk library." )
|
||||
|
||||
for function_prototype in function_prototypes:
|
||||
_, function_name, _ = function_prototype
|
||||
setattr(self, function_name, notImplementedFunction)
|
||||
|
||||
def glk_char_to_lower(self, ch):
|
||||
raise NotImplementedError("Use unicode.lower() instead.")
|
||||
|
||||
def glk_char_to_upper(self, ch):
|
||||
raise NotImplementedError("Use unicode.upper() instead.")
|
||||
|
||||
def glk_buffer_to_lower_case_uni(self, buf, len, numchars):
|
||||
raise NotImplementedError("Use unicode.lower() instead.")
|
||||
|
||||
def glk_buffer_to_upper_case_uni(self, buf, len, numchars):
|
||||
raise NotImplementedError("Use unicode.upper() instead.")
|
||||
|
||||
def glk_buffer_to_title_case_uni(self, buf, len, numchars, lowerrest):
|
||||
raise NotImplementedError("Use unicode.title() instead.")
|
||||
463
src/mudlib/zmachine/quetzal.py
Normal file
463
src/mudlib/zmachine/quetzal.py
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
#
|
||||
# A class which knows how to write and parse 'Quetzal' files, which is
|
||||
# the standard save-file format for modern Z-machine implementations.
|
||||
# This allows ZVM's saved games to load in other interpreters, and
|
||||
# vice versa.
|
||||
#
|
||||
# The Quetzal format is documented at:
|
||||
# http://www.ifarchive.org/if-archive/infocom/\
|
||||
# interpreters/specification/savefile_14.txt
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
# Woohoo! Python has a module to parse IFF files, which is a generic
|
||||
# interchange format. A Quetzal file is in fact a type of IFF file.
|
||||
import chunk
|
||||
import os
|
||||
|
||||
from . import bitfield, zstackmanager
|
||||
from .zlogging import log
|
||||
|
||||
# The general format of Queztal is that of a "FORM" IFF file, which is
|
||||
# a container class for 'chunks'.
|
||||
#
|
||||
# "FORM", 4 bytes of container total-length, "IFZS",
|
||||
# 4-byte chunkname, 4-byte length, length bytes of data
|
||||
# 4-byte chunkname, 4-byte length, length bytes of data
|
||||
# 4-byte chunkname, 4-byte length, length bytes of data
|
||||
# ...
|
||||
|
||||
class QuetzalError(Exception):
|
||||
"General exception for Quetzal classes."
|
||||
pass
|
||||
|
||||
class QuetzalMalformedChunk(QuetzalError):
|
||||
"Malformed chunk detected."
|
||||
|
||||
class QuetzalNoSuchSavefile(QuetzalError):
|
||||
"Cannot locate save-game file."
|
||||
|
||||
class QuetzalUnrecognizedFileFormat(QuetzalError):
|
||||
"Not a valid Quetzal file."
|
||||
|
||||
class QuetzalIllegalChunkOrder(QuetzalError):
|
||||
"IFhd chunk came after Umem/Cmem/Stks chunks (see section 5.4)."
|
||||
|
||||
class QuetzalMismatchedFile(QuetzalError):
|
||||
"Quetzal file dosen't match current game."
|
||||
|
||||
class QuetzalMemoryOutOfBounds(QuetzalError):
|
||||
"Decompressed dynamic memory has gone out of bounds."
|
||||
|
||||
class QuetzalMemoryMismatch(QuetzalError):
|
||||
"Savefile's dynamic memory image is incorrectly sized."
|
||||
|
||||
class QuetzalStackFrameOverflow(QuetzalError):
|
||||
"Stack frame parsing went beyond bounds of 'Stks' chunk."
|
||||
|
||||
|
||||
class QuetzalParser:
|
||||
"""A class to read a Quetzal save-file and modify a z-machine."""
|
||||
|
||||
def __init__(self, zmachine):
|
||||
log("Creating new instance of QuetzalParser")
|
||||
self._zmachine = zmachine
|
||||
self._seen_mem_or_stks = False
|
||||
self._last_loaded_metadata = {} # metadata for tests & debugging
|
||||
|
||||
|
||||
def _parse_ifhd(self, data):
|
||||
"""Parse a chunk of type IFhd, and check that the quetzal file
|
||||
really belongs to the current story (by comparing release number,
|
||||
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:
|
||||
"""A class to write the current state of a z-machine into a
|
||||
Quetzal-format file."""
|
||||
|
||||
def __init__(self, zmachine):
|
||||
log("Creating new instance of QuetzalWriter")
|
||||
self._zmachine = zmachine
|
||||
|
||||
def _generate_ifhd_chunk(self):
|
||||
"""Return a chunk of type IFhd, containing metadata about the
|
||||
zmachine and story being played."""
|
||||
|
||||
### TODO: write this. payload must be *exactly* 13 bytes, even if
|
||||
### it means padding the program counter.
|
||||
|
||||
### Some old infocom games don't have checksums stored in header.
|
||||
### If not, generate it from the *original* story file memory
|
||||
### image and put it into this chunk. See ZMemory.generate_checksum().
|
||||
pass
|
||||
|
||||
return "0"
|
||||
|
||||
|
||||
def _generate_cmem_chunk(self):
|
||||
"""Return a compressed chunk of data representing the compressed
|
||||
image of the zmachine's main memory."""
|
||||
|
||||
### TODO: debug this when ready
|
||||
return "0"
|
||||
|
||||
# XOR the original game image with the current one
|
||||
diffarray = list(self._zmachine._pristine_mem)
|
||||
for index in range(len(self._zmachine._pristine_mem._total_size)):
|
||||
diffarray[index] = self._zmachine._pristine_mem[index] \
|
||||
^ self._zmachine._mem[index]
|
||||
log("XOR array is %s" % diffarray)
|
||||
|
||||
# Run-length encode the resulting list of 0's and 1's.
|
||||
result = []
|
||||
zerocounter = 0
|
||||
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
|
||||
|
||||
|
||||
def _generate_stks_chunk(self):
|
||||
"""Return a stacks chunk, describing the stack state of the
|
||||
zmachine at this moment."""
|
||||
|
||||
### TODO: write this
|
||||
return "0"
|
||||
|
||||
|
||||
def _generate_anno_chunk(self):
|
||||
"""Return an annotation chunk, containing metadata about the ZVM
|
||||
interpreter which created the savefile."""
|
||||
|
||||
### TODO: write this
|
||||
return "0"
|
||||
|
||||
|
||||
#--------- Public APIs -----------
|
||||
|
||||
|
||||
def write(self, savefile_path):
|
||||
"""Write the current zmachine state to a new Quetzal-file at
|
||||
SAVEFILE_PATH."""
|
||||
|
||||
log("Attempting to write game-state to '%s'" % savefile_path)
|
||||
self._file = open(savefile_path, 'w')
|
||||
|
||||
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.")
|
||||
378
src/mudlib/zmachine/trivialzui.py
Normal file
378
src/mudlib/zmachine/trivialzui.py
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
#
|
||||
# A trivial user interface for a Z-Machine that uses (mostly) stdio for
|
||||
# everything and supports little to no optional features.
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
# TODO: There are a few edge-cases in this UI implementation in
|
||||
# regards to word-wrapping. For example, if keyboard input doesn't
|
||||
# terminate in a newline, then word-wrapping can be temporarily thrown
|
||||
# off; the text I/O performed by the audio and filesystem doesn't
|
||||
# really communicate with the screen object, which means that
|
||||
# operations performed by them can temporarily throw off word-wrapping
|
||||
# as well.
|
||||
|
||||
import sys
|
||||
from functools import reduce
|
||||
|
||||
from . import zaudio, zfilesystem, zscreen, zstream, zui
|
||||
from .zlogging import log
|
||||
|
||||
|
||||
class TrivialAudio(zaudio.ZAudio):
|
||||
def __init__(self):
|
||||
zaudio.ZAudio.__init__(self)
|
||||
self.features = {
|
||||
"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("Invalid bleep_type: %s" % str(bleep_type))
|
||||
|
||||
class TrivialScreen(zscreen.ZScreen):
|
||||
def __init__(self):
|
||||
zscreen.ZScreen.__init__(self)
|
||||
self.__styleIsAllUppercase = False
|
||||
|
||||
# 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
|
||||
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)
|
||||
|
||||
class TrivialKeyboardInputStream(zstream.ZInputStream):
|
||||
def __init__(self, screen):
|
||||
zstream.ZInputStream.__init__(self)
|
||||
self.__screen = screen
|
||||
self.features = {
|
||||
"has_timed_input" : False,
|
||||
}
|
||||
|
||||
def read_line(self, original_text=None, max_length=0,
|
||||
terminating_characters=None,
|
||||
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,
|
||||
# because terminating_characters may include characters other than
|
||||
# carriage return.
|
||||
self.__screen.on_input_occurred(newline_occurred=True)
|
||||
|
||||
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)
|
||||
|
||||
class TrivialFilesystem(zfilesystem.ZFilesystem):
|
||||
def __report_io_error(self, exception):
|
||||
sys.stdout.write("FILESYSTEM: An error occurred: %s\n" % exception)
|
||||
|
||||
def save_game(self, data, suggested_filename=None):
|
||||
success = False
|
||||
|
||||
sys.stdout.write("Enter a name for the saved game " \
|
||||
"(hit enter to cancel): ")
|
||||
filename = _read_line(suggested_filename)
|
||||
if filename:
|
||||
try:
|
||||
file_obj = open(filename, "wb")
|
||||
file_obj.write(data)
|
||||
file_obj.close()
|
||||
success = True
|
||||
except OSError as e:
|
||||
self.__report_io_error(e)
|
||||
|
||||
return success
|
||||
|
||||
def restore_game(self):
|
||||
data = None
|
||||
|
||||
sys.stdout.write("Enter the name of the saved game to restore " \
|
||||
"(hit enter to cancel): ")
|
||||
filename = _read_line()
|
||||
if filename:
|
||||
try:
|
||||
file_obj = open(filename, "rb")
|
||||
data = file_obj.read()
|
||||
file_obj.close()
|
||||
except OSError as e:
|
||||
self.__report_io_error(e)
|
||||
|
||||
return data
|
||||
|
||||
def open_transcript_file_for_writing(self):
|
||||
file_obj = None
|
||||
|
||||
sys.stdout.write("Enter a name for the transcript file " \
|
||||
"(hit enter to cancel): ")
|
||||
filename = _read_line()
|
||||
if filename:
|
||||
try:
|
||||
file_obj = open(filename, "w")
|
||||
except OSError as e:
|
||||
self.__report_io_error(e)
|
||||
|
||||
return file_obj
|
||||
|
||||
def open_transcript_file_for_reading(self):
|
||||
file_obj = None
|
||||
|
||||
sys.stdout.write("Enter the name of the transcript file to read " \
|
||||
"(hit enter to cancel): ")
|
||||
filename = _read_line()
|
||||
if filename:
|
||||
try:
|
||||
file_obj = open(filename)
|
||||
except OSError as e:
|
||||
self.__report_io_error(e)
|
||||
|
||||
return file_obj
|
||||
|
||||
def create_zui():
|
||||
"""Creates and returns a ZUI instance representing a trivial user
|
||||
interface."""
|
||||
|
||||
audio = TrivialAudio()
|
||||
screen = TrivialScreen()
|
||||
keyboard_input = TrivialKeyboardInputStream(screen)
|
||||
filesystem = TrivialFilesystem()
|
||||
|
||||
return zui.ZUI(
|
||||
audio,
|
||||
screen,
|
||||
keyboard_input,
|
||||
filesystem
|
||||
)
|
||||
|
||||
# Keyboard input functions
|
||||
|
||||
_INTERRUPT_CHAR = chr(3)
|
||||
_BACKSPACE_CHAR = chr(8)
|
||||
_DELETE_CHAR = chr(127)
|
||||
|
||||
def _win32_read_char():
|
||||
"""Win32-specific function that reads a character of input from the
|
||||
keyboard and returns it without printing it to the screen."""
|
||||
|
||||
import msvcrt
|
||||
|
||||
return str(msvcrt.getch())
|
||||
|
||||
def _unix_read_char():
|
||||
"""Unix-specific function that reads a character of input from the
|
||||
keyboard and returns it without printing it to the screen."""
|
||||
|
||||
# This code was excised from:
|
||||
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/134892
|
||||
|
||||
import termios
|
||||
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)
|
||||
|
||||
def _read_char():
|
||||
"""Reads a character of input from the keyboard and returns it
|
||||
without printing it to the screen."""
|
||||
|
||||
if sys.platform == "win32":
|
||||
_platform_read_char = _win32_read_char
|
||||
else:
|
||||
# We're not running on Windows, so assume we're running on Unix.
|
||||
_platform_read_char = _unix_read_char
|
||||
|
||||
char = _platform_read_char()
|
||||
if char == _INTERRUPT_CHAR:
|
||||
raise KeyboardInterrupt()
|
||||
else:
|
||||
return char
|
||||
|
||||
def _read_line(original_text=None, terminating_characters=None):
|
||||
"""Reads a line of input with the given unicode string of original
|
||||
text, which is editable, and the given unicode string of terminating
|
||||
characters (used to terminate text input). By default,
|
||||
terminating_characters is a string containing the carriage return
|
||||
character ('\r')."""
|
||||
|
||||
if original_text == None:
|
||||
original_text = ""
|
||||
if not terminating_characters:
|
||||
terminating_characters = "\r"
|
||||
|
||||
assert isinstance(original_text, str)
|
||||
assert isinstance(terminating_characters, str)
|
||||
|
||||
chars_entered = len(original_text)
|
||||
sys.stdout.write(original_text)
|
||||
string = original_text
|
||||
finished = False
|
||||
while not finished:
|
||||
char = _read_char()
|
||||
|
||||
if char in (_BACKSPACE_CHAR, _DELETE_CHAR):
|
||||
if chars_entered > 0:
|
||||
chars_entered -= 1
|
||||
string = string[:-1]
|
||||
else:
|
||||
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 = "%s %s" % (_BACKSPACE_CHAR, _BACKSPACE_CHAR)
|
||||
else:
|
||||
char_to_print = char
|
||||
|
||||
sys.stdout.write(char_to_print)
|
||||
return string
|
||||
|
||||
# Word wrapping helper function
|
||||
|
||||
def _word_wrap(text, width):
|
||||
"""
|
||||
A word-wrap function that preserves existing line breaks
|
||||
and most spaces in the text. Expects that existing line
|
||||
breaks are posix newlines (\n).
|
||||
"""
|
||||
|
||||
# This code was taken from:
|
||||
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061
|
||||
|
||||
return reduce(lambda line, word, width=width: '%s%s%s' %
|
||||
(line,
|
||||
' \n'[(len(line)-line.rfind('\n')-1
|
||||
+ len(word.split('\n',1)[0]
|
||||
) >= width)],
|
||||
word),
|
||||
text.split(' ')
|
||||
)
|
||||
76
src/mudlib/zmachine/zaudio.py
Normal file
76
src/mudlib/zmachine/zaudio.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#
|
||||
# A template class representing the audio interface of a z-machine.
|
||||
#
|
||||
# Third-party programs are expected to subclass ZAudio and override
|
||||
# all the methods, then pass an instance of their class to be driven
|
||||
# by the main z-machine engine.
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
# Constants for simple bleeps. These are human-readable names for the
|
||||
# first two sound effect numbers for the Z-Machine's 'sound_effect'
|
||||
# opcode.
|
||||
BLEEP_HIGH = 1
|
||||
BLEEP_LOW = 2
|
||||
|
||||
# Constants for sound effects. These are human-readable names for
|
||||
# the 'effect' operand of the Z-Machine's 'sound_effect' opcode.
|
||||
EFFECT_PREPARE = 1
|
||||
EFFECT_START = 2
|
||||
EFFECT_STOP = 3
|
||||
EFFECT_FINISH = 4
|
||||
|
||||
class ZAudio:
|
||||
def __init__(self):
|
||||
"""Constructor of the audio system."""
|
||||
|
||||
# Subclasses must define real values for all the features they
|
||||
# support (or don't support).
|
||||
|
||||
self.features = {
|
||||
"has_more_than_a_bleep": False,
|
||||
}
|
||||
|
||||
def play_bleep(self, bleep_type):
|
||||
"""Plays a bleep sound of the given type:
|
||||
|
||||
BLEEP_HIGH - a high-pitched bleep
|
||||
BLEEP_LOW - a low-pitched bleep
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def play_sound_effect(self, id, effect, volume, repeats,
|
||||
routine=None):
|
||||
"""The given effect happens to the given sound number. The id
|
||||
must be 3 or above is supplied by the ZAudio object for the
|
||||
particular game in question.
|
||||
|
||||
The effect can be:
|
||||
|
||||
EFFECT_PREPARE - prepare a sound effect for playing
|
||||
EFFECT_START - start a sound effect
|
||||
EFFECT_STOP - stop a sound effect
|
||||
EFFECT_FINISH - finish a sound effect
|
||||
|
||||
The volume is an integer from 1 to 8 (8 being loudest of
|
||||
these). The volume level -1 means 'loudest possible'.
|
||||
|
||||
The repeats specify how many times for the sound to repeatedly
|
||||
play itself, if it is provided.
|
||||
|
||||
The routine, if supplied, is a Python function that will be called
|
||||
once the sound has finished playing. Note that this routine may
|
||||
be called from any thread. The routine should have the following
|
||||
form:
|
||||
|
||||
def on_sound_finished(id)
|
||||
|
||||
where 'id' is the id of the sound that finished playing.
|
||||
|
||||
This method should only be implemented if the
|
||||
has_more_than_a_bleep feature is enabled."""
|
||||
|
||||
raise NotImplementedError()
|
||||
934
src/mudlib/zmachine/zcpu.py
Normal file
934
src/mudlib/zmachine/zcpu.py
Normal file
|
|
@ -0,0 +1,934 @@
|
|||
#
|
||||
# A class which represents the CPU itself, the brain of the virtual
|
||||
# machine. It ties all the systems together and runs the story.
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
import random
|
||||
import time
|
||||
|
||||
from . import bitfield, zopdecoder, zscreen
|
||||
from .zlogging import log, log_disasm
|
||||
|
||||
|
||||
class ZCpuError(Exception):
|
||||
"General exception for Zcpu class"
|
||||
|
||||
|
||||
class ZCpuOpcodeOverlap(ZCpuError):
|
||||
"Overlapping opcodes registered"
|
||||
|
||||
|
||||
class ZCpuIllegalInstruction(ZCpuError):
|
||||
"Illegal instruction encountered"
|
||||
|
||||
|
||||
class ZCpuDivideByZero(ZCpuError):
|
||||
"Divide by zero error"
|
||||
|
||||
|
||||
class ZCpuNotImplemented(ZCpuError):
|
||||
"Opcode not yet implemented"
|
||||
|
||||
|
||||
class ZCpuQuit(ZCpuError):
|
||||
"Quit opcode executed"
|
||||
|
||||
|
||||
class ZCpu:
|
||||
def __init__(
|
||||
self, zmem, zopdecoder, zstack, zobjects, zstring, zstreammanager, zui
|
||||
):
|
||||
self._memory = zmem
|
||||
self._opdecoder = zopdecoder
|
||||
self._stackmanager = zstack
|
||||
self._objects = zobjects
|
||||
self._string = zstring
|
||||
self._streammanager = zstreammanager
|
||||
self._ui = zui
|
||||
|
||||
def _get_handler(self, opcode_class, opcode_number):
|
||||
try:
|
||||
opcode_decl = self.opcodes[opcode_class][opcode_number]
|
||||
except IndexError:
|
||||
opcode_decl = None
|
||||
if not opcode_decl:
|
||||
raise ZCpuIllegalInstruction
|
||||
|
||||
# If the opcode declaration is a sequence, we have extra
|
||||
# thinking to do.
|
||||
if not isinstance(opcode_decl, (list, tuple)):
|
||||
opcode_func = opcode_decl
|
||||
else:
|
||||
# We have several different implementations for the
|
||||
# opcode, and we need to select the right one based on
|
||||
# version.
|
||||
if isinstance(opcode_decl[0], (list, tuple)):
|
||||
for func, version in opcode_decl:
|
||||
if version <= self._memory.version:
|
||||
opcode_func = func
|
||||
break
|
||||
# Only one implementation, check that our machine is
|
||||
# recent enough.
|
||||
elif opcode_decl[1] <= self._memory.version:
|
||||
opcode_func = opcode_decl[0]
|
||||
else:
|
||||
raise ZCpuIllegalInstruction
|
||||
|
||||
# The following is a hack, based on our policy of only
|
||||
# documenting opcodes we implement. If we ever hit an
|
||||
# undocumented opcode, we crash with a not implemented
|
||||
# error.
|
||||
if not opcode_func.__doc__:
|
||||
return False, opcode_func
|
||||
else:
|
||||
return True, opcode_func
|
||||
|
||||
def _make_signed(self, a):
|
||||
"""Turn the given 16-bit value into a signed integer."""
|
||||
assert a < (1 << 16)
|
||||
# This is a little ugly.
|
||||
bf = bitfield.BitField(a)
|
||||
if bf[15]:
|
||||
a = a - (1 << 16)
|
||||
return a
|
||||
|
||||
def _unmake_signed(self, a):
|
||||
"""Turn the given signed integer into a 16-bit value ready for
|
||||
storage."""
|
||||
if a < 0:
|
||||
a = (1 << 16) + a
|
||||
return a
|
||||
|
||||
def _read_variable(self, addr):
|
||||
"""Return the value of the given variable, which can come from
|
||||
the stack, or from a local/global variable. If it comes from
|
||||
the stack, the value is popped from the stack."""
|
||||
if addr == 0x0:
|
||||
return self._stackmanager.pop_stack()
|
||||
elif 0x0 < addr < 0x10:
|
||||
return self._stackmanager.get_local_variable(addr - 1)
|
||||
else:
|
||||
return self._memory.read_global(addr)
|
||||
|
||||
def _write_result(self, result_value, store_addr=None):
|
||||
"""Write the given result value to the stack or to a
|
||||
local/global variable. Write result_value to the store_addr
|
||||
variable, or if None, extract the destination variable from
|
||||
the opcode."""
|
||||
if store_addr is None:
|
||||
result_addr = self._opdecoder.get_store_address()
|
||||
else:
|
||||
result_addr = store_addr
|
||||
|
||||
if result_addr is not None:
|
||||
if result_addr == 0x0:
|
||||
log(f"Push {result_value} to stack")
|
||||
self._stackmanager.push_stack(result_value)
|
||||
elif 0x0 < result_addr < 0x10:
|
||||
log(f"Local variable {result_addr - 1} = {result_value}")
|
||||
self._stackmanager.set_local_variable(result_addr - 1, result_value)
|
||||
else:
|
||||
log(f"Global variable {result_addr} = {result_value}")
|
||||
self._memory.write_global(result_addr, result_value)
|
||||
|
||||
def _call(self, routine_address, args, store_return_value):
|
||||
"""Set up a function call to the given routine address,
|
||||
passing the given arguments. If store_return_value is True,
|
||||
the routine's return value will be stored."""
|
||||
addr = self._memory.packed_address(routine_address)
|
||||
if store_return_value:
|
||||
return_value = self._opdecoder.get_store_address()
|
||||
else:
|
||||
return_value = None
|
||||
current_addr = self._opdecoder.program_counter
|
||||
new_addr = self._stackmanager.start_routine(
|
||||
addr, return_value, current_addr, args
|
||||
)
|
||||
self._opdecoder.program_counter = new_addr
|
||||
|
||||
def _branch(self, test_result):
|
||||
"""Retrieve the branch information, and set the instruction
|
||||
pointer according to the type of branch and the test_result."""
|
||||
branch_cond, branch_offset = self._opdecoder.get_branch_offset()
|
||||
|
||||
if test_result == branch_cond:
|
||||
if branch_offset == 0 or branch_offset == 1:
|
||||
log(f"Return from routine with {branch_offset}")
|
||||
addr = self._stackmanager.finish_routine(branch_offset)
|
||||
self._opdecoder.program_counter = addr
|
||||
else:
|
||||
log(f"Jump to offset {branch_offset:+d}")
|
||||
self._opdecoder.program_counter += branch_offset - 2
|
||||
|
||||
def run(self):
|
||||
"""The Magic Function that takes little bits and bytes, twirls
|
||||
them around, and brings the magic to your screen!"""
|
||||
log("Execution started")
|
||||
while True:
|
||||
current_pc = self._opdecoder.program_counter
|
||||
log(f"Reading next opcode at address {current_pc:x}")
|
||||
(opcode_class, opcode_number, operands) = (
|
||||
self._opdecoder.get_next_instruction()
|
||||
)
|
||||
implemented, func = self._get_handler(opcode_class, opcode_number)
|
||||
log_disasm(
|
||||
current_pc,
|
||||
zopdecoder.OPCODE_STRINGS[opcode_class],
|
||||
opcode_number,
|
||||
func.__name__,
|
||||
", ".join([str(x) for x in operands]),
|
||||
)
|
||||
if not implemented:
|
||||
log(f"Unimplemented opcode {func.__name__}, halting execution")
|
||||
break
|
||||
|
||||
# The returned function is unbound, so we must pass
|
||||
# self to it ourselves.
|
||||
func(self, *operands)
|
||||
|
||||
##
|
||||
## Opcode implementation functions start here.
|
||||
##
|
||||
|
||||
## 2OP opcodes (opcodes 1-127 and 192-223)
|
||||
def op_je(self, a, *others):
|
||||
"""Branch if the first argument is equal to any subsequent
|
||||
arguments. Note that the second operand may be absent, in
|
||||
which case there is no jump."""
|
||||
for b in others:
|
||||
if a == b:
|
||||
self._branch(True)
|
||||
return
|
||||
|
||||
# Fallthrough: No args were equal to a.
|
||||
self._branch(False)
|
||||
|
||||
def op_jl(self, a, *others):
|
||||
"""Branch if the first argument is less than any subsequent
|
||||
argument. Note that the second operand may be absent, in
|
||||
which case there is no jump."""
|
||||
for b in others:
|
||||
if a < b:
|
||||
self._branch(True)
|
||||
return
|
||||
|
||||
# Fallthrough: No args were greater than a.
|
||||
self._branch(False)
|
||||
|
||||
def op_jg(self, a, b):
|
||||
"""Branch if the first argument is greater than the second."""
|
||||
a = self._make_signed(a)
|
||||
b = self._make_signed(b)
|
||||
if a > b:
|
||||
self._branch(True)
|
||||
else:
|
||||
self._branch(False)
|
||||
|
||||
def op_dec_chk(self, variable, test_value):
|
||||
"""Decrement the variable, and branch if the value becomes
|
||||
less than test_value."""
|
||||
val = self._read_variable(variable)
|
||||
val = (val - 1) % 65536
|
||||
self._write_result(val, store_addr=variable)
|
||||
self._branch(val < test_value)
|
||||
|
||||
def op_inc_chk(self, variable, test_value):
|
||||
"""Increment the variable, and branch if the value becomes
|
||||
greater than the test value."""
|
||||
val = self._read_variable(variable)
|
||||
val = (val + 1) % 65536
|
||||
self._write_result(val, store_addr=variable)
|
||||
self._branch(val > test_value)
|
||||
|
||||
def op_jin(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_test(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_or(self, a, b):
|
||||
"""Bitwise OR between the two arguments."""
|
||||
self._write_result(a | b)
|
||||
|
||||
def op_and(self, a, b):
|
||||
"""Bitwise AND between the two arguments."""
|
||||
self._write_result(a & b)
|
||||
|
||||
def op_test_attr(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_set_attr(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_clear_attr(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_store(self, variable, value):
|
||||
"""Store the given value to the given variable."""
|
||||
self._write_result(value, store_addr=variable)
|
||||
|
||||
def op_insert_obj(self, object, dest):
|
||||
"""Move object OBJECT to become the first child of object
|
||||
DEST. After the move, the prior first child of DEST is now
|
||||
the OBJECT's sibling."""
|
||||
self._objects.insert_object(dest, object)
|
||||
|
||||
def op_loadw(self, base, offset):
|
||||
"""Store in the given result register the word value at
|
||||
(base+2*offset)."""
|
||||
val = self._memory.read_word(base + 2 * offset)
|
||||
self._write_result(val)
|
||||
|
||||
def op_loadb(self, base, offset):
|
||||
"""Store in the given result register the byte value at
|
||||
(base+offset)."""
|
||||
val = self._memory[base + offset]
|
||||
self._write_result(val)
|
||||
|
||||
def op_get_prop(self, objectnum, propnum):
|
||||
"""Store in the given result an object's property value
|
||||
(either a byte or word)."""
|
||||
val = self._objects.get_prop(objectnum, propnum)
|
||||
self._write_result(val)
|
||||
|
||||
def op_get_prop_addr(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_get_next_prop(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_add(self, a, b):
|
||||
"""Signed 16-bit addition."""
|
||||
result = self._unmake_signed(self._make_signed(a) + self._make_signed(b))
|
||||
self._write_result(result)
|
||||
|
||||
def op_sub(self, a, b):
|
||||
"""Signed 16-bit subtraction"""
|
||||
result = self._unmake_signed(self._make_signed(a) - self._make_signed(b))
|
||||
self._write_result(result)
|
||||
|
||||
def op_mul(self, a, b):
|
||||
"""Signed 16-bit multiplication."""
|
||||
result = self._unmake_signed(self._make_signed(a) * self._make_signed(b))
|
||||
self._write_result(result)
|
||||
|
||||
def op_div(self, a, b):
|
||||
"""Signed 16-bit division."""
|
||||
a = self._make_signed(a)
|
||||
b = self._make_signed(b)
|
||||
if b == 0:
|
||||
raise ZCpuDivideByZero
|
||||
self._write_result(self._unmake_signed(a // b))
|
||||
|
||||
def op_mod(self, a, b):
|
||||
"""Signed 16-bit modulo (remainder after division)."""
|
||||
a = self._make_signed(a)
|
||||
b = self._make_signed(b)
|
||||
if b == 0:
|
||||
raise ZCpuDivideByZero
|
||||
# Z-machine uses truncation toward zero, not Python's floor division
|
||||
quotient = int(a / b)
|
||||
if quotient < 0 and quotient < a / b:
|
||||
quotient += 1
|
||||
if quotient > 0 and quotient > a / b:
|
||||
quotient -= 1
|
||||
remainder = a - (quotient * b)
|
||||
self._write_result(self._unmake_signed(remainder))
|
||||
|
||||
def op_call_2s(self, routine_addr, arg1):
|
||||
"""Call routine(arg1) and store the result."""
|
||||
self._call(routine_addr, [arg1], True)
|
||||
|
||||
def op_call_2n(self, routine_addr, arg1):
|
||||
"""Call routine(arg1) and throw away the result."""
|
||||
self._call(routine_addr, [arg1], False)
|
||||
|
||||
def op_set_colour(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_throw(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
## 1OP opcodes (opcodes 128-175)
|
||||
|
||||
def op_jz(self, val):
|
||||
"""Branch if the val is zero."""
|
||||
self._branch(val == 0)
|
||||
|
||||
def op_get_sibling(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_get_child(self, object_num):
|
||||
"""Get and store the first child of the given object."""
|
||||
self._write_result(self._objects.get_child(object_num))
|
||||
|
||||
def op_get_parent(self, object_num):
|
||||
"""Get and store the parent of the given object."""
|
||||
self._write_result(self._objects.get_parent(object_num))
|
||||
|
||||
def op_get_prop_len(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_inc(self, variable):
|
||||
"""Increment the given value."""
|
||||
val = self._read_variable(variable)
|
||||
val = (val + 1) % 65536
|
||||
self._write_result(val, store_addr=variable)
|
||||
|
||||
def op_dec(self, variable):
|
||||
"""Decrement the given variable."""
|
||||
val = self._read_variable(variable)
|
||||
val = self._make_signed(val)
|
||||
val = val - 1
|
||||
val = self._unmake_signed(val)
|
||||
self._write_result(val, store_addr=variable)
|
||||
|
||||
def op_print_addr(self, string_byte_address):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_call_1s(self, routine_address):
|
||||
"""Call the given routine and store the return value."""
|
||||
self._call(routine_address, [], True)
|
||||
|
||||
def op_remove_obj(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_print_obj(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_ret(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_jump(self, offset):
|
||||
"""Jump unconditionally to the given branch offset. This
|
||||
opcode does not follow the usual branch decision algorithm,
|
||||
and so we do not call the _branch method to dispatch the call."""
|
||||
|
||||
old_pc = self._opdecoder.program_counter
|
||||
|
||||
# The offset to the jump instruction is known to be a 2-byte
|
||||
# signed integer. We need to make it signed before applying
|
||||
# the offset.
|
||||
if offset >= (1 << 15):
|
||||
offset = -(1 << 16) + offset
|
||||
log(f"Jump unconditionally to relative offset {offset}")
|
||||
|
||||
# Apparently reading the 2 bytes of operand *isn't* supposed
|
||||
# to increment the PC, thus we need to apply this offset to PC
|
||||
# that's still pointing at the 'jump' opcode. Hence the -2
|
||||
# modifier below.
|
||||
new_pc = self._opdecoder.program_counter + offset - 2
|
||||
self._opdecoder.program_counter = new_pc
|
||||
log(f"PC has changed from from {old_pc:x} to {new_pc:x}")
|
||||
|
||||
def op_print_paddr(self, string_paddr):
|
||||
"""Print the string at the given packed address."""
|
||||
zstr_address = self._memory.packed_address(string_paddr)
|
||||
self._ui.screen.write(self._string.get(zstr_address))
|
||||
|
||||
def op_load(self, variable):
|
||||
"""Load the value of the given variable and store it."""
|
||||
value = self._read_variable(variable)
|
||||
self._write_result(value)
|
||||
|
||||
def op_not(self, value):
|
||||
"""Bitwise NOT of the given value."""
|
||||
result = ~value & 0xFFFF
|
||||
self._write_result(result)
|
||||
|
||||
def op_call_1n(self, routine_addr):
|
||||
"""Call the given routine, and discard the return value."""
|
||||
self._call(routine_addr, [], False)
|
||||
|
||||
## 0OP opcodes (opcodes 176-191)
|
||||
|
||||
def op_rtrue(self, *args):
|
||||
"""Make the current routine return true (1)."""
|
||||
pc = self._stackmanager.finish_routine(1)
|
||||
self._opdecoder.program_counter = pc
|
||||
|
||||
def op_rfalse(self, *args):
|
||||
"""Make the current routine return false (0)."""
|
||||
pc = self._stackmanager.finish_routine(0)
|
||||
self._opdecoder.program_counter = pc
|
||||
|
||||
def op_print(self):
|
||||
"""Print the embedded ZString."""
|
||||
zstr_address = self._opdecoder.get_zstring()
|
||||
self._ui.screen.write(self._string.get(zstr_address))
|
||||
|
||||
def op_print_ret(self):
|
||||
"""TODO: Write docstring here."""
|
||||
self.op_print()
|
||||
self.op_rtrue()
|
||||
|
||||
def op_nop(self, *args):
|
||||
"""Do nothing."""
|
||||
pass
|
||||
|
||||
def op_save(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_save_v4(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_restore(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_restore_v4(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_restart(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_ret_popped(self, *args):
|
||||
"""Pop a value from the stack and return it from the current routine."""
|
||||
value = self._stackmanager.pop_stack()
|
||||
pc = self._stackmanager.finish_routine(value)
|
||||
self._opdecoder.program_counter = pc
|
||||
|
||||
def op_pop(self, *args):
|
||||
"""Pop and discard the top value from the stack."""
|
||||
self._stackmanager.pop_stack()
|
||||
|
||||
def op_catch(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_quit(self, *args):
|
||||
"""Quit the game."""
|
||||
raise ZCpuQuit
|
||||
|
||||
def op_new_line(self, *args):
|
||||
"""Print a newline."""
|
||||
self._ui.screen.write("\n")
|
||||
|
||||
def op_show_status(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_verify(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_piracy(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
## VAR opcodes (opcodes 224-255)
|
||||
|
||||
# call in v1-3
|
||||
def op_call(self, routine_addr, *args):
|
||||
"""Call the routine r1, passing it any of r2, r3, r4 if defined."""
|
||||
addr = self._memory.packed_address(routine_addr)
|
||||
return_addr = self._opdecoder.get_store_address()
|
||||
current_addr = self._opdecoder.program_counter
|
||||
new_addr = self._stackmanager.start_routine(
|
||||
addr, return_addr, current_addr, args
|
||||
)
|
||||
self._opdecoder.program_counter = new_addr
|
||||
|
||||
def op_call_vs(self, routine_addr, *args):
|
||||
"""See op_call."""
|
||||
self.op_call(routine_addr, *args)
|
||||
|
||||
def op_storew(self, array, offset, value):
|
||||
"""Store the given 16-bit value at array+2*byte_index."""
|
||||
store_address = array + 2 * offset
|
||||
self._memory.write_word(store_address, value)
|
||||
|
||||
def op_storeb(self, array, byte_index, value):
|
||||
"""Store the given byte value at array+byte_index."""
|
||||
self._memory[array + byte_index] = value & 0xFF
|
||||
|
||||
def op_put_prop(self, object_number, property_number, value):
|
||||
"""Set an object's property to the given value."""
|
||||
self._objects.set_property(object_number, property_number, value)
|
||||
|
||||
def op_sread(self, *args):
|
||||
"""Not implemented yet, but documented so that the detection
|
||||
code will be foiled."""
|
||||
|
||||
def op_sread_v4(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_aread(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_print_char(self, char):
|
||||
"""Output the given ZSCII character."""
|
||||
self._ui.screen.write(self._string.zscii.get([char]))
|
||||
|
||||
def op_print_num(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_random(self, n):
|
||||
"""Generate a random number, or seed the PRNG.
|
||||
|
||||
If the input is positive, generate a uniformly random number
|
||||
in the range [1:input]. If the input is negative, seed the
|
||||
PRNG with that value. If the input is zero, seed the PRNG with
|
||||
the current time.
|
||||
"""
|
||||
result = 0
|
||||
if n > 0:
|
||||
log(f"Generate random number in [1:{n}]")
|
||||
result = random.randint(1, n)
|
||||
elif n < 0:
|
||||
log(f"Seed PRNG with {n}")
|
||||
random.seed(n)
|
||||
else:
|
||||
log("Seed PRNG with time")
|
||||
random.seed(time.time())
|
||||
self._write_result(result)
|
||||
|
||||
def op_push(self, value):
|
||||
"""Push a value onto the current routine's game stack."""
|
||||
self._stackmanager.push_stack(value)
|
||||
|
||||
def op_pull(self, variable):
|
||||
"""Pop a value from the stack and store it in the given variable."""
|
||||
value = self._stackmanager.pop_stack()
|
||||
self._write_result(value, store_addr=variable)
|
||||
|
||||
def op_split_window(self, height):
|
||||
"""Split or unsplit the window horizontally."""
|
||||
self._ui.screen.split_window(height)
|
||||
|
||||
def op_set_window(self, window_num):
|
||||
"""Set the given window as the active window."""
|
||||
self._ui.screen.select_window(window_num)
|
||||
|
||||
def op_call_vs2(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_erase_window(self, window_number):
|
||||
"""Clear the window with the given number. If # is -1, unsplit
|
||||
all and clear (full reset). If # is -2, clear all but don't
|
||||
unsplit."""
|
||||
if window_number == -1:
|
||||
self.op_split_window(0)
|
||||
self._ui.screen.erase_window(zscreen.WINDOW_LOWER)
|
||||
if window_number == -2:
|
||||
self._ui.screen.erase_window(zscreen.WINDOW_LOWER)
|
||||
self._ui.screen.erase_window(zscreen.WINDOW_UPPER)
|
||||
else:
|
||||
self._ui.screen.erase_window(window_number)
|
||||
|
||||
def op_erase_line(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_set_cursor(self, x, y):
|
||||
"""Set the cursor position within the active window."""
|
||||
self._ui.screen.set_cursor_position(x, y)
|
||||
|
||||
def op_get_cursor(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_set_text_style(self, text_style):
|
||||
"""Set the text style."""
|
||||
self._ui.screen.set_text_style(text_style)
|
||||
|
||||
def op_buffer_mode(self, flag):
|
||||
"""If set to 1, text output on the lower window in stream 1 is
|
||||
buffered up so that it can be word-wrapped properly. If set to
|
||||
0, it isn't."""
|
||||
|
||||
self._ui.screen.buffer_mode = bool(flag)
|
||||
|
||||
def op_output_stream(self, stream_num):
|
||||
"""Enable or disable the given stream.
|
||||
|
||||
This is the v3/4 implementation of the opcode, which just
|
||||
delegates to the backwards compatible v5 implementation.
|
||||
"""
|
||||
self.op_output_stream_v5(stream_num)
|
||||
|
||||
def op_output_stream_v5(self, stream_num, table=None):
|
||||
"""Enable or disable the given output stream."""
|
||||
stream_num = self._make_signed(stream_num)
|
||||
if stream_num < 0:
|
||||
self._streammanager.output.unselect(-stream_num)
|
||||
else:
|
||||
self._streammanager.output.select(stream_num)
|
||||
|
||||
def op_input_stream(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
# This one may have been used prematurely in v3 stories. Keep an
|
||||
# eye out for it if we ever get bug reports.
|
||||
def op_sound_effect(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_read_char(self, unused, time, input_routine):
|
||||
"""Read a single character from input stream 0 (keyboard).
|
||||
|
||||
Optionally, call a routine periodically to decide whether or
|
||||
not to interrupt user input.
|
||||
"""
|
||||
# According to the spec, the first argument is always one, and
|
||||
# exists only for Historical Reasons(tm)
|
||||
assert unused == 1
|
||||
|
||||
# TODO: shiny timer stuff not implemented yet.
|
||||
if time != 0 or input_routine != 0:
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
char = self._ui.keyboard_input.read_char()
|
||||
self._write_result(char)
|
||||
|
||||
def op_scan_table(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_not_v5(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_call_vn(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_call_vn2(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_tokenize(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_encode_text(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_copy_table(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_print_table(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_check_arg_count(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
## EXT opcodes (opcodes 256-284)
|
||||
|
||||
def op_save_v5(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_restore_v5(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_log_shift(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_art_shift(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_set_font(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_save_undo(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_restore_undo(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_print_unicode(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
def op_check_unicode(self, *args):
|
||||
"""TODO: Write docstring here."""
|
||||
raise ZCpuNotImplemented
|
||||
|
||||
# Declaration of the opcode tables. In a Z-Machine, opcodes are
|
||||
# divided into tables based on the operand type. Within each
|
||||
# table, the operand is then indexed by its number. We preserve
|
||||
# that organization in this opcode table.
|
||||
#
|
||||
# The opcode table is a dictionary mapping an operand type to a
|
||||
# list of opcodes definitions. Each opcode definition's index in
|
||||
# the table is the opcode number within that opcode table.
|
||||
#
|
||||
# The opcodes are in one of three forms:
|
||||
#
|
||||
# - If the opcode is available and unchanging in all versions,
|
||||
# then the definition is simply the function implementing the
|
||||
# opcode.
|
||||
#
|
||||
# - If the opcode is only available as of a certain version
|
||||
# upwards, then the definition is the tuple (opcode_func,
|
||||
# first_version), where first_version is the version of the
|
||||
# Z-machine where the opcode appeared.
|
||||
#
|
||||
# - If the opcode changes meaning with successive revisions of the
|
||||
# Z-machine, then the definition is a list of the above tuples,
|
||||
# sorted in descending order (tuple with the highest
|
||||
# first_version comes first). If an instruction became illegal
|
||||
# after a given version, it should have a tuple with the opcode
|
||||
# function set to None.
|
||||
|
||||
opcodes = {
|
||||
# 2OP opcodes
|
||||
zopdecoder.OPCODE_2OP: [
|
||||
None,
|
||||
op_je,
|
||||
op_jl,
|
||||
op_jg,
|
||||
op_dec_chk,
|
||||
op_inc_chk,
|
||||
op_jin,
|
||||
op_test,
|
||||
op_or,
|
||||
op_and,
|
||||
op_test_attr,
|
||||
op_set_attr,
|
||||
op_clear_attr,
|
||||
op_store,
|
||||
op_insert_obj,
|
||||
op_loadw,
|
||||
op_loadb,
|
||||
op_get_prop,
|
||||
op_get_prop_addr,
|
||||
op_get_next_prop,
|
||||
op_add,
|
||||
op_sub,
|
||||
op_mul,
|
||||
op_div,
|
||||
op_mod,
|
||||
(op_call_2s, 4),
|
||||
(op_call_2n, 5),
|
||||
(op_set_colour, 5),
|
||||
(op_throw, 5),
|
||||
],
|
||||
# 1OP opcodes
|
||||
zopdecoder.OPCODE_1OP: [
|
||||
op_jz,
|
||||
op_get_sibling,
|
||||
op_get_child,
|
||||
op_get_parent,
|
||||
op_get_prop_len,
|
||||
op_inc,
|
||||
op_dec,
|
||||
op_print_addr,
|
||||
(op_call_1s, 4),
|
||||
op_remove_obj,
|
||||
op_print_obj,
|
||||
op_ret,
|
||||
op_jump,
|
||||
op_print_paddr,
|
||||
op_load,
|
||||
[(op_call_1n, 5), (op_not, 1)],
|
||||
],
|
||||
# 0OP opcodes
|
||||
zopdecoder.OPCODE_0OP: [
|
||||
op_rtrue,
|
||||
op_rfalse,
|
||||
op_print,
|
||||
op_print_ret,
|
||||
op_nop,
|
||||
[(None, 5), (op_save_v4, 4), (op_save, 1)],
|
||||
[(None, 5), (op_restore_v4, 4), (op_restore, 1)],
|
||||
op_restart,
|
||||
op_ret_popped,
|
||||
[(op_catch, 5), (op_pop, 1)],
|
||||
op_quit,
|
||||
op_new_line,
|
||||
[(None, 4), (op_show_status, 3)],
|
||||
(op_verify, 3),
|
||||
None, # Padding. Opcode 0OP:E is the extended opcode marker.
|
||||
(op_piracy, 5),
|
||||
],
|
||||
# VAR opcodes
|
||||
zopdecoder.OPCODE_VAR: [
|
||||
[(op_call_vs, 4), (op_call, 1)],
|
||||
op_storew,
|
||||
op_storeb,
|
||||
op_put_prop,
|
||||
[(op_aread, 5), (op_sread_v4, 4), (op_sread, 1)],
|
||||
op_print_char,
|
||||
op_print_num,
|
||||
op_random,
|
||||
op_push,
|
||||
op_pull,
|
||||
(op_split_window, 3),
|
||||
(op_set_window, 3),
|
||||
(op_call_vs2, 4),
|
||||
(op_erase_window, 4),
|
||||
(op_erase_line, 4),
|
||||
(op_set_cursor, 4),
|
||||
(op_get_cursor, 4),
|
||||
(op_set_text_style, 4),
|
||||
(op_buffer_mode, 4),
|
||||
[(op_output_stream_v5, 5), (op_output_stream, 3)],
|
||||
(op_input_stream, 3),
|
||||
(op_sound_effect, 5),
|
||||
(op_read_char, 4),
|
||||
(op_scan_table, 4),
|
||||
(op_not, 5),
|
||||
(op_call_vn, 5),
|
||||
(op_call_vn2, 5),
|
||||
(op_tokenize, 5),
|
||||
(op_encode_text, 5),
|
||||
(op_copy_table, 5),
|
||||
(op_print_table, 5),
|
||||
(op_check_arg_count, 5),
|
||||
],
|
||||
# EXT opcodes
|
||||
zopdecoder.OPCODE_EXT: [
|
||||
(op_save_v5, 5),
|
||||
(op_restore_v5, 5),
|
||||
(op_log_shift, 5),
|
||||
(op_art_shift, 5),
|
||||
(op_set_font, 5),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
(op_save_undo, 5),
|
||||
(op_restore_undo, 5),
|
||||
(op_print_unicode, 5),
|
||||
(op_check_unicode, 5),
|
||||
],
|
||||
}
|
||||
65
src/mudlib/zmachine/zfilesystem.py
Normal file
65
src/mudlib/zmachine/zfilesystem.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#
|
||||
# A template class representing the interactions that the end-user has
|
||||
# with the filesystem in a z-machine.
|
||||
#
|
||||
# Third-party programs are expected to subclass ZFilesystem and
|
||||
# override all the methods, then pass an instance of their class to be
|
||||
# driven by the main z-machine engine.
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
class ZFilesystem:
|
||||
"""Encapsulates the interactions that the end-user has with the
|
||||
filesystem."""
|
||||
|
||||
def save_game(self, data, suggested_filename=None):
|
||||
"""Prompt for a filename (possibly using suggested_filename), and
|
||||
attempt to write DATA as a saved-game file. Return True on
|
||||
success, False on failure.
|
||||
|
||||
Note that file-handling errors such as 'disc corrupt' and 'disc
|
||||
full' should be reported directly to the player by the method in
|
||||
question method, and they should also cause this function to
|
||||
return False. If the user clicks 'cancel' or its equivalent,
|
||||
this function should return False."""
|
||||
|
||||
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.
|
||||
|
||||
Note that file-handling errors such as 'disc corrupt' and 'disc
|
||||
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."""
|
||||
|
||||
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()
|
||||
142
src/mudlib/zmachine/zlexer.py
Normal file
142
src/mudlib/zmachine/zlexer.py
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
#
|
||||
# A class for parsing word dictionaries and performing lexical
|
||||
# analysis of user input. (See section 13 of the z-machine spec.)
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
import re
|
||||
|
||||
from .zstring import ZsciiTranslator, ZStringFactory
|
||||
|
||||
|
||||
class ZLexerError(Exception):
|
||||
"General exception for ZLexer class"
|
||||
|
||||
# Note that the specification describes tokenisation as a process
|
||||
# whereby the user's input is divided into words, each word converted
|
||||
# to a z-string, then searched for in the 'standard' dictionary. This
|
||||
# is really inefficient. Therefore, because the standard dictionary
|
||||
# is immutable (lives in static memory), this class parses and loads
|
||||
# it *once* into a private python dictionary. We can then forever do
|
||||
# O(1) lookups of unicode words, rather than O(N) lookups of
|
||||
# zscii-encoded words.
|
||||
|
||||
# Note that the main API here (tokenise_input()) can work with any
|
||||
# dictionary, not just the standard one.
|
||||
|
||||
class ZLexer:
|
||||
|
||||
def __init__(self, mem):
|
||||
|
||||
self._memory = mem
|
||||
self._stringfactory = ZStringFactory(self._memory)
|
||||
self._zsciitranslator = ZsciiTranslator(self._memory)
|
||||
|
||||
# Load and parse game's 'standard' dictionary from static memory.
|
||||
dict_addr = self._memory.read_word(0x08)
|
||||
self._num_entries, self._entry_length, self._separators, entries_addr = \
|
||||
self._parse_dict_header(dict_addr)
|
||||
self._dict = self.get_dictionary(dict_addr)
|
||||
|
||||
|
||||
def _parse_dict_header(self, address):
|
||||
"""Parse the header of the dictionary at ADDRESS. Return the
|
||||
number of entries, the length of each entry, a list of zscii
|
||||
word separators, and an address of the beginning the entries."""
|
||||
|
||||
addr = address
|
||||
num_separators = self._memory[addr]
|
||||
separators = self._memory[(addr + 1):(addr + num_separators)]
|
||||
addr += (1 + num_separators)
|
||||
entry_length = self._memory[addr]
|
||||
addr += 1
|
||||
num_entries = self._memory.read_word(addr)
|
||||
addr += 2
|
||||
|
||||
return num_entries, entry_length, separators, addr
|
||||
|
||||
|
||||
def _tokenise_string(self, string, separators):
|
||||
"""Split unicode STRING into a list of words, and return the list.
|
||||
Whitespace always counts as a word separator, but so do any
|
||||
unicode characters provided in the list of SEPARATORS. Note,
|
||||
however, that instances of these separators caunt as words
|
||||
themselves."""
|
||||
|
||||
# re.findall(r'[,.;]|\w+', 'abc, def')
|
||||
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)
|
||||
|
||||
|
||||
#--------- Public APIs -----------
|
||||
|
||||
|
||||
def get_dictionary(self, address):
|
||||
"""Load a z-machine-format dictionary at ADDRESS -- which maps
|
||||
zstrings to bytestrings -- into a python dictionary which maps
|
||||
unicode strings to the address of the word in the original
|
||||
dictionary. Return the new dictionary."""
|
||||
|
||||
dict = {}
|
||||
|
||||
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
|
||||
|
||||
return dict
|
||||
|
||||
|
||||
def parse_input(self, string, dict_addr=None):
|
||||
"""Given a unicode string, parse it into words based on a dictionary.
|
||||
|
||||
if DICT_ADDR is provided, use the custom dictionary at that
|
||||
address to do the analysis, otherwise default to using the game's
|
||||
'standard' dictionary.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
44
src/mudlib/zmachine/zlogging.py
Normal file
44
src/mudlib/zmachine/zlogging.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#
|
||||
# Logging assistance. This provides a logging facility for the rest of
|
||||
# the Z-Machine. As a Z-Machine is inherently I/O intensive, dumb screen
|
||||
# dumping is no longer adequate. This logging facility, based on
|
||||
# python's logging module, provides file logging.
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
# Top-level initialization
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
# Create the logging objects regardless. If debugmode is False, then
|
||||
# they won't actually do anything when used.
|
||||
mainlog = logging.FileHandler('debug.log', 'a')
|
||||
mainlog.setLevel(logging.DEBUG)
|
||||
mainlog.setFormatter(logging.Formatter('%(asctime)s: %(message)s'))
|
||||
logging.getLogger('mainlog').addHandler(mainlog)
|
||||
|
||||
# We'll store the disassembly in a separate file, for better
|
||||
# readability.
|
||||
disasm = logging.FileHandler('disasm.log', 'a')
|
||||
disasm.setLevel(logging.DEBUG)
|
||||
disasm.setFormatter(logging.Formatter('%(message)s'))
|
||||
logging.getLogger('disasm').addHandler(disasm)
|
||||
|
||||
mainlog = logging.getLogger('mainlog')
|
||||
mainlog.info('*** Log reopened ***')
|
||||
disasm = logging.getLogger('disasm')
|
||||
disasm.info('*** Log reopened ***')
|
||||
|
||||
# Pubilc routines used by other modules
|
||||
def set_debug(state):
|
||||
if state:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.getLogger().setLevel(logging.CRITICAL)
|
||||
|
||||
def log(msg):
|
||||
mainlog.debug(msg)
|
||||
|
||||
def log_disasm(pc, opcode_type, opcode_num, opcode_name, args):
|
||||
disasm.debug("%06x %s:%02x %s %s" % (pc, opcode_type, opcode_num,
|
||||
opcode_name, args))
|
||||
42
src/mudlib/zmachine/zmachine.py
Normal file
42
src/mudlib/zmachine/zmachine.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# The Z-Machine black box. It initializes the whole Z computer, loads
|
||||
# a story, and starts execution of the cpu.
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
from . import zlogging
|
||||
from .zcpu import ZCpu
|
||||
from .zmemory import ZMemory
|
||||
from .zobjectparser import ZObjectParser
|
||||
from .zopdecoder import ZOpDecoder
|
||||
from .zstackmanager import ZStackManager
|
||||
from .zstreammanager import ZStreamManager
|
||||
from .zstring import ZStringFactory
|
||||
|
||||
|
||||
class ZMachineError(Exception):
|
||||
"""General exception for ZMachine class"""
|
||||
|
||||
class ZMachine:
|
||||
"""The Z-Machine black box."""
|
||||
|
||||
def __init__(self, story, ui, debugmode=False):
|
||||
zlogging.set_debug(debugmode)
|
||||
self._pristine_mem = ZMemory(story) # the original memory image
|
||||
self._mem = ZMemory(story) # the memory image which changes during play
|
||||
self._stringfactory = ZStringFactory(self._mem)
|
||||
self._objectparser = ZObjectParser(self._mem)
|
||||
self._stackmanager = ZStackManager(self._mem)
|
||||
self._opdecoder = ZOpDecoder(self._mem, self._stackmanager)
|
||||
self._opdecoder.program_counter = self._mem.read_word(0x06)
|
||||
self._ui = ui
|
||||
self._stream_manager = ZStreamManager(self._mem, self._ui)
|
||||
self._cpu = ZCpu(self._mem, self._opdecoder, self._stackmanager,
|
||||
self._objectparser, self._stringfactory,
|
||||
self._stream_manager, self._ui)
|
||||
|
||||
#--------- Public APIs -----------
|
||||
|
||||
def run(self):
|
||||
return self._cpu.run()
|
||||
278
src/mudlib/zmachine/zmemory.py
Normal file
278
src/mudlib/zmachine/zmemory.py
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
#
|
||||
# A class which represents the z-machine's main memory bank.
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
from . import bitfield
|
||||
from .zlogging import log
|
||||
|
||||
# This class that represents the "main memory" of the z-machine. It's
|
||||
# readable and writable through normal indexing and slice notation,
|
||||
# just like a typical python 'sequence' object (e.g. mem[342] and
|
||||
# mem[22:90]). The class validates memory layout, enforces read-only
|
||||
# areas of memory, and also the ability to return both word-addresses
|
||||
# and 'packed' addresses.
|
||||
|
||||
|
||||
class ZMemoryError(Exception):
|
||||
"General exception for ZMemory class"
|
||||
pass
|
||||
|
||||
class ZMemoryIllegalWrite(ZMemoryError):
|
||||
"Tried to write to a read-only part of memory"
|
||||
def __init__(self, address):
|
||||
super().__init__(
|
||||
"Illegal write to address %d" % address)
|
||||
|
||||
class ZMemoryBadInitialization(ZMemoryError):
|
||||
"Failure to initialize ZMemory class"
|
||||
pass
|
||||
|
||||
class ZMemoryOutOfBounds(ZMemoryError):
|
||||
"Accessed an address beyond the bounds of memory."
|
||||
pass
|
||||
|
||||
class ZMemoryBadMemoryLayout(ZMemoryError):
|
||||
"Static plus dynamic memory exceeds 64k"
|
||||
pass
|
||||
|
||||
class ZMemoryBadStoryfileSize(ZMemoryError):
|
||||
"Story is too large for Z-machine version."
|
||||
pass
|
||||
|
||||
class ZMemoryUnsupportedVersion(ZMemoryError):
|
||||
"Unsupported version of Z-story file."
|
||||
pass
|
||||
|
||||
|
||||
class ZMemory:
|
||||
|
||||
# A list of 64 tuples describing who's allowed to tweak header-bytes.
|
||||
# Index into the list is the header-byte being tweaked.
|
||||
# List value is a tuple of the form
|
||||
#
|
||||
# [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.
|
||||
|
||||
HEADER_PERMS = ([1,0,0], [3,0,1], None, None,
|
||||
[1,0,0], None, [1,0,0], None,
|
||||
[1,0,0], None, [1,0,0], None,
|
||||
[1,0,0], None, [1,0,0], None,
|
||||
[1,1,1], [1,1,1], None, None,
|
||||
None, None, None, None,
|
||||
[2,0,0], None, [3,0,0], None,
|
||||
[3,0,0], None, [4,1,1], [4,1,1],
|
||||
[4,0,1], [4,0,1], [5,0,1], None,
|
||||
[5,0,1], None, [5,0,1], [5,0,1],
|
||||
[6,0,0], None, [6,0,0], None,
|
||||
[5,0,1], [5,0,1], [5,0,0], None,
|
||||
[6,0,1], None, [1,0,1], None,
|
||||
[5,0,0], None, [5,0,0], None,
|
||||
None, None, None, None,
|
||||
None, None, None, None)
|
||||
|
||||
def __init__(self, initial_string):
|
||||
"""Construct class based on a string that represents an initial
|
||||
'snapshot' of main memory."""
|
||||
if initial_string is None:
|
||||
raise ZMemoryBadInitialization
|
||||
|
||||
# Copy string into a _memory sequence that represents main memory.
|
||||
self._total_size = len(initial_string)
|
||||
self._memory = bytearray(initial_string)
|
||||
|
||||
# Figure out the different sections of memory
|
||||
self._static_start = self.read_word(0x0e)
|
||||
self._static_end = min(0x0ffff, self._total_size)
|
||||
self._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)
|
||||
|
||||
# Dynamic + static must not exceed 64k
|
||||
dynamic_plus_static = ((self._dynamic_end - self._dynamic_start)
|
||||
+ (self._static_end - self._static_start))
|
||||
if dynamic_plus_static > 65534:
|
||||
raise ZMemoryBadMemoryLayout
|
||||
|
||||
# What z-machine version is this story file?
|
||||
self.version = self._memory[0]
|
||||
|
||||
# Validate game size
|
||||
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
|
||||
|
||||
log("Memory system initialized, map follows")
|
||||
log(" Dynamic memory: %x - %x" % (self._dynamic_start, self._dynamic_end))
|
||||
log(" Static memory: %x - %x" % (self._static_start, self._static_end))
|
||||
log(" High memory: %x - %x" % (self._high_start, self._high_end))
|
||||
log(" Global variable start: %x" % self._global_variable_start)
|
||||
|
||||
def _check_bounds(self, index):
|
||||
if isinstance(index, slice):
|
||||
start, stop = index.start, index.stop
|
||||
else:
|
||||
start, stop = index, index
|
||||
if not ((0 <= start < self._total_size) and (0 <= stop < self._total_size)):
|
||||
raise ZMemoryOutOfBounds
|
||||
|
||||
def _check_static(self, index):
|
||||
"""Throw error if INDEX is within the static-memory area."""
|
||||
if isinstance(index, slice):
|
||||
start, stop = index.start, index.stop
|
||||
else:
|
||||
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):
|
||||
"""Pretty-print a description of the memory map."""
|
||||
print("Dynamic memory: ", self._dynamic_start, "-", self._dynamic_end)
|
||||
print(" Static memory: ", self._static_start, "-", self._static_end)
|
||||
print(" High memory: ", self._high_start, "-", self._high_end)
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""Return the byte value stored at address INDEX.."""
|
||||
self._check_bounds(index)
|
||||
return self._memory[index]
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
"""Set VALUE in memory address INDEX."""
|
||||
self._check_bounds(index)
|
||||
self._check_static(index)
|
||||
self._memory[index] = value
|
||||
|
||||
def __getslice__(self, start, end):
|
||||
"""Return a sequence of bytes from memory."""
|
||||
self._check_bounds(start)
|
||||
self._check_bounds(end)
|
||||
return self._memory[start:end]
|
||||
|
||||
def __setslice__(self, start, end, sequence):
|
||||
"""Set a range of memory addresses to SEQUENCE."""
|
||||
self._check_bounds(start)
|
||||
self._check_bounds(end - 1)
|
||||
self._check_static(start)
|
||||
self._check_static(end - 1)
|
||||
self._memory[start:end] = sequence
|
||||
|
||||
def word_address(self, address):
|
||||
"""Return the 'actual' address of word address ADDRESS."""
|
||||
if address < 0 or address > (self._total_size / 2):
|
||||
raise ZMemoryOutOfBounds
|
||||
return address*2
|
||||
|
||||
def packed_address(self, address):
|
||||
"""Return the 'actual' address of packed address ADDRESS."""
|
||||
if 1 <= self.version <= 3:
|
||||
if address < 0 or address > (self._total_size / 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 read_word(self, address):
|
||||
"""Return the 16-bit value stored at ADDRESS, ADDRESS+1."""
|
||||
if address < 0 or address >= (self._total_size - 1):
|
||||
raise ZMemoryOutOfBounds
|
||||
return (self._memory[address] << 8) + self._memory[(address + 1)]
|
||||
|
||||
def write_word(self, address, value):
|
||||
"""Write the given 16-bit value at ADDRESS, ADDRESS+1."""
|
||||
if address < 0 or address >= (self._total_size - 1):
|
||||
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
|
||||
# header. Instead, the interpreter or game must call one of the
|
||||
# following APIs.
|
||||
|
||||
def interpreter_set_header(self, address, value):
|
||||
"""Possibly allow the interpreter to set header ADDRESS to VALUE."""
|
||||
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 game_set_header(self, address, value):
|
||||
"""Possibly allow the game code to set header ADDRESS to VALUE."""
|
||||
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
|
||||
# global variables are located at a place determined by the header.
|
||||
|
||||
def read_global(self, varnum):
|
||||
"""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 write_global(self, varnum, value):
|
||||
"""Write 16-bit VALUE to global variable VARNUM. Incoming VARNUM
|
||||
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
|
||||
# a checksum of memory generated.
|
||||
|
||||
def generate_checksum(self):
|
||||
"""Return a checksum value which represents all the bytes of
|
||||
memory added from $0040 upwards, modulo $10000."""
|
||||
count = 0x40
|
||||
total = 0
|
||||
while count < self._total_size:
|
||||
total += self._memory[count]
|
||||
count += 1
|
||||
return (total % 0x10000)
|
||||
432
src/mudlib/zmachine/zobjectparser.py
Normal file
432
src/mudlib/zmachine/zobjectparser.py
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
#
|
||||
# A class which knows how to parse objects in the object tree.
|
||||
# Implements section 12 of Z-code specification.
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
# This part of of the z-machine is where it becomes really clear that
|
||||
# the original authoris were MIT Lisp-heads. :-) They've got a tree
|
||||
# of objects going, where each object is basically a linked list of
|
||||
# siblings. Specifically, each object contains a pointer to a parent,
|
||||
# a pointer to its "next sibling" in the list, and a pointer to the
|
||||
# head of its own children-list.
|
||||
|
||||
from .bitfield import BitField
|
||||
from .zlogging import log
|
||||
from .zstring import ZStringFactory
|
||||
|
||||
|
||||
class ZObjectError(Exception):
|
||||
"General exception for ZObject class"
|
||||
pass
|
||||
|
||||
class ZObjectIllegalObjectNumber(ZObjectError):
|
||||
"Illegal object number given."
|
||||
pass
|
||||
|
||||
class ZObjectIllegalAttributeNumber(ZObjectError):
|
||||
"Illegal attribute number given."
|
||||
pass
|
||||
|
||||
class ZObjectIllegalPropertyNumber(ZObjectError):
|
||||
"Illegal property number given."
|
||||
pass
|
||||
|
||||
class ZObjectIllegalPropertySet(ZObjectError):
|
||||
"Illegal set of a property whose size is not 1 or 2."
|
||||
pass
|
||||
|
||||
class ZObjectIllegalVersion(ZObjectError):
|
||||
"Unsupported z-machine version."
|
||||
pass
|
||||
|
||||
class ZObjectIllegalPropLength(ZObjectError):
|
||||
"Illegal property length."
|
||||
pass
|
||||
|
||||
class ZObjectMalformedTree(ZObjectError):
|
||||
"Object tree is malformed."
|
||||
pass
|
||||
|
||||
|
||||
# The interpreter should only need exactly one instance of this class.
|
||||
|
||||
class ZObjectParser:
|
||||
|
||||
def __init__(self, zmem):
|
||||
|
||||
self._memory = zmem
|
||||
self._propdefaults_addr = zmem.read_word(0x0a)
|
||||
self._stringfactory = ZStringFactory(self._memory)
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _get_object_addr(self, objectnum):
|
||||
"""Return address of object number OBJECTNUM."""
|
||||
|
||||
result = 0
|
||||
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))
|
||||
return result
|
||||
|
||||
|
||||
def _get_parent_sibling_child(self, objectnum):
|
||||
"""Return [parent, sibling, child] object numbers of object OBJECTNUM."""
|
||||
|
||||
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 + 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:
|
||||
if bf[6]:
|
||||
size = 2
|
||||
else:
|
||||
size = 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:
|
||||
if bf[6]:
|
||||
size = 2
|
||||
else:
|
||||
size = 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(" [%2d] :" % key, end=' ')
|
||||
for i in range(0, len):
|
||||
print("%02X" % self._memory[addr+i], end=' ')
|
||||
print()
|
||||
|
||||
234
src/mudlib/zmachine/zopdecoder.py
Normal file
234
src/mudlib/zmachine/zopdecoder.py
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
#
|
||||
# A class which represents the Program Counter and decodes instructions
|
||||
# to be executed by the ZPU. Implements section 4 of Z-code specification.
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
from .bitfield import BitField
|
||||
from .zlogging import log
|
||||
|
||||
|
||||
class ZOperationError(Exception):
|
||||
"General exception for ZOperation class"
|
||||
pass
|
||||
|
||||
# Constants defining the known instruction types. These types are
|
||||
# related to the number of operands the opcode has: for each operand
|
||||
# count, there is a separate opcode table, and the actual opcode
|
||||
# number is an index into that table.
|
||||
OPCODE_0OP = 0
|
||||
OPCODE_1OP = 1
|
||||
OPCODE_2OP = 2
|
||||
OPCODE_VAR = 3
|
||||
OPCODE_EXT = 4
|
||||
|
||||
# Mapping of those constants to strings describing the opcode
|
||||
# classes. Used for pretty-printing only.
|
||||
OPCODE_STRINGS = {
|
||||
OPCODE_0OP: '0OP',
|
||||
OPCODE_1OP: '1OP',
|
||||
OPCODE_2OP: '2OP',
|
||||
OPCODE_VAR: 'VAR',
|
||||
OPCODE_EXT: 'EXT',
|
||||
}
|
||||
|
||||
# Constants defining the possible operand types.
|
||||
LARGE_CONSTANT = 0x0
|
||||
SMALL_CONSTANT = 0x1
|
||||
VARIABLE = 0x2
|
||||
ABSENT = 0x3
|
||||
|
||||
class ZOpDecoder:
|
||||
def __init__(self, zmem, zstack):
|
||||
""
|
||||
self._memory = zmem
|
||||
self._stack = zstack
|
||||
self._parse_map = {}
|
||||
self.program_counter = self._memory.read_word(0x6)
|
||||
|
||||
def _get_pc(self):
|
||||
byte = self._memory[self.program_counter]
|
||||
self.program_counter += 1
|
||||
return byte
|
||||
|
||||
def get_next_instruction(self):
|
||||
"""Decode the opcode & operands currently pointed to by the
|
||||
program counter, and appropriately increment the program counter
|
||||
afterwards. A decoded operation is returned to the caller in the form:
|
||||
|
||||
[opcode-class, opcode-number, [operand, operand, operand, ...]]
|
||||
|
||||
If the opcode has no operands, the operand list is present but empty."""
|
||||
|
||||
opcode = self._get_pc()
|
||||
|
||||
log("Decode opcode %x" % opcode)
|
||||
|
||||
# Determine the opcode type, and hand off further parsing.
|
||||
if self._memory.version == 5 and opcode == 0xBE:
|
||||
# Extended opcode
|
||||
return self._parse_opcode_extended()
|
||||
|
||||
opcode = BitField(opcode)
|
||||
if opcode[7] == 0:
|
||||
# Long opcode
|
||||
return self._parse_opcode_long(opcode)
|
||||
elif opcode[6] == 0:
|
||||
# Short opcode
|
||||
return self._parse_opcode_short(opcode)
|
||||
else:
|
||||
# Variable opcode
|
||||
return self._parse_opcode_variable(opcode)
|
||||
|
||||
def _parse_opcode_long(self, opcode):
|
||||
"""Parse an opcode of the long form."""
|
||||
# Long opcodes are always 2OP. The types of the two operands are
|
||||
# encoded in bits 5 and 6 of the opcode.
|
||||
log("Opcode is long")
|
||||
LONG_OPERAND_TYPES = [SMALL_CONSTANT, VARIABLE]
|
||||
operands = [self._parse_operand(LONG_OPERAND_TYPES[opcode[6]]),
|
||||
self._parse_operand(LONG_OPERAND_TYPES[opcode[5]])]
|
||||
return (OPCODE_2OP, opcode[0:5], operands)
|
||||
|
||||
def _parse_opcode_short(self, opcode):
|
||||
"""Parse an opcode of the short form."""
|
||||
# Short opcodes can have either 1 operand, or no operand.
|
||||
log("Opcode is short")
|
||||
operand_type = opcode[4:6]
|
||||
operand = self._parse_operand(operand_type)
|
||||
if operand is None: # 0OP variant
|
||||
log("Opcode is 0OP variant")
|
||||
return (OPCODE_0OP, opcode[0:4], [])
|
||||
else:
|
||||
log("Opcode is 1OP variant")
|
||||
return (OPCODE_1OP, opcode[0:4], [operand])
|
||||
|
||||
def _parse_opcode_variable(self, opcode):
|
||||
"""Parse an opcode of the variable form."""
|
||||
log("Opcode is variable")
|
||||
if opcode[5]:
|
||||
log("Variable opcode of VAR kind")
|
||||
opcode_type = OPCODE_VAR
|
||||
else:
|
||||
log("Variable opcode of 2OP kind")
|
||||
opcode_type = OPCODE_2OP
|
||||
|
||||
opcode_num = opcode[0:5]
|
||||
|
||||
# Parse the types byte to retrieve the operands.
|
||||
operands = self._parse_operands_byte()
|
||||
|
||||
# Special case: opcodes 12 and 26 have a second operands byte.
|
||||
if opcode[0:7] == 0xC or opcode[0:7] == 0x1A:
|
||||
log("Opcode has second operand byte")
|
||||
operands += self._parse_operands_byte()
|
||||
|
||||
return (opcode_type, opcode_num, operands)
|
||||
|
||||
def _parse_operand(self, operand_type):
|
||||
"""Read and return an operand of the given type.
|
||||
|
||||
This assumes that the operand is in memory, at the address pointed
|
||||
by the Program Counter."""
|
||||
assert operand_type <= 0x3
|
||||
|
||||
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("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
|
||||
|
||||
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
|
||||
# opcode being executed:
|
||||
|
||||
def get_zstring(self):
|
||||
"""For string opcodes, return the address of the zstring pointed
|
||||
to by the PC. Increment PC just past the text."""
|
||||
|
||||
start_addr = self.program_counter
|
||||
bf = BitField(0)
|
||||
|
||||
while True:
|
||||
bf.__init__(self._memory[self.program_counter])
|
||||
self.program_counter += 2
|
||||
if bf[7] == 1:
|
||||
break
|
||||
|
||||
return start_addr
|
||||
|
||||
|
||||
def get_store_address(self):
|
||||
"""For store opcodes, read byte pointed to by PC and return the
|
||||
variable number in which the operation result should be stored.
|
||||
Increment the PC as necessary."""
|
||||
return self._get_pc()
|
||||
|
||||
|
||||
def get_branch_offset(self):
|
||||
"""For branching opcodes, examine address pointed to by PC, and
|
||||
return two values: first, either True or False (indicating whether
|
||||
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
|
||||
303
src/mudlib/zmachine/zscreen.py
Normal file
303
src/mudlib/zmachine/zscreen.py
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
#
|
||||
# A template class representing the screen of a z-machine.
|
||||
#
|
||||
# Third-party programs are expected to subclass zscreen and override all
|
||||
# the methods, then pass an instance of their class to be driven by
|
||||
# the main z-machine engine.
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
from . import zstream
|
||||
|
||||
# Constants for window numbers.
|
||||
#
|
||||
# TODO: The Z-Machine standard mentions upper and lower windows and
|
||||
# window numbers, but never appears to define a mapping between the
|
||||
# two. So the following values are simply a best guess and may need
|
||||
# to be changed in the future.
|
||||
WINDOW_UPPER = 1
|
||||
WINDOW_LOWER = 2
|
||||
|
||||
# Constants for fonts. These are human-readable names for the font ID
|
||||
# numbers as described in section 8.1.2 of the Z-Machine Standards
|
||||
# Document.
|
||||
FONT_NORMAL = 1
|
||||
FONT_PICTURE = 2
|
||||
FONT_CHARACTER_GRAPHICS = 3
|
||||
FONT_FIXED_PITCH = 4
|
||||
|
||||
# Constants for text styles. These are human-readable names for the
|
||||
# 'style' operand of the Z-Machine's 'set_text_style' opcode.
|
||||
STYLE_ROMAN = 0
|
||||
STYLE_REVERSE_VIDEO = 1
|
||||
STYLE_BOLD = 2
|
||||
STYLE_ITALIC = 4
|
||||
STYLE_FIXED_PITCH = 8
|
||||
|
||||
# Constants for colors. These are human-readable names for the color
|
||||
# codes as described in section 8.3.1 of the Z-Machine Standards
|
||||
# Document. Note that the colors defined by Z-Machine Version 6 are
|
||||
# not defined here, since we are not currently supporting that
|
||||
# version.
|
||||
COLOR_CURRENT = 0
|
||||
COLOR_DEFAULT = 1
|
||||
COLOR_BLACK = 2
|
||||
COLOR_RED = 3
|
||||
COLOR_GREEN = 4
|
||||
COLOR_YELLOW = 5
|
||||
COLOR_BLUE = 6
|
||||
COLOR_MAGENTA = 7
|
||||
COLOR_CYAN = 8
|
||||
COLOR_WHITE = 9
|
||||
|
||||
# The number of screen rows that represents an "infinite" screen
|
||||
# height; a screen with its rows set to this value should never
|
||||
# display a [MORE] prompt, as described in section 8.4.1 of the
|
||||
# Z-Machine Standards Document.
|
||||
INFINITE_ROWS = 255
|
||||
|
||||
|
||||
class ZScreenObserver:
|
||||
"""Observer that is notified of changes in the state of a ZScreen
|
||||
object.
|
||||
|
||||
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
|
||||
the integrity of any data they modify."""
|
||||
|
||||
def on_screen_size_change(self, zscreen):
|
||||
"""Called when the screen size of a ZScreen changes."""
|
||||
|
||||
pass
|
||||
|
||||
def on_font_size_change(self, zscreen):
|
||||
"""Called when the font size of a ZScreen changes."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ZScreen(zstream.ZBufferableOutputStream):
|
||||
"""Subclass of zstream.ZBufferableOutputStream that provides an
|
||||
abstraction of a computer screen."""
|
||||
|
||||
def __init__(self):
|
||||
"Constructor for the screen."
|
||||
|
||||
zstream.ZBufferableOutputStream.__init__(self)
|
||||
|
||||
# The size of the screen.
|
||||
self._columns = 79
|
||||
self._rows = 24
|
||||
|
||||
# The size of the current font, in characters
|
||||
self._fontheight = 1
|
||||
self._fontwidth = 1
|
||||
|
||||
# List of our observers; clients can directly append to and remove
|
||||
# from this.
|
||||
self.observers = []
|
||||
|
||||
# Subclasses must define real values for all the features they
|
||||
# support (or don't support).
|
||||
|
||||
self.features = {
|
||||
"has_status_line" : False,
|
||||
"has_upper_window" : False,
|
||||
"has_graphics_font" : False,
|
||||
"has_text_colors": False,
|
||||
}
|
||||
|
||||
# Window Management
|
||||
#
|
||||
# The z-machine has 2 windows for displaying text, "upper" and
|
||||
# "lower". (The upper window has an inital height of 0.)
|
||||
#
|
||||
# The upper window is not necessarily where the "status line"
|
||||
# appears; see section 8.6.1.1 of the Z-Machine Standards Document.
|
||||
#
|
||||
# The UI is responsible for making the lower window scroll properly,
|
||||
# as well as wrapping words ("buffering"). The upper window,
|
||||
# however, should *never* scroll or wrap words.
|
||||
#
|
||||
# The UI is also responsible for displaying [MORE] prompts when
|
||||
# printing more text than the screen's rows can display. (Note: if
|
||||
# the number of screen rows is INFINITE_ROWS, then it should never
|
||||
# prompt [MORE].)
|
||||
|
||||
def get_screen_size(self):
|
||||
"""Return the current size of the screen as [rows, columns]."""
|
||||
|
||||
return [self._rows, self._columns]
|
||||
|
||||
|
||||
def select_window(self, window):
|
||||
"""Select a window to be the 'active' window, and move that
|
||||
window's cursor to the upper left.
|
||||
|
||||
WINDOW should be one of WINDOW_UPPER or WINDOW_LOWER.
|
||||
|
||||
This method should only be implemented if the
|
||||
has_upper_window feature is enabled."""
|
||||
|
||||
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.
|
||||
|
||||
This method should only be implemented if the has_upper_window
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
If the has_upper_window feature is not supported, WINDOW is
|
||||
ignored (in such a case, this function should clear the entire
|
||||
screen).
|
||||
|
||||
COLOR should be one of the COLOR_* constants.
|
||||
|
||||
If the has_text_colors feature is not supported, COLOR is ignored."""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def erase_line(self):
|
||||
"""Erase from the current cursor position to the end of its line
|
||||
in the current window.
|
||||
|
||||
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()
|
||||
|
||||
|
||||
# Status Line
|
||||
#
|
||||
# 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):
|
||||
"""Print a status line in the upper window, as follows:
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def print_status_time(self, hours, minutes):
|
||||
"""Print a status line in the upper window, as follows:
|
||||
|
||||
On the left side of the status line, print TEXT.
|
||||
On the right side of the status line, print HOURS:MINUTES.
|
||||
|
||||
This method should only be implemented if the has_status_line
|
||||
feature is enabled.
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
# Text Appearances
|
||||
#
|
||||
|
||||
def get_font_size(self):
|
||||
"""Return the current font's size as [width, height]."""
|
||||
|
||||
return [self._fontwidth, self._fontheight]
|
||||
|
||||
|
||||
def set_font(self, font_number):
|
||||
"""Set the current window's font to one of
|
||||
|
||||
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.
|
||||
|
||||
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."""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
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:
|
||||
|
||||
STYLE_ROMAN - Roman
|
||||
STYLE_REVERSE_VIDEO - Reverse video
|
||||
STYLE_BOLD - Bold
|
||||
STYLE_ITALIC - Italic
|
||||
STYLE_FIXED_PITCH - Fixed-width
|
||||
|
||||
It is not a requirement that the screen implementation support
|
||||
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()
|
||||
204
src/mudlib/zmachine/zstackmanager.py
Normal file
204
src/mudlib/zmachine/zstackmanager.py
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
#
|
||||
# A class which manages both (1) the general purpose stack ("data
|
||||
# stack") used by the story code to store temporary data, and (2) the
|
||||
# interpreter-private stack of routines ("call stack") and their local
|
||||
# variables.
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
from .zlogging import log
|
||||
|
||||
|
||||
class ZStackError(Exception):
|
||||
"General exception for stack or routine-related errors"
|
||||
pass
|
||||
|
||||
class ZStackUnsupportedVersion(ZStackError):
|
||||
"Unsupported version of Z-story file."
|
||||
pass
|
||||
|
||||
class ZStackNoRoutine(ZStackError):
|
||||
"No routine is being executed."
|
||||
pass
|
||||
|
||||
class ZStackNoSuchVariable(ZStackError):
|
||||
"Trying to access non-existent local variable."
|
||||
pass
|
||||
|
||||
class ZStackPopError(ZStackError):
|
||||
"Nothing to pop from stack!"
|
||||
pass
|
||||
|
||||
# Helper class used by ZStackManager; a 'routine' object which
|
||||
# includes its own private stack of data.
|
||||
class ZRoutine:
|
||||
|
||||
def __init__(self, start_addr, return_addr, zmem, args,
|
||||
local_vars=None, stack=None):
|
||||
"""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."""
|
||||
|
||||
self.start_addr = start_addr
|
||||
self.return_addr = return_addr
|
||||
self.program_counter = 0 # used when execution interrupted
|
||||
|
||||
if stack is None:
|
||||
self.stack = []
|
||||
else:
|
||||
self.stack = stack[:]
|
||||
|
||||
if local_vars is not None:
|
||||
self.local_vars = local_vars[:]
|
||||
else:
|
||||
num_local_vars = zmem[self.start_addr]
|
||||
if not (0 <= num_local_vars <= 15):
|
||||
log("num local vars is %d" % num_local_vars)
|
||||
raise ZStackError
|
||||
self.start_addr += 1
|
||||
|
||||
# Initialize the local vars in the ZRoutine's dictionary. This is
|
||||
# only needed on machines v1 through v4. In v5 machines, all local
|
||||
# variables are preinitialized to zero.
|
||||
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
|
||||
for i in range(0, len(args)):
|
||||
self.local_vars[i] = args[i]
|
||||
|
||||
|
||||
def pretty_print(self):
|
||||
"Display a ZRoutine nicely, for debugging purposes."
|
||||
|
||||
log("ZRoutine: start address: %d" % self.start_addr)
|
||||
log("ZRoutine: return value address: %d" % self.return_addr)
|
||||
log("ZRoutine: program counter: %d" % self.program_counter)
|
||||
log("ZRoutine: local variables: %d" % self.local_vars)
|
||||
|
||||
|
||||
class ZStackBottom:
|
||||
|
||||
def __init__(self):
|
||||
self.program_counter = 0 # used as a cache only
|
||||
|
||||
|
||||
class ZStackManager:
|
||||
|
||||
def __init__(self, zmem):
|
||||
|
||||
self._memory = zmem
|
||||
self._stackbottom = ZStackBottom()
|
||||
self._call_stack = [self._stackbottom]
|
||||
|
||||
|
||||
def get_local_variable(self, varnum):
|
||||
"""Return value of local variable VARNUM from currently-running
|
||||
routine. VARNUM must be a value between 0 and 15, and must
|
||||
exist."""
|
||||
|
||||
if self._call_stack[-1] == self._stackbottom:
|
||||
raise ZStackNoRoutine
|
||||
|
||||
if not 0 <= varnum <= 15:
|
||||
raise ZStackNoSuchVariable
|
||||
|
||||
current_routine = self._call_stack[-1]
|
||||
|
||||
return current_routine.local_vars[varnum]
|
||||
|
||||
|
||||
def set_local_variable(self, varnum, value):
|
||||
"""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:
|
||||
raise ZStackNoRoutine
|
||||
|
||||
if not 0 <= varnum <= 15:
|
||||
raise ZStackNoSuchVariable
|
||||
|
||||
current_routine = self._call_stack[-1]
|
||||
|
||||
current_routine.local_vars[varnum] = value
|
||||
|
||||
|
||||
def push_stack(self, value):
|
||||
"Push VALUE onto the top of the current routine's data stack."
|
||||
|
||||
current_routine = self._call_stack[-1]
|
||||
current_routine.stack.append(value)
|
||||
|
||||
|
||||
def pop_stack(self):
|
||||
"Remove and return value from the top of the data stack."
|
||||
|
||||
current_routine = self._call_stack[-1]
|
||||
return current_routine.stack.pop()
|
||||
|
||||
|
||||
def get_stack_frame_index(self):
|
||||
"Return current stack frame number. For use by 'catch' opcode."
|
||||
|
||||
return len(self._call_stack) - 1
|
||||
|
||||
|
||||
# Used by quetzal save-file parser to reconstruct stack-frames.
|
||||
def push_routine(self, routine):
|
||||
"""Blindly push a ZRoutine object to the call stack.
|
||||
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
|
||||
|
||||
98
src/mudlib/zmachine/zstream.py
Normal file
98
src/mudlib/zmachine/zstream.py
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
#
|
||||
# Template classes representing input/output streams of a z-machine.
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
class ZOutputStream:
|
||||
"""Abstract class representing an output stream for a z-machine."""
|
||||
|
||||
def write(self, string):
|
||||
"""Prints the given unicode string to the output stream."""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ZBufferableOutputStream(ZOutputStream):
|
||||
"""Abstract class representing a buffered output stream for a
|
||||
z-machine, which can be optionally configured at run-time to provide
|
||||
'buffering', also known as word-wrap."""
|
||||
|
||||
def __init__(self):
|
||||
# This is a public variable that determines whether buffering is
|
||||
# enabled for this stream or not. Subclasses can make it a
|
||||
# Python property if necessary.
|
||||
self.buffer_mode = False
|
||||
|
||||
|
||||
class ZInputStream:
|
||||
"""Abstract class representing an input stream for a z-machine."""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructor for the input stream."""
|
||||
# Subclasses must define real values for all the features they
|
||||
# support (or don't support).
|
||||
|
||||
self.features = {
|
||||
"has_timed_input" : False,
|
||||
}
|
||||
|
||||
def read_line(self, original_text=None, max_length=0,
|
||||
terminating_characters=None,
|
||||
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
|
||||
end-user may delete or otherwise modify if they so choose.
|
||||
|
||||
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
|
||||
many characters have been entered is ignored. 0 means that there
|
||||
is no practical limit to the number of characters the end-user can
|
||||
enter.
|
||||
|
||||
terminating_characters is a string of unicode characters
|
||||
representing the characters that can signify the end of a line of
|
||||
input. If not provided, it defaults to a string containing a
|
||||
carriage return character ('\r'). The terminating character is
|
||||
not contained in the returned string.
|
||||
|
||||
timed_input_routine is a function that will be called every
|
||||
time_input_interval milliseconds. This function should be of the
|
||||
form:
|
||||
|
||||
def timed_input_routine(interval)
|
||||
|
||||
where interval is simply the value of timed_input_interval that
|
||||
was passed in to read_line(). The function should also return
|
||||
True if input should continue to be collected, or False if input
|
||||
should stop being collected; if False is returned, then
|
||||
read_line() will return a unicode string representing the
|
||||
characters typed so far.
|
||||
|
||||
The timed input routine will be called from the same thread that
|
||||
called read_line().
|
||||
|
||||
Note, however, that supplying a timed input routine is only useful
|
||||
if the has_timed_input feature is supported by the input stream.
|
||||
If it is unsupported, then the timed input routine will not be
|
||||
called."""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def read_char(self, timed_input_routine=None,
|
||||
timed_input_interval=0):
|
||||
"""Reads a single character from the stream and returns it as a
|
||||
unicode character.
|
||||
|
||||
timed_input_routine and timed_input_interval are the same as
|
||||
described in the documentation for read_line().
|
||||
|
||||
TODO: Should the character be automatically printed to the screen?
|
||||
The Z-Machine documentation for the read_char opcode, which this
|
||||
function is meant to ultimately implement, doesn't specify."""
|
||||
|
||||
raise NotImplementedError()
|
||||
97
src/mudlib/zmachine/zstreammanager.py
Normal file
97
src/mudlib/zmachine/zstreammanager.py
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#
|
||||
# A class which represents the i/o streams of the Z-Machine and their
|
||||
# current state of selection.
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
# 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
|
||||
# of the Z-Machine Standards Document.
|
||||
OUTPUT_SCREEN = 1 # spews text to the the screen
|
||||
OUTPUT_TRANSCRIPT = 2 # contains everything player typed, plus our responses
|
||||
OUTPUT_MEMORY = 3 # if the z-machine wants to write to memory
|
||||
OUTPUT_PLAYER_INPUT = 4 # contains *only* the player's typed commands
|
||||
|
||||
# Constants for input streams. These are human-readable names for the
|
||||
# stream ID numbers as described in section 10.2 of the Z-Machine
|
||||
# Standards Document.
|
||||
INPUT_KEYBOARD = 0
|
||||
INPUT_FILE = 1
|
||||
|
||||
class ZOutputStreamManager:
|
||||
"""Manages output streams for a Z-Machine."""
|
||||
|
||||
def __init__(self, zmem, zui):
|
||||
# TODO: Actually set/create the streams as necessary.
|
||||
|
||||
self._selectedStreams = []
|
||||
self._streams = {}
|
||||
|
||||
def select(self, stream):
|
||||
"""Selects the given stream ID for output."""
|
||||
|
||||
if stream not in self._selectedStreams:
|
||||
self._selectedStreams.append(stream)
|
||||
|
||||
def unselect(self, stream):
|
||||
"""Unselects the given stream ID for output."""
|
||||
|
||||
if stream in self._selectedStreams:
|
||||
self._selectedStreams.remove(stream)
|
||||
|
||||
def get(self, stream):
|
||||
"""Retrieves the given stream ID."""
|
||||
|
||||
return self._streams[stream]
|
||||
|
||||
def write(self, string):
|
||||
"""Writes the given unicode string to all currently selected output
|
||||
streams."""
|
||||
|
||||
# TODO: Implement section 7.1.2.2 of the Z-Machine Standards
|
||||
# Document, so that while stream 3 is selected, no text is
|
||||
# sent to any other output streams which are selected. (However,
|
||||
# they remain selected.).
|
||||
|
||||
# TODO: Implement section 7.1.2.2.1, so that newlines are written to
|
||||
# output stream 3 as ZSCII 13.
|
||||
|
||||
# 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
|
||||
# commands and keypresses (as read by read_char). This may not
|
||||
# ultimately happen via this method.
|
||||
|
||||
for stream in self._selectedStreams:
|
||||
self._streams[stream].write(string)
|
||||
|
||||
class ZInputStreamManager:
|
||||
"""Manages input streams for a Z-Machine."""
|
||||
|
||||
def __init__(self, zui):
|
||||
# TODO: Actually set/create the streams as necessary.
|
||||
|
||||
self._selectedStream = None
|
||||
self._streams = {}
|
||||
|
||||
def select(self, 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,
|
||||
# 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
|
||||
# will ultimately go, however.
|
||||
|
||||
self._selectedStream = stream
|
||||
|
||||
def getSelected(self):
|
||||
"""Returns the input stream object for the currently active input
|
||||
stream."""
|
||||
|
||||
return self._streams[self._selectedStream]
|
||||
|
||||
class ZStreamManager:
|
||||
def __init__(self, zmem, zui):
|
||||
self.input = ZInputStreamManager(zui)
|
||||
self.output = ZOutputStreamManager(zmem, zui)
|
||||
408
src/mudlib/zmachine/zstring.py
Normal file
408
src/mudlib/zmachine/zstring.py
Normal file
|
|
@ -0,0 +1,408 @@
|
|||
#
|
||||
# A ZString-to-Unicode Universal Translator.
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
import itertools
|
||||
|
||||
|
||||
class ZStringEndOfString(Exception):
|
||||
"""No more data left in string."""
|
||||
|
||||
class ZStringIllegalAbbrevInString(Exception):
|
||||
"""String abbreviation encountered within a string in a context
|
||||
where it is not allowed."""
|
||||
|
||||
|
||||
class ZStringTranslator:
|
||||
def __init__(self, zmem):
|
||||
self._mem = zmem
|
||||
|
||||
def get(self, addr):
|
||||
from .bitfield import BitField
|
||||
pos = (addr, BitField(self._mem.read_word(addr)), 0)
|
||||
|
||||
s = []
|
||||
try:
|
||||
while True:
|
||||
s.append(self._read_char(pos))
|
||||
pos = self._next_pos(pos)
|
||||
except ZStringEndOfString:
|
||||
return s
|
||||
|
||||
def _read_char(self, pos):
|
||||
offset = (2 - pos[2]) * 5
|
||||
return pos[1][offset:offset+5]
|
||||
|
||||
def _is_final(self, pos):
|
||||
return pos[1][15] == 1
|
||||
|
||||
def _next_pos(self, pos):
|
||||
from .bitfield import BitField
|
||||
|
||||
offset = pos[2] + 1
|
||||
# Overflowing from current block?
|
||||
if offset == 3:
|
||||
# Was last block?
|
||||
if self._is_final(pos):
|
||||
# Kill processing.
|
||||
raise ZStringEndOfString
|
||||
# Get and return the next block.
|
||||
return (pos[0] + 2,
|
||||
BitField(self._mem.read_word(pos[0] + 2)),
|
||||
0)
|
||||
|
||||
# Just increment the intra-block counter.
|
||||
return (pos[0], pos[1], offset)
|
||||
|
||||
|
||||
class ZCharTranslator:
|
||||
|
||||
# The default alphabet tables for ZChar translation.
|
||||
# As the codes 0-5 are special, alphabets start with code 0x6.
|
||||
DEFAULT_A0 = [ord(x) for x in "abcdefghijklmnopqrstuvwxyz"]
|
||||
DEFAULT_A1 = [ord(x) for x in "ABCDEFGHIJKLMNOPQRSTUVWXYZ"]
|
||||
# A2 also has 0x6 as special char, so they start at 0x7.
|
||||
DEFAULT_A2 = [ord(x) for x in "0123456789.,!?_#'\"/\\<-:()"]
|
||||
DEFAULT_A2_V5 = [ord(x) for x in "\n0123456789.,!?_#'\"/\\-:()"]
|
||||
|
||||
ALPHA = (DEFAULT_A0, DEFAULT_A1, DEFAULT_A2)
|
||||
ALPHA_V5 = (DEFAULT_A0, DEFAULT_A1, DEFAULT_A2_V5)
|
||||
|
||||
def __init__(self, zmem):
|
||||
self._mem = zmem
|
||||
|
||||
# Initialize the alphabets
|
||||
if self._mem.version == 5:
|
||||
self._alphabet = self._load_custom_alphabet() or self.ALPHA_V5
|
||||
else:
|
||||
self._alphabet = self.ALPHA
|
||||
|
||||
# Initialize the special state handlers
|
||||
self._load_specials()
|
||||
|
||||
# Initialize the abbreviations (if supported)
|
||||
self._load_abbrev_tables()
|
||||
|
||||
def _load_custom_alphabet(self):
|
||||
"""Check for the existence of a custom alphabet, and load it
|
||||
if it does exist. Return the custom alphabet if it was found,
|
||||
None otherwise."""
|
||||
# The custom alphabet table address is at 0x34 in the memory.
|
||||
if self._mem[0x34] == 0:
|
||||
return None
|
||||
|
||||
alph_addr = self._mem.read_word(0x34)
|
||||
alphabet = self._mem[alph_addr:alph_addr+78]
|
||||
return [alphabet[0:26], alphabet[26:52], alphabet[52:78]]
|
||||
|
||||
def _load_abbrev_tables(self):
|
||||
self._abbrevs = {}
|
||||
|
||||
# If the ZM doesn't do abbrevs, just return an empty dict.
|
||||
if self._mem.version == 1:
|
||||
return
|
||||
|
||||
# Build ourselves a ZStringTranslator for the abbrevs.
|
||||
xlator = ZStringTranslator(self._mem)
|
||||
|
||||
def _load_subtable(num, base):
|
||||
for i,zoff in [(i,base+(num*64)+(i*2))
|
||||
for i in range(0, 32)]:
|
||||
zaddr = self._mem.read_word(zoff)
|
||||
zstr = xlator.get(self._mem.word_address(zaddr))
|
||||
zchr = self.get(zstr, allow_abbreviations=False)
|
||||
self._abbrevs[(num, i)] = zchr
|
||||
|
||||
abbrev_base = self._mem.read_word(0x18)
|
||||
_load_subtable(0, abbrev_base)
|
||||
|
||||
# Does this ZM support the extended abbrev tables?
|
||||
if self._mem.version >= 3:
|
||||
_load_subtable(1, abbrev_base)
|
||||
_load_subtable(2, abbrev_base)
|
||||
|
||||
def _load_specials(self):
|
||||
"""Load the special character code handlers for the current
|
||||
machine version.
|
||||
"""
|
||||
# The following three functions define the three possible
|
||||
# special character code handlers.
|
||||
def newline(state):
|
||||
"""Append ZSCII 13 (newline) to the output."""
|
||||
state['zscii'].append(13)
|
||||
|
||||
def shift_alphabet(state, direction, lock):
|
||||
"""Shift the current alphaber up or down. If lock is
|
||||
False, the alphabet will revert to the previous alphabet
|
||||
after outputting 1 character. Else, the alphabet will
|
||||
remain unchanged until the next shift.
|
||||
"""
|
||||
state['curr_alpha'] = (state['curr_alpha'] + direction) % 3
|
||||
if lock:
|
||||
state['prev_alpha'] = state['curr_alpha']
|
||||
|
||||
def abbreviation(state, abbrev):
|
||||
"""Insert the given abbreviation from the given table into
|
||||
the output stream.
|
||||
|
||||
This character was an abbreviation table number. The next
|
||||
character will be the offset within that table of the
|
||||
abbreviation. Set up a state handler to intercept the next
|
||||
character and output the right abbreviation."""
|
||||
def write_abbreviation(state, c, subtable):
|
||||
state['zscii'] += self._abbrevs[(subtable, c)]
|
||||
del state['state_handler']
|
||||
|
||||
# If we're parsing an abbreviation, there should be no
|
||||
# nested abbreviations. So this is just a sanity check for
|
||||
# people feeding us bad stories.
|
||||
if not state['allow_abbreviations']:
|
||||
raise ZStringIllegalAbbrevInString
|
||||
|
||||
state['state_handler'] = lambda s,c: write_abbreviation(s, c,
|
||||
abbrev)
|
||||
|
||||
# Register the specials handlers depending on machine version.
|
||||
if self._mem.version == 1:
|
||||
self._specials = {
|
||||
1: lambda s: newline(s),
|
||||
2: lambda s: shift_alphabet(s, +1, False),
|
||||
3: lambda s: shift_alphabet(s, -1, False),
|
||||
4: lambda s: shift_alphabet(s, +1, True),
|
||||
5: lambda s: shift_alphabet(s, -1, True),
|
||||
}
|
||||
elif self._mem.version == 2:
|
||||
self._specials = {
|
||||
1: lambda s: abbreviation(s, 0),
|
||||
2: lambda s: shift_alphabet(s, +1, False),
|
||||
3: lambda s: shift_alphabet(s, -1, False),
|
||||
4: lambda s: shift_alphabet(s, +1, True),
|
||||
5: lambda s: shift_alphabet(s, -1, True),
|
||||
}
|
||||
else: # ZM v3-5
|
||||
self._specials = {
|
||||
1: lambda s: abbreviation(s, 0),
|
||||
2: lambda s: abbreviation(s, 1),
|
||||
3: lambda s: abbreviation(s, 2),
|
||||
4: lambda s: shift_alphabet(s, +1, False),
|
||||
5: lambda s: shift_alphabet(s, -1, False),
|
||||
}
|
||||
|
||||
def _special_zscii(self, state, char):
|
||||
if 'zscii_char' not in list(state.keys()):
|
||||
state['zscii_char'] = char
|
||||
else:
|
||||
zchar = (state['zscii_char'] << 5) + char
|
||||
state['zscii'].append(zchar)
|
||||
del state['zscii_char']
|
||||
del state['state_handler']
|
||||
|
||||
def get(self, zstr, allow_abbreviations=True):
|
||||
state = {
|
||||
'curr_alpha': 0,
|
||||
'prev_alpha': 0,
|
||||
'zscii': [],
|
||||
'allow_abbreviations': allow_abbreviations,
|
||||
}
|
||||
|
||||
for c in zstr:
|
||||
if 'state_handler' in list(state.keys()):
|
||||
# If a special handler has registered itself, then hand
|
||||
# processing over to it.
|
||||
state['state_handler'](state, c)
|
||||
elif c in list(self._specials.keys()):
|
||||
# Hand off per-ZM version special char handling.
|
||||
self._specials[c](state)
|
||||
elif state['curr_alpha'] == 2 and c == 6:
|
||||
# Handle the strange A2/6 character
|
||||
state['state_handler'] = self._special_zscii
|
||||
else:
|
||||
# Do the usual Thing: append a zscii code to the
|
||||
# decoded sequence and revert to the "previous"
|
||||
# alphabet (or not, if it hasn't recently changed or
|
||||
# was locked)
|
||||
if c == 0:
|
||||
# Append a space.
|
||||
z = 32
|
||||
elif state['curr_alpha'] == 2:
|
||||
# The symbol alphabet table only has 25 chars
|
||||
# because of the A2/6 special char, so we need to
|
||||
# adjust differently.
|
||||
z = self._alphabet[state['curr_alpha']][c-7]
|
||||
else:
|
||||
z = self._alphabet[state['curr_alpha']][c-6]
|
||||
state['zscii'].append(z)
|
||||
state['curr_alpha'] = state['prev_alpha']
|
||||
|
||||
return state['zscii']
|
||||
|
||||
|
||||
class ZsciiTranslator:
|
||||
# The default Unicode Translation Table that maps to ZSCII codes
|
||||
# 155-251. The codes are unicode codepoints for a host of strange
|
||||
# characters.
|
||||
DEFAULT_UTT = [chr(x) for x in
|
||||
(0xe4, 0xf6, 0xfc, 0xc4, 0xd6, 0xdc,
|
||||
0xdf, 0xbb, 0xab, 0xeb, 0xef, 0xff,
|
||||
0xcb, 0xcf, 0xe1, 0xe9, 0xed, 0xf3,
|
||||
0xfa, 0xfd, 0xc1, 0xc9, 0xcd, 0xd3,
|
||||
0xda, 0xdd, 0xe0, 0xe8, 0xec, 0xf2,
|
||||
0xf9, 0xc0, 0xc8, 0xcc, 0xd2, 0xd9,
|
||||
0xe2, 0xea, 0xee, 0xf4, 0xfb, 0xc2,
|
||||
0xca, 0xce, 0xd4, 0xdb, 0xe5, 0xc5,
|
||||
0xf8, 0xd8, 0xe3, 0xf1, 0xf5, 0xc3,
|
||||
0xd1, 0xd5, 0xe6, 0xc6, 0xe7, 0xc7,
|
||||
0xfe, 0xf0, 0xde, 0xd0, 0xa3, 0x153,
|
||||
0x152, 0xa1, 0xbf)]
|
||||
# And here is the offset at which the Unicode Translation Table
|
||||
# starts.
|
||||
UTT_OFFSET = 155
|
||||
|
||||
# This subclass just lists all the "special" character codes that
|
||||
# are capturable from an input stream. They're just there so that
|
||||
# the user of the virtual machine can give them a nice name.
|
||||
class Input:
|
||||
DELETE = 8
|
||||
ESCAPE = 27
|
||||
# The cursor pad
|
||||
CUR_UP = 129
|
||||
CUR_DOWN = 130
|
||||
CUR_LEFT = 131
|
||||
CUR_RIGHT = 132
|
||||
# The Function keys
|
||||
F1 = 133
|
||||
F2 = 134
|
||||
F3 = 135
|
||||
F4 = 136
|
||||
F5 = 137
|
||||
F6 = 138
|
||||
F7 = 139
|
||||
F8 = 140
|
||||
F9 = 141
|
||||
F10 = 142
|
||||
F11 = 143
|
||||
F12 = 144
|
||||
# The numpad (keypad) keys.
|
||||
KP_0 = 145
|
||||
KP_1 = 146
|
||||
KP_2 = 147
|
||||
KP_3 = 148
|
||||
KP_4 = 149
|
||||
KP_5 = 150
|
||||
KP_6 = 151
|
||||
KP_7 = 152
|
||||
KP_8 = 153
|
||||
KP_9 = 154
|
||||
|
||||
def __init__(self, zmem):
|
||||
self._mem = zmem
|
||||
self._output_table = {
|
||||
0 : "",
|
||||
10: "\n"
|
||||
}
|
||||
self._input_table = {
|
||||
"\n": 10
|
||||
}
|
||||
|
||||
self._load_unicode_table()
|
||||
|
||||
# Populate the input and output tables with the ASCII and UTT
|
||||
# characters.
|
||||
for code,char in [(x,chr(x)) for x in range(32,127)]:
|
||||
self._output_table[code] = char
|
||||
self._input_table[char] = code
|
||||
|
||||
# Populate the input table with the extra "special" input
|
||||
# codes. The cool trick we use here, is that all these values
|
||||
# are in fact numbers, so their key will be available in both
|
||||
# dicts, and ztoa will provide the correct code if you pass it
|
||||
# a special symbol instead of a character to translate!
|
||||
#
|
||||
# Oh and we also pull the items from the subclass into this
|
||||
# instance, so as to make reference to these special codes
|
||||
# easier.
|
||||
for name,code in [(c,v) for c,v in list(self.Input.__dict__.items())
|
||||
if not c.startswith('__')]:
|
||||
self._input_table[code] = code
|
||||
setattr(self, name, code)
|
||||
|
||||
# The only special support required for ZSCII: ZM v5 defines
|
||||
# an extra character code to represent a mouse click. If we're
|
||||
# booting a v5 ZM, define this.
|
||||
if self._mem.version == 5:
|
||||
self.MOUSE_CLICK = 254
|
||||
self._input_table[254] = 254
|
||||
|
||||
def _load_unicode_table(self):
|
||||
if self._mem.version == 5:
|
||||
# Read the header extension table address
|
||||
ext_table_addr = self._mem.read_word(0x36)
|
||||
|
||||
# If:
|
||||
# - The extension header's address is non-null
|
||||
# - There are at least 3 words in the extension header
|
||||
# (the unicode translation table is the third word)
|
||||
# - The 3rd word (unicode translation table address) is
|
||||
# non-null
|
||||
#
|
||||
# Then there is a unicode translation table other than the
|
||||
# default that needs loading.
|
||||
if (ext_table_addr != 0 and
|
||||
self._mem.read_word(ext_table_addr) >= 3 and
|
||||
self._mem.read_word(ext_table_addr+6) != 0):
|
||||
|
||||
# The first byte is the number of unicode characters
|
||||
# in the table.
|
||||
utt_len = self._mem[ext_table_addr]
|
||||
|
||||
# Build the range of addresses to load from, and build
|
||||
# the unicode translation table as a list of unicode
|
||||
# chars.
|
||||
utt_range = range(ext_table+1, ext_table+1+(utt_len*2), 2)
|
||||
utt = [chr(self._mem.read_word(i)) for i in utt_range]
|
||||
else:
|
||||
utt = self.DEFAULT_UTT
|
||||
|
||||
# One way or another, we have a unicode translation
|
||||
# table. Add all the characters in it to the input and
|
||||
# output translation tables.
|
||||
for zscii, unichar in zip(itertools.count(155), utt):
|
||||
self._output_table[zscii] = unichar
|
||||
self._input_table[unichar] = zscii
|
||||
|
||||
def ztou(self, index):
|
||||
"""Translate the given ZSCII code into the corresponding
|
||||
output Unicode character and return it, or raise an exception if
|
||||
the requested index has no translation."""
|
||||
try:
|
||||
return self._output_table[index]
|
||||
except KeyError:
|
||||
raise IndexError("No such ZSCII character")
|
||||
|
||||
def utoz(self, char):
|
||||
"""Translate the given Unicode code into the corresponding
|
||||
input ZSCII character and return it, or raise an exception if
|
||||
the requested character has no translation."""
|
||||
try:
|
||||
return self._input_table[char]
|
||||
except KeyError:
|
||||
raise IndexError("No such input character")
|
||||
|
||||
def get(self, zscii):
|
||||
return ''.join([self.ztou(c) for c in zscii])
|
||||
|
||||
|
||||
class ZStringFactory:
|
||||
def __init__(self, zmem):
|
||||
self._mem = zmem
|
||||
self.zstr = ZStringTranslator(zmem)
|
||||
self.zchr = ZCharTranslator(zmem)
|
||||
self.zscii = ZsciiTranslator(zmem)
|
||||
|
||||
def get(self, addr):
|
||||
zstr = self.zstr.get(addr)
|
||||
zchr = self.zchr.get(zstr)
|
||||
return self.zscii.get(zchr)
|
||||
31
src/mudlib/zmachine/zui.py
Normal file
31
src/mudlib/zmachine/zui.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#
|
||||
# A class representing the entire user interface of a Z-Machine.
|
||||
#
|
||||
# For the license of this file, please consult the LICENSE file in the
|
||||
# root directory of this distribution.
|
||||
#
|
||||
|
||||
from . import zaudio, zfilesystem, zscreen, zstream
|
||||
|
||||
|
||||
class ZUI:
|
||||
"""This class encapsulates the entire user interface of a
|
||||
Z-Machine, providing access to all functionality that the end-user
|
||||
directly experiences or interacts with."""
|
||||
|
||||
def __init__(self, audio, screen, keyboard_input, filesystem):
|
||||
"""Initializes the ZUI with the given components."""
|
||||
|
||||
assert isinstance(audio, zaudio.ZAudio)
|
||||
assert isinstance(screen, zscreen.ZScreen)
|
||||
assert isinstance(keyboard_input, zstream.ZInputStream)
|
||||
assert isinstance(filesystem, zfilesystem.ZFilesystem)
|
||||
|
||||
# The following are all public attributes of the instance, but
|
||||
# should be considered read-only. In the future, we may want
|
||||
# to make them Python properties.
|
||||
|
||||
self.audio = audio
|
||||
self.screen = screen
|
||||
self.keyboard_input = keyboard_input
|
||||
self.filesystem = filesystem
|
||||
251
tests/test_zmachine_opcodes.py
Normal file
251
tests/test_zmachine_opcodes.py
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
"""
|
||||
Unit tests for the 12 newly implemented Z-machine opcodes.
|
||||
|
||||
These tests verify the basic behavior of each opcode by mocking the
|
||||
required dependencies (memory, stack, decoder, etc).
|
||||
"""
|
||||
|
||||
from unittest import TestCase
|
||||
from unittest.mock import Mock
|
||||
|
||||
from mudlib.zmachine.zcpu import ZCpu, ZCpuDivideByZero, ZCpuQuit
|
||||
|
||||
|
||||
class MockMemory:
|
||||
"""Mock memory for testing."""
|
||||
|
||||
def __init__(self):
|
||||
self.data = bytearray(65536)
|
||||
self.version = 3
|
||||
self.globals = {}
|
||||
|
||||
def __getitem__(self, addr):
|
||||
return self.data[addr]
|
||||
|
||||
def __setitem__(self, addr, value):
|
||||
self.data[addr] = value & 0xFF
|
||||
|
||||
def read_word(self, addr):
|
||||
return (self.data[addr] << 8) | self.data[addr + 1]
|
||||
|
||||
def write_word(self, addr, value):
|
||||
self.data[addr] = (value >> 8) & 0xFF
|
||||
self.data[addr + 1] = value & 0xFF
|
||||
|
||||
def read_global(self, varnum):
|
||||
return self.globals.get(varnum, 0)
|
||||
|
||||
def write_global(self, varnum, value):
|
||||
self.globals[varnum] = value
|
||||
|
||||
|
||||
class MockStackManager:
|
||||
"""Mock stack manager for testing."""
|
||||
|
||||
def __init__(self):
|
||||
self.stack = []
|
||||
self.locals = [0] * 15
|
||||
|
||||
def push_stack(self, value):
|
||||
self.stack.append(value)
|
||||
|
||||
def pop_stack(self):
|
||||
return self.stack.pop()
|
||||
|
||||
def get_local_variable(self, index):
|
||||
return self.locals[index]
|
||||
|
||||
def set_local_variable(self, index, value):
|
||||
self.locals[index] = value
|
||||
|
||||
def finish_routine(self, return_value):
|
||||
# Mock implementation - just return a PC value
|
||||
return 0x1000
|
||||
|
||||
|
||||
class MockOpDecoder:
|
||||
"""Mock opcode decoder for testing."""
|
||||
|
||||
def __init__(self):
|
||||
self.program_counter = 0x800
|
||||
self.store_address = None
|
||||
self.branch_condition = True
|
||||
self.branch_offset = 2
|
||||
|
||||
def get_store_address(self):
|
||||
return self.store_address
|
||||
|
||||
def get_branch_offset(self):
|
||||
return (self.branch_condition, self.branch_offset)
|
||||
|
||||
|
||||
class MockUI:
|
||||
"""Mock UI for testing."""
|
||||
|
||||
def __init__(self):
|
||||
self.screen = Mock()
|
||||
self.screen.write = Mock()
|
||||
|
||||
|
||||
class ZMachineOpcodeTests(TestCase):
|
||||
"""Test suite for Z-machine opcodes."""
|
||||
|
||||
def setUp(self):
|
||||
"""Create a minimal CPU for testing."""
|
||||
self.memory = MockMemory()
|
||||
self.stack = MockStackManager()
|
||||
self.decoder = MockOpDecoder()
|
||||
self.ui = MockUI()
|
||||
|
||||
# Create CPU with mocked dependencies
|
||||
self.cpu = ZCpu(
|
||||
self.memory,
|
||||
self.decoder,
|
||||
self.stack,
|
||||
Mock(), # objects
|
||||
Mock(), # string
|
||||
Mock(), # stream manager
|
||||
self.ui,
|
||||
)
|
||||
|
||||
def test_op_nop(self):
|
||||
"""Test NOP does nothing."""
|
||||
# Should just return without error
|
||||
self.cpu.op_nop()
|
||||
|
||||
def test_op_new_line(self):
|
||||
"""Test new_line prints a newline."""
|
||||
self.cpu.op_new_line()
|
||||
self.ui.screen.write.assert_called_once_with("\n")
|
||||
|
||||
def test_op_ret_popped(self):
|
||||
"""Test ret_popped pops stack and returns."""
|
||||
self.stack.push_stack(42)
|
||||
self.cpu.op_ret_popped()
|
||||
# Should have popped the value and set PC
|
||||
self.assertEqual(len(self.stack.stack), 0)
|
||||
self.assertEqual(self.cpu._opdecoder.program_counter, 0x1000)
|
||||
|
||||
def test_op_pop(self):
|
||||
"""Test pop discards top of stack."""
|
||||
self.stack.push_stack(100)
|
||||
self.stack.push_stack(200)
|
||||
self.cpu.op_pop()
|
||||
self.assertEqual(len(self.stack.stack), 1)
|
||||
self.assertEqual(self.stack.stack[0], 100)
|
||||
|
||||
def test_op_quit(self):
|
||||
"""Test quit raises exception."""
|
||||
with self.assertRaises(ZCpuQuit):
|
||||
self.cpu.op_quit()
|
||||
|
||||
def test_op_dec(self):
|
||||
"""Test decrement variable."""
|
||||
# Set local variable 1 to 10
|
||||
self.stack.set_local_variable(0, 10)
|
||||
# Decrement it (variable 1 = local 0)
|
||||
self.cpu.op_dec(1)
|
||||
# Should be 9 now
|
||||
self.assertEqual(self.stack.get_local_variable(0), 9)
|
||||
|
||||
def test_op_dec_wrapping(self):
|
||||
"""Test decrement wraps at zero."""
|
||||
# Set local variable 1 to 0
|
||||
self.stack.set_local_variable(0, 0)
|
||||
# Decrement it
|
||||
self.cpu.op_dec(1)
|
||||
# Should wrap to 65535
|
||||
self.assertEqual(self.stack.get_local_variable(0), 65535)
|
||||
|
||||
def test_op_not(self):
|
||||
"""Test bitwise NOT."""
|
||||
self.decoder.store_address = 0 # Store to stack
|
||||
self.cpu.op_not(0x00FF)
|
||||
result = self.stack.pop_stack()
|
||||
self.assertEqual(result, 0xFF00)
|
||||
|
||||
def test_op_not_all_ones(self):
|
||||
"""Test NOT of all ones gives zero."""
|
||||
self.decoder.store_address = 0
|
||||
self.cpu.op_not(0xFFFF)
|
||||
result = self.stack.pop_stack()
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
def test_op_load(self):
|
||||
"""Test load reads variable."""
|
||||
# Set local variable 2 to 42
|
||||
self.stack.set_local_variable(1, 42)
|
||||
self.decoder.store_address = 0 # Store to stack
|
||||
# Load variable 2
|
||||
self.cpu.op_load(2)
|
||||
result = self.stack.pop_stack()
|
||||
self.assertEqual(result, 42)
|
||||
|
||||
def test_op_mod_positive(self):
|
||||
"""Test modulo with positive numbers."""
|
||||
self.decoder.store_address = 0
|
||||
self.cpu.op_mod(17, 5)
|
||||
result = self.stack.pop_stack()
|
||||
self.assertEqual(result, 2)
|
||||
|
||||
def test_op_mod_negative_dividend(self):
|
||||
"""Test modulo with negative dividend."""
|
||||
self.decoder.store_address = 0
|
||||
# -17 mod 5 = -2 (z-machine uses C-style truncation toward zero)
|
||||
self.cpu.op_mod(self.cpu._unmake_signed(-17), 5)
|
||||
result = self.cpu._make_signed(self.stack.pop_stack())
|
||||
self.assertEqual(result, -2)
|
||||
|
||||
def test_op_mod_divide_by_zero(self):
|
||||
"""Test modulo by zero raises exception."""
|
||||
self.decoder.store_address = 0
|
||||
with self.assertRaises(ZCpuDivideByZero):
|
||||
self.cpu.op_mod(10, 0)
|
||||
|
||||
def test_op_storeb(self):
|
||||
"""Test store byte to memory."""
|
||||
self.cpu.op_storeb(0x1000, 5, 0x42)
|
||||
self.assertEqual(self.memory[0x1005], 0x42)
|
||||
|
||||
def test_op_storeb_truncates(self):
|
||||
"""Test store byte truncates to 8 bits."""
|
||||
self.cpu.op_storeb(0x2000, 10, 0x1FF)
|
||||
self.assertEqual(self.memory[0x200A], 0xFF)
|
||||
|
||||
def test_op_jg_true(self):
|
||||
"""Test jump if greater (signed) - true case."""
|
||||
self.decoder.branch_condition = True
|
||||
self.decoder.branch_offset = 100
|
||||
old_pc = self.cpu._opdecoder.program_counter
|
||||
self.cpu.op_jg(10, 5)
|
||||
# Should have branched (offset - 2)
|
||||
self.assertEqual(self.cpu._opdecoder.program_counter, old_pc + 98)
|
||||
|
||||
def test_op_jg_false(self):
|
||||
"""Test jump if greater (signed) - false case."""
|
||||
self.decoder.branch_condition = True # Branch if true (but test is false)
|
||||
self.decoder.branch_offset = 100
|
||||
old_pc = self.cpu._opdecoder.program_counter
|
||||
self.cpu.op_jg(5, 10)
|
||||
# Should not have branched (test is false, condition is true)
|
||||
self.assertEqual(self.cpu._opdecoder.program_counter, old_pc)
|
||||
|
||||
def test_op_jg_signed(self):
|
||||
"""Test jump if greater handles signed comparison."""
|
||||
# -1 (as unsigned 65535) should NOT be greater than 1
|
||||
self.decoder.branch_condition = False
|
||||
old_pc = self.cpu._opdecoder.program_counter
|
||||
self.cpu.op_jg(65535, 1)
|
||||
# Should not branch (false condition matches)
|
||||
self.assertEqual(self.cpu._opdecoder.program_counter, old_pc)
|
||||
|
||||
def test_op_pull(self):
|
||||
"""Test pull from stack to variable."""
|
||||
# Push value onto stack
|
||||
self.stack.push_stack(123)
|
||||
# Pull into local variable 1
|
||||
self.cpu.op_pull(1)
|
||||
# Should have stored in local variable 0 (variable 1)
|
||||
self.assertEqual(self.stack.get_local_variable(0), 123)
|
||||
# Stack should be empty
|
||||
self.assertEqual(len(self.stack.stack), 0)
|
||||
Loading…
Reference in a new issue