#!/usr/bin/env python3 """Diagnostic script to trace z-string decoding in Zork 1. This script loads Zork 1 and traces the z-string decoding pipeline: 1. Dumps all abbreviations from the abbreviation table 2. Monkey-patches ZStringFactory.get() to log every string decode 3. Runs the game with piped input to capture string decoding during gameplay """ import inspect import sys from pathlib import Path def main(): # Add src to path so we can import mudlib project_root = Path(__file__).parent.parent sys.path.insert(0, str(project_root / "src")) from mudlib.zmachine import ZMachine from mudlib.zmachine.trivialzui import create_zui from mudlib.zmachine.zstring import ZStringFactory print("=" * 80) print("Z-STRING DECODING DIAGNOSTIC FOR ZORK 1") print("=" * 80) print() story_path = project_root / "content" / "stories" / "zork1.z3" if not story_path.exists(): print(f"ERROR: Story file not found at {story_path}") sys.exit(1) with open(story_path, "rb") as f: story_bytes = f.read() print(f"Loaded {len(story_bytes)} bytes from {story_path.name}") print() # Create ZUI and ZMachine ui = create_zui() zmachine = ZMachine(story_bytes, ui, debugmode=False) # Access the string factory string_factory = zmachine._stringfactory # PART 1: Dump all abbreviations print("=" * 80) print("ABBREVIATION TABLE DUMP") print("=" * 80) print() abbrevs = string_factory.zchr._abbrevs if not abbrevs: print("No abbreviations found (version 1 game or failed to load)") else: print(f"Total abbreviations: {len(abbrevs)}") print() for table in range(3): print(f"--- Table {table} ---") for index in range(32): key = (table, index) if key in abbrevs: zscii_codes = abbrevs[key] text = string_factory.zscii.get(zscii_codes) # Show ZSCII codes (truncate if too long) if len(zscii_codes) > 20: zscii_display = str(zscii_codes[:20]) + "..." else: zscii_display = str(zscii_codes) # Show text (truncate if too long) if len(text) > 60: text_display = repr(text[:60]) + "..." else: text_display = repr(text) print(f" [{table},{index:2d}] ZSCII={zscii_display}") print(f" Text={text_display}") print() # PART 2: Monkey-patch ZStringFactory.get() to trace all string decodes print("=" * 80) print("STRING DECODING TRACE") print("=" * 80) print() print("Format: [opcode args] -> addr") print(" zchars -> zscii -> text") print() def traced_get(self, addr): # Get caller information using inspect stack = inspect.stack() caller_name = "unknown" caller_info = "" # Walk up the stack to find the opcode function for frame_info in stack[1:]: # Skip our own frame func_name = frame_info.function if func_name.startswith("op_"): caller_name = func_name # Try to extract arguments from the frame frame_locals = frame_info.frame.f_locals # For op_print_addr, show the addr argument if func_name == "op_print_addr" and "addr" in frame_locals: caller_info = f" addr={frame_locals['addr']:#06x}" # For op_print_paddr, show packed address and conversion elif func_name == "op_print_paddr" and "string_paddr" in frame_locals: paddr = frame_locals["string_paddr"] # Show both the packed address and what it converts to # For z3, byte_addr = paddr * 2 byte_addr = paddr * 2 caller_info = f" paddr={paddr:#06x} (byte={byte_addr:#06x})" # For op_print_obj, show the object number elif func_name == "op_print_obj" and "obj" in frame_locals: caller_info = f" obj={frame_locals['obj']}" # For op_print, note it's inline (z-string follows opcode) elif func_name == "op_print": caller_info = " (inline z-string)" break # Get z-chars from ZStringTranslator zchars = self.zstr.get(addr) # Convert z-chars to ZSCII codes zscii_codes = self.zchr.get(zchars) # Convert ZSCII to Unicode text text = self.zscii.get(zscii_codes) # Log to stderr so it doesn't interfere with game output # Truncate long lists/strings for readability if len(zchars) > 20: zchars_display = str(zchars[:20]) + f"... (len={len(zchars)})" else: zchars_display = str(zchars) if len(zscii_codes) > 20: zscii_display = str(zscii_codes[:20]) + f"... (len={len(zscii_codes)})" else: zscii_display = str(zscii_codes) text_display = repr(text[:80]) + "..." if len(text) > 80 else repr(text) print(f"[{caller_name}{caller_info}] -> {addr:#06x}", file=sys.stderr) print(f" zchars={zchars_display}", file=sys.stderr) print(f" zscii={zscii_display}", file=sys.stderr) print(f" text={text_display}", file=sys.stderr) print(file=sys.stderr) return text # Apply the monkey patch ZStringFactory.get = traced_get # type: ignore[assignment] # PART 3: Run the game with piped input print("=" * 80) print("GAME OUTPUT (running interpreter with tracing enabled)") print("=" * 80) print() print( "Note: String decode traces are written to stderr and " "interleaved with game output." ) print("To separate them, run: python3 scripts/debug_zstrings.py 2>trace.log") print() print( "To see just the trace: " "python3 scripts/debug_zstrings.py 2>&1 >/dev/null | grep zchars" ) print() print("Press Ctrl+C to stop the game.") print() try: zmachine.run() except KeyboardInterrupt: print("\n[Interrupted by user]") except Exception as e: print(f"\n[Game ended: {type(e).__name__}: {e}]") print() print("=" * 80) print("DIAGNOSTIC COMPLETE") print("=" * 80) if __name__ == "__main__": main()