mud/scripts/debug_zstrings.py

189 lines
6.4 KiB
Python
Executable file

#!/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()