Wizard Sniffer is Glulx (.gblorb), not z-machine — out of scope. Lost Pig is V8, not V5 as originally assumed. Added milestone section documenting the V8 support work, bugs found and fixed, and new opcode implementations. Updated game descriptions and version notes. Added trace_lostpig.py utility script.
127 lines
3.5 KiB
Python
127 lines
3.5 KiB
Python
#!/usr/bin/env -S uv run --script
|
|
"""Trace Lost Pig V8 opcodes — find what's needed for V5+ support.
|
|
|
|
Runs the interpreter step-by-step, collecting unique opcodes hit.
|
|
When an unimplemented opcode is encountered, reports what was seen
|
|
and what's missing.
|
|
"""
|
|
|
|
# ruff: noqa: E402
|
|
import sys
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
project_root = Path(__file__).parent.parent
|
|
sys.path.insert(0, str(project_root / "src"))
|
|
|
|
from mudlib.zmachine import ZMachine, zopdecoder, zstream, zui
|
|
from mudlib.zmachine.trivialzui import (
|
|
TrivialAudio,
|
|
TrivialFilesystem,
|
|
TrivialScreen,
|
|
)
|
|
from mudlib.zmachine.zcpu import (
|
|
ZCpuNotImplemented,
|
|
ZCpuQuit,
|
|
ZCpuRestart,
|
|
)
|
|
|
|
story_path = project_root / "content" / "stories" / "LostPig.z8"
|
|
if not story_path.exists():
|
|
print(f"ERROR: {story_path} not found")
|
|
sys.exit(1)
|
|
|
|
story_bytes = story_path.read_bytes()
|
|
print(f"Loaded LostPig.z8: {len(story_bytes)} bytes, version {story_bytes[0]}")
|
|
|
|
|
|
class AutoInputStream(zstream.ZInputStream):
|
|
"""Input stream that auto-feeds commands."""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._input_count = 0
|
|
self._max_inputs = 3
|
|
|
|
def read_line(self, *args, **kwargs):
|
|
self._input_count += 1
|
|
if self._input_count > self._max_inputs:
|
|
raise ZCpuQuit
|
|
return "look"
|
|
|
|
|
|
audio = TrivialAudio()
|
|
screen = TrivialScreen()
|
|
keyboard = AutoInputStream()
|
|
filesystem = TrivialFilesystem()
|
|
ui = zui.ZUI(audio, screen, keyboard, filesystem)
|
|
zm = ZMachine(story_bytes, ui)
|
|
|
|
opcodes_seen = Counter()
|
|
step_count = 0
|
|
max_steps = 500_000
|
|
|
|
print(f"Tracing up to {max_steps} steps...")
|
|
print()
|
|
|
|
try:
|
|
while step_count < max_steps:
|
|
pc = zm._cpu._opdecoder.program_counter
|
|
(opcode_class, opcode_number, operands) = (
|
|
zm._cpu._opdecoder.get_next_instruction()
|
|
)
|
|
|
|
cls_str = zopdecoder.OPCODE_STRINGS.get(opcode_class, f"?{opcode_class}")
|
|
key = f"{cls_str}:{opcode_number:02x}"
|
|
|
|
try:
|
|
implemented, func = zm._cpu._get_handler(opcode_class, opcode_number)
|
|
except Exception as e:
|
|
print(f"ILLEGAL at step {step_count}: {key} (pc={pc:#x})")
|
|
print(f" {e}")
|
|
break
|
|
|
|
opcodes_seen[f"{key} ({func.__name__})"] += 1
|
|
|
|
if not implemented:
|
|
print(f"UNIMPLEMENTED at step {step_count}: {key} -> {func.__name__}")
|
|
print(f" PC: {pc:#x}")
|
|
print()
|
|
break
|
|
|
|
try:
|
|
func(zm._cpu, *operands)
|
|
except ZCpuQuit:
|
|
print(f"Game quit after {step_count} steps")
|
|
break
|
|
except ZCpuRestart:
|
|
print(f"Game restart after {step_count} steps")
|
|
break
|
|
except ZCpuNotImplemented as e:
|
|
print(f"NOT IMPLEMENTED at step {step_count}: {key} -> {func.__name__}")
|
|
print(f" PC: {pc:#x}")
|
|
print(f" Error: {e}")
|
|
print()
|
|
break
|
|
except Exception as e:
|
|
print(f"ERROR at step {step_count}: {key} -> {func.__name__}")
|
|
print(f" PC: {pc:#x}")
|
|
print(f" Operands: {operands}")
|
|
print(f" {type(e).__name__}: {e}")
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
zm._cpu._dump_trace()
|
|
break
|
|
|
|
step_count += 1
|
|
|
|
except KeyboardInterrupt:
|
|
print(f"\nInterrupted at step {step_count}")
|
|
|
|
print(f"\nTotal steps: {step_count}")
|
|
print(f"Unique opcodes seen: {len(opcodes_seen)}")
|
|
print()
|
|
print("Opcodes by frequency:")
|
|
for op, count in opcodes_seen.most_common():
|
|
print(f" {count:>8} {op}")
|