189 lines
6.4 KiB
Python
Executable file
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()
|