- 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>
214 lines
8.0 KiB
Python
214 lines
8.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Compare the 40250 GC header registry with the classic wire adapter.
|
|
|
|
This is intentionally a static audit, not a replacement for wire tests. It
|
|
keeps the reference-side registry count and dynamic/static classification
|
|
machine-checkable so a new packet alias or compatibility branch cannot be
|
|
silently treated as 40250 parity.
|
|
"""
|
|
|
|
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
|
|
|
|
|
|
REFERENCE_DYNAMIC = {
|
|
"HEADER_GC_QUEST_INFO",
|
|
"HEADER_GC_DUEL_START",
|
|
"HEADER_GC_CHAT",
|
|
"HEADER_GC_SYNC_POSITION",
|
|
"HEADER_GC_SHOP",
|
|
"HEADER_GC_SCRIPT",
|
|
"HEADER_GC_MESSENGER",
|
|
"HEADER_GC_GUILD",
|
|
"HEADER_GC_DUNGEON",
|
|
"HEADER_GC_NPC_POSITION",
|
|
"HEADER_GC_LAND_LIST",
|
|
"HEADER_GC_HYBRIDCRYPT_KEYS",
|
|
"HEADER_GC_HYBRIDCRYPT_SDB",
|
|
}
|
|
|
|
# These are documented current-client adaptations. They remain differences:
|
|
# allowing them here makes the report useful without pretending they are in the
|
|
# reference CMainPacketHeaderMap.
|
|
KNOWN_CURRENT_DYNAMIC_ADAPTATIONS = {
|
|
"HDR_GC_WHISPER",
|
|
"HDR_GC_SYMBOL_DATA",
|
|
}
|
|
|
|
|
|
def active_lines(text: str, *, gaiden: bool = False) -> list[str]:
|
|
"""Keep the 40250 branch used by the checked-in Win32 project.
|
|
|
|
The snapshot contains both GAIDEN and non-GAIDEN enum aliases in Packet.h.
|
|
The UserInterface project does not define GAIDEN, so evaluating that one
|
|
branch is necessary before comparing numeric header identities. Other
|
|
optional blocks are intentionally retained: the registry source itself
|
|
uses the improved-encryption symbols supplied by ServiceDefs.h.
|
|
"""
|
|
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 reference_registry(path: Path) -> list[dict[str, str]]:
|
|
row = re.compile(
|
|
r"\bSet\((HEADER_GC_[A-Z0-9_]+),\s*"
|
|
r"CNetworkPacketHeaderMap::TPacketType\(sizeof\(([^)]+)\),\s*"
|
|
r"(STATIC_SIZE_PACKET|DYNAMIC_SIZE_PACKET)\)"
|
|
)
|
|
out: list[dict[str, str]] = []
|
|
for line in active_lines(path.read_text(errors="replace")):
|
|
match = row.search(line)
|
|
if match:
|
|
out.append({"header": match.group(1), "struct": match.group(2), "kind": match.group(3)})
|
|
return out
|
|
|
|
|
|
def function_body(text: str, name: str, return_type: str) -> str:
|
|
match = re.search(
|
|
rf"constexpr\s+{re.escape(return_type)}\s+{re.escape(name)}\s*\([^)]*\)\s*\{{(?P<body>.*?)\n\}}",
|
|
text,
|
|
re.S,
|
|
)
|
|
if not match:
|
|
raise ValueError(f"cannot find {name} in wire header")
|
|
return match.group("body")
|
|
|
|
|
|
def current_wire(path: Path) -> dict[str, list[str]]:
|
|
text = path.read_text(errors="replace")
|
|
size_body = function_body(text, "packet_size_gc", "int")
|
|
dynamic_body = function_body(text, "is_dynamic_gc", "bool")
|
|
case_re = re.compile(r"\bcase\s+(HDR_[A-Z0-9_]+)\s*:")
|
|
return {
|
|
"size_cases": case_re.findall(size_body),
|
|
"dynamic_cases": case_re.findall(dynamic_body),
|
|
}
|
|
|
|
|
|
def header_values(path: Path, prefix: str) -> dict[str, int]:
|
|
values: 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"), gaiden=False):
|
|
match = pattern.search(line)
|
|
if match:
|
|
values.setdefault(match.group(1), int(match.group(2), 0))
|
|
return values
|
|
|
|
|
|
def audit(reference_path: Path, wire_path: Path) -> dict:
|
|
ref = reference_registry(reference_path)
|
|
wire = current_wire(wire_path)
|
|
reference_values = header_values(reference_path.parent / "Packet.h", "HEADER_GC_")
|
|
current_values = header_values(wire_path, "HDR_")
|
|
ref_dynamic = {row["header"].replace("HEADER_", "HDR_") for row in ref if row["kind"] == "DYNAMIC_SIZE_PACKET"}
|
|
current_dynamic = set(wire["dynamic_cases"])
|
|
extras = sorted(current_dynamic - ref_dynamic)
|
|
missing = sorted(ref_dynamic - current_dynamic)
|
|
unexpected_extras = sorted(set(extras) - KNOWN_CURRENT_DYNAMIC_ADAPTATIONS)
|
|
reference_by_value: dict[int, list[dict[str, str]]] = {}
|
|
for row in ref:
|
|
value = reference_values.get(row["header"])
|
|
if value is not None:
|
|
reference_by_value.setdefault(value, []).append(row)
|
|
|
|
current_registered_names = set(wire["size_cases"]) | current_dynamic
|
|
current_by_value: dict[int, list[str]] = {}
|
|
for name in current_registered_names:
|
|
value = current_values.get(name)
|
|
if value is not None:
|
|
current_by_value.setdefault(value, []).append(name)
|
|
|
|
reference_values_set = set(reference_by_value)
|
|
current_values_set = set(current_by_value)
|
|
numeric_missing = sorted(reference_values_set - current_values_set)
|
|
numeric_extras = sorted(current_values_set - reference_values_set)
|
|
|
|
kind_mismatches: list[dict[str, object]] = []
|
|
for value, rows in sorted(reference_by_value.items()):
|
|
expected = {row["kind"] for row in rows}
|
|
actual = {
|
|
"DYNAMIC_SIZE_PACKET" if name in current_dynamic else "STATIC_SIZE_PACKET"
|
|
for name in current_by_value.get(value, [])
|
|
}
|
|
if actual and not actual.intersection(expected):
|
|
kind_mismatches.append({"value": value, "reference": rows, "current": current_by_value[value]})
|
|
|
|
result = {
|
|
"reference": {
|
|
"registered": len(ref),
|
|
"static": sum(row["kind"] == "STATIC_SIZE_PACKET" for row in ref),
|
|
"dynamic": sum(row["kind"] == "DYNAMIC_SIZE_PACKET" for row in ref),
|
|
"dynamic_headers": sorted(ref_dynamic),
|
|
"numeric_values": len(reference_values_set),
|
|
},
|
|
"current": {
|
|
"packet_size_gc_cases": len(set(wire["size_cases"])),
|
|
"is_dynamic_gc_cases": len(current_dynamic),
|
|
"dynamic_headers": sorted(current_dynamic),
|
|
"numeric_values": len(current_values_set),
|
|
},
|
|
"difference": {
|
|
"missing_reference_dynamic": missing,
|
|
"current_dynamic_extras": extras,
|
|
"unapproved_current_dynamic_extras": unexpected_extras,
|
|
"known_adaptations": sorted(set(extras) & KNOWN_CURRENT_DYNAMIC_ADAPTATIONS),
|
|
"missing_reference_numeric_values": numeric_missing,
|
|
"current_extra_numeric_values": numeric_extras,
|
|
"kind_mismatches": kind_mismatches,
|
|
},
|
|
"status": "PASS"
|
|
if not missing and not extras and not numeric_missing and not numeric_extras and not kind_mismatches
|
|
else "PARTIAL",
|
|
}
|
|
return result
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
default_root = Path(__file__).resolve().parents[4]
|
|
parser.add_argument("--repo-root", type=Path, default=default_root)
|
|
parser.add_argument("--strict", action="store_true", help="return non-zero when the registry is not exact")
|
|
args = parser.parse_args()
|
|
|
|
root = args.repo_root.resolve()
|
|
reference = refroot.reference_root(root) / "UserInterface/PythonNetworkStream.cpp"
|
|
wire = root / "extension/src/net/classic/wire_classic.h"
|
|
try:
|
|
result = audit(reference, wire)
|
|
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())
|