- refroot.py: one reference-root resolver (MT_40250_SOURCE, then manifest) used by every audit script; missing tree or fingerprint input is an error - port_map.py: per-function inventory of active 40250 units plus the 40250 Python root, status/queue/init/show/check, 40250: tag scan - manifest: fix 18 wrong reference paths (Python UI now points at the 40250 Client/Eternexus/root, not the m2dev assets/root), drop 2 deleted implementation files, move 64 prose test entries to evidence.commands - first port-map entry: PythonPlayerEventHandler.cpp - roadmap replaced by a batch/unit queue Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
146 lines
5.6 KiB
Python
146 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare 40250 phase switch values with the classic session gates.
|
|
|
|
The Win32 source has three kinds of cases that must not be confused: phase
|
|
payload handlers, transport/control packets consumed by ClassicStream, and
|
|
observer-only cases that are not in the main packet registry. Compare numeric
|
|
wire values so enum aliases cannot hide a missing branch.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import refroot # script directory is on sys.path when run directly
|
|
|
|
|
|
CONTROL_VALUES = {44, 152, 153, 250, 251, 252, 253, 254, 255}
|
|
OBSERVER_VALUES = {96, 97, 98}
|
|
|
|
|
|
def active_lines(text: str, *, gaiden: bool = False) -> list[str]:
|
|
out: list[str] = []
|
|
stack: list[bool] = []
|
|
enabled = True
|
|
for line in text.splitlines():
|
|
stripped = line.strip()
|
|
if stripped in {"#if defined(GAIDEN)", "#ifdef GAIDEN"}:
|
|
stack.append(enabled)
|
|
enabled = enabled and gaiden
|
|
continue
|
|
if stripped == "#ifndef GAIDEN":
|
|
stack.append(enabled)
|
|
enabled = enabled and not gaiden
|
|
continue
|
|
if stripped == "#else" and stack:
|
|
parent = stack[-1]
|
|
enabled = parent and not enabled
|
|
continue
|
|
if stripped.startswith("#endif") and stack:
|
|
enabled = stack.pop()
|
|
continue
|
|
if enabled and not stripped.startswith("//"):
|
|
out.append(line)
|
|
return out
|
|
|
|
|
|
def values(path: Path, prefix: str) -> dict[str, int]:
|
|
result: dict[str, int] = {}
|
|
pattern = re.compile(rf"\b({re.escape(prefix)}[A-Z0-9_]+)\s*=\s*(0x[0-9A-Fa-f]+|\d+)")
|
|
for line in active_lines(path.read_text(errors="replace")):
|
|
match = pattern.search(line)
|
|
if match:
|
|
result.setdefault(match.group(1), int(match.group(2), 0))
|
|
return result
|
|
|
|
|
|
def function_body(text: str, name: str) -> str:
|
|
match = re.search(rf"\bbool\s+{re.escape(name)}\s*\([^)]*\)\s*\{{", text)
|
|
if not match:
|
|
raise ValueError(f"cannot find {name}")
|
|
start = match.end()
|
|
depth = 1
|
|
index = start
|
|
while index < len(text) and depth:
|
|
if text[index] == "{":
|
|
depth += 1
|
|
elif text[index] == "}":
|
|
depth -= 1
|
|
index += 1
|
|
if depth:
|
|
raise ValueError(f"unterminated {name}")
|
|
return text[start:index - 1]
|
|
|
|
|
|
def case_values(body: str, prefix: str, table: dict[str, int]) -> set[int]:
|
|
result: set[int] = set()
|
|
for name in re.findall(rf"\b({re.escape(prefix)}[A-Z0-9_]+)\s*:", body):
|
|
if name in table:
|
|
result.add(table[name])
|
|
return result
|
|
|
|
|
|
def reference_phase_values(root: Path, filename: str) -> set[int]:
|
|
text = (root / "UserInterface" / filename).read_text(errors="replace")
|
|
enum_values = values(root / "UserInterface" / "Packet.h", "HEADER_GC_")
|
|
return case_values(text, "HEADER_GC_", enum_values)
|
|
|
|
|
|
def audit(repo_root: Path) -> dict:
|
|
reference_root = refroot.reference_root(repo_root)
|
|
wire = repo_root / "extension/src/net/classic/wire_classic.h"
|
|
session_path = repo_root / "extension/src/net/classic/classic_session.cpp"
|
|
reference_phases = {
|
|
"login": reference_phase_values(reference_root, "PythonNetworkStreamPhaseLogin.cpp"),
|
|
"select": reference_phase_values(reference_root, "PythonNetworkStreamPhaseSelect.cpp"),
|
|
"loading": reference_phase_values(reference_root, "PythonNetworkStreamPhaseLoading.cpp"),
|
|
"game": reference_phase_values(reference_root, "PythonNetworkStreamPhaseGame.cpp"),
|
|
}
|
|
current_values = values(wire, "HDR_GC_")
|
|
session_text = session_path.read_text(errors="replace")
|
|
current_phases = {
|
|
"login": case_values(function_body(session_text, "login_phase_packet"), "HDR_GC_", current_values),
|
|
"select": case_values(function_body(session_text, "select_phase_packet"), "HDR_GC_", current_values),
|
|
"loading": case_values(function_body(session_text, "loading_phase_packet"), "HDR_GC_", current_values),
|
|
"game": case_values(function_body(session_text, "game_phase_packet"), "HDR_GC_", current_values),
|
|
}
|
|
differences: dict[str, dict[str, list[int]]] = {}
|
|
for phase, reference in reference_phases.items():
|
|
ignored = CONTROL_VALUES | (OBSERVER_VALUES if phase == "game" else set())
|
|
expected = reference - ignored
|
|
actual = current_phases[phase] - ignored
|
|
differences[phase] = {
|
|
"reference_payload_not_current": sorted(expected - actual),
|
|
"current_not_reference_payload": sorted(actual - expected),
|
|
}
|
|
status = "PASS" if all(not values for row in differences.values() for values in row.values()) else "PARTIAL"
|
|
return {
|
|
"status": status,
|
|
"ignored_transport_control_values": sorted(CONTROL_VALUES),
|
|
"ignored_observer_values": sorted(OBSERVER_VALUES),
|
|
"reference_case_counts": {phase: len(items) for phase, items in reference_phases.items()},
|
|
"current_case_counts": {phase: len(items) for phase, items in current_phases.items()},
|
|
"differences": differences,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[4])
|
|
parser.add_argument("--strict", action="store_true")
|
|
args = parser.parse_args()
|
|
try:
|
|
result = audit(args.repo_root.resolve())
|
|
except (OSError, ValueError) as exc:
|
|
print(json.dumps({"status": "ERROR", "error": str(exc)}, indent=2, sort_keys=True))
|
|
return 2
|
|
print(json.dumps(result, indent=2, sort_keys=True))
|
|
return 1 if args.strict and result["status"] != "PASS" else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|