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
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_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 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_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 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(address) 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]
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:]
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("Return from routine with %d" % branch_offset) addr = self._stackmanager.finish_routine(branch_offset) self._opdecoder.program_counter = addr else: log("Jump to offset %+d" % branch_offset) self._opdecoder.program_counter += (branch_offset - 2)
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)
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 = [ord(x) for x in 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 _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 __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 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 >= 2**15): offset = -2**16 + offset log("Jump unconditionally to relative offset %d" % 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("PC has changed from from %x to %x" % (old_pc, new_pc))
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 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 >= 2**15): offset = - 2**16 + offset log("Jump unconditionally to relative offset %d" % 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("PC has changed from from %x to %x" % (old_pc, new_pc))
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.")
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 = [ord(x) for x in data] if len(bytes) != 13: raise QuetzalMalformedChunk chunk_release = (ord(data[0]) << 8) + ord(data[1]) chunk_serial = data[2:8] chunk_checksum = (ord(data[8]) << 8) + ord(data[9]) chunk_pc = (ord(data[10]) << 16) + (ord(data[11]) << 8) + ord(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 = [ord(x) for x in 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 _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 _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_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 actually 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_num == 0xC or opcode_num == 0x1A: log("Opcode has second operand byte") operands += self._parse_operands_byte() return (opcode_type, opcode_num, operands)
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("Generate random number in [1:%d]" % n) result = random.randint(1, n) elif n < 0: log("Seed PRNG with %d" % n) random.seed(n) else: log("Seed PRNG with time") random.seed(time.time()) self._write_result(result)
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("Reading next opcode at address %x" % current_pc) (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("Unimplemented opcode %s, " "halting execution" % func.__name__) break # The returned function is unbound, so we must pass # self to it ourselves. func(self, *operands)
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 _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 == None: result_addr = self._opdecoder.get_store_address() else: result_addr = store_addr if result_addr != None: if result_addr == 0x0: log("Push %d to stack" % result_value) self._stackmanager.push_stack(result_value) elif 0x0 < result_addr < 0x10: log("Local variable %d = %d" % (result_addr - 1, result_value)) self._stackmanager.set_local_variable(result_addr - 1, result_value) else: log("Global variable %d = %d" % (result_addr, result_value)) self._memory.write_global(result_addr, result_value)
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 == None: result_addr = self._opdecoder.get_store_address() else: result_addr = store_addr if result_addr != None: if result_addr == 0x0: log("Push %d to stack" % result_value) self._stackmanager.push_stack(result_value) elif 0x0 < result_addr < 0x10: log("Local variable %d = %d" % ( result_addr - 1, result_value)) self._stackmanager.set_local_variable(result_addr - 1, result_value) else: log("Global variable %d = %d" % (result_addr, result_value)) self._memory.write_global(result_addr, result_value)
def __init__(self, zmachine): log("Creating new instance of QuetzalWriter") self._zmachine = zmachine
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) # 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 != "FORM": raise QuetzalUnrecognizedFileFormat bytestring = self._file.read(4) self._len = ord(bytestring[0]) << 24 self._len += (ord(bytestring[1]) << 16) self._len += (ord(bytestring[2]) << 8) self._len += ord(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 != "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 == "IFhd": self._parse_ifhd(data) elif chunkname == "CMem": self._parse_cmem(data) elif chunkname == "UMem": self._parse_umem(data) elif chunkname == "Stks": self._parse_stks(data) elif chunkname == "IntD": self._parse_intd(data) elif chunkname == "AUTH": self._parse_auth(data) elif chunkname == "(c) ": self._parse_copyright(data) elif chunkname == "ANNO": self._parse_anno(data) else: # spec says to ignore and skip past unrecognized chunks pass except EOFError: pass self._file.close() log("Finished parsing Quetzal file.")
def _parse_anno(self, data): """Parse a chunk of type ANNO. Display any annotation""" log("Annotation: %s" % data) self._last_loaded_metadata["annotation"] = 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 __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_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 = [ord(x) for x in 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_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 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 _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 = [ord(x) for x in 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_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_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