#!/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.*?)\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())