From 55f733fda6338803bfc6bbef74af6b713fe81a5e Mon Sep 17 00:00:00 2001 From: shenlei Date: Tue, 22 Sep 2026 17:17:49 +0900 Subject: [PATCH] audit: phase 0 tooling for file-by-file 40250 porting - 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 --- .../skills/metin2-40250-parity-audit/SKILL.md | 14 +- .../references/audit-schema.md | 3 +- .../references/project-map.md | 17 +- .../scripts/audit_ledger.py | 11 +- .../scripts/audit_packet_registry.py | 4 +- .../scripts/audit_phase_dispatch.py | 4 +- .../scripts/audit_sequence_table.py | 4 +- .../scripts/audit_source_coverage.py | 6 +- .../scripts/port_map.py | 295 +++++++++++++++ .../scripts/refroot.py | 30 ++ audit/manifest.json | 220 +++++------ .../PythonPlayerEventHandler.cpp.json | 96 +++++ audit/remediation-roadmap.md | 353 ++---------------- audit/reports/source-coverage.md | 10 +- audit/source-coverage.json | 19 +- project/net_play.gd | 3 + 16 files changed, 621 insertions(+), 468 deletions(-) create mode 100644 .agents/skills/metin2-40250-parity-audit/scripts/port_map.py create mode 100644 .agents/skills/metin2-40250-parity-audit/scripts/refroot.py create mode 100644 audit/port-map/UserInterface/PythonPlayerEventHandler.cpp.json diff --git a/.agents/skills/metin2-40250-parity-audit/SKILL.md b/.agents/skills/metin2-40250-parity-audit/SKILL.md index 0ab659fa..02fb76e9 100644 --- a/.agents/skills/metin2-40250-parity-audit/SKILL.md +++ b/.agents/skills/metin2-40250-parity-audit/SKILL.md @@ -30,7 +30,7 @@ The 40250 source is the specification. Do not design behavior; transcribe it. Co | --- | --- | --- | | Platform-independent C++ logic (packet structs/handlers, sequence table, formulas, state machines in `UserInterface/`, `GameLib/`; `EterPack`, `EterLocale`) | Transliterate C++ -> C++, keeping class/function structure and names so each function has one counterpart | `extension/src/` | | 40250 C++ logic that currently lives in GDScript | Port function by function, same names in snake_case, same order of statements | existing `.gd` owner | -| Python UI (`assets/root/*.py`, `uiscript/`) | Translate per `.py` file into its GDScript window, keeping method structure | `project/ui/` | +| Python UI (40250 `Client/Eternexus/root/*.py` + `uiscript/`, **not** `assets/root`) | Translate per `.py` file into its GDScript window, keeping method structure | `project/ui/` | | Direct3D, Granny, Miles, Win32 input/window, IME, DirectX math | Platform adapter; equivalence by observable output | `extension/`, `project/` | Mark every ported function with a one-line tag at its definition so coverage can be scanned @@ -39,9 +39,9 @@ mechanically: `// 40250: CInstanceBase::SetMoveSpeed` (C++) or `# 40250: CInstan ## Round workflow -1. **Pick a unit** from the queue in `audit/remediation-roadmap.md` (ordered by user-visible - gameplay; see Priority). Skip units whose port-map entries are all `PORTED`/`ADAPTED`/`N_A` and - whose reference hash is unchanged. +1. **Pick a unit** from the current batch in `audit/remediation-roadmap.md`, or from + `port_map.py queue` (ordered by user-visible gameplay; see Priority). Run + `port_map.py init ` and `port_map.py show ` to get the function list. 2. **Read the whole reference unit** under the reference root (see `references/project-map.md`; use `grep -a`, many files contain CP949 bytes). Read every current counterpart, found by the `40250:` tags, the port-map entry, or `references/project-map.md`. @@ -110,8 +110,12 @@ behavior verified from code inspection alone. ## Tools +- `scripts/port_map.py status | queue | init | show | check` — function inventory + (active `.vcxproj` sources + the 40250 Python root), per-unit status, and tag/port-map consistency. + Run `check` before every commit that touches `audit/port-map/`. - `scripts/audit_ledger.py refresh --write | report --write | validate` — run once per batch, not - per round. Missing reference files are an error, not a silent fingerprint. + per round. Missing reference, implementation or test files are an error; `evidence.tests` holds + paths only, command lines go in `evidence.commands`. - `scripts/audit_packet_registry.py`, `audit_phase_dispatch.py`, `audit_sequence_table.py` — run when a round touches `extension/src/net/`. - `scripts/audit_source_coverage.py` — maps active 40250 sources (from the `.vcxproj` files) to diff --git a/.agents/skills/metin2-40250-parity-audit/references/audit-schema.md b/.agents/skills/metin2-40250-parity-audit/references/audit-schema.md index 146af97e..4421000f 100644 --- a/.agents/skills/metin2-40250-parity-audit/references/audit-schema.md +++ b/.agents/skills/metin2-40250-parity-audit/references/audit-schema.md @@ -18,7 +18,8 @@ Required fields: - `equivalence`: implementation-equivalence matrix described below. - `platform_adaptations`: documented engine/platform substitutions; use an empty array when none exist. - `evidence.contract`: detailed Markdown contract path relative to the repository root. -- `evidence.tests`: test paths relative to the repository root. +- `evidence.tests`: test file paths relative to the repository root (paths only; missing files fail `refresh`). +- `evidence.commands` (optional): command lines and live/manual evidence descriptions. - `evidence.last_test_result`: `PASS`, `FAIL`, or `NOT_RUN`. - `remaining`: explicit unverified branches; use an empty array only when none remain. diff --git a/.agents/skills/metin2-40250-parity-audit/references/project-map.md b/.agents/skills/metin2-40250-parity-audit/references/project-map.md index e23df617..e256dfbf 100644 --- a/.agents/skills/metin2-40250-parity-audit/references/project-map.md +++ b/.agents/skills/metin2-40250-parity-audit/references/project-map.md @@ -8,9 +8,12 @@ there are not product code. - Many `UserInterface/` files contain CP949 bytes in comments: use `grep -a` / `grep -arn`, or plain grep silently reports no match. -- Python UI and game scripts are **not** in the source tree: they are in this repo at - `assets/root/*.py` (e.g. `uitarget.py`, `uitaskbar.py`) and `assets/uiscript/`. -- Server source (for protocol questions only, not client behavior): `../40250/Server Client TMP4/Server`. +- Python UI and game scripts: `../../Client/Eternexus/root/*.py` relative to the reference root + (90 files; identical to the shipped `Client/pack/root.epk`), window layouts in + `../../Client/Eternexus/uiscript/uiscript/`, locale data in `../../Client/Eternexus/locale_*/`. + **`assets/root` in this repo is a different (m2dev) version** with material differences + (e.g. `uitarget.py` button table); never use it as the 40250 reference. +- Server source (for protocol questions only, not client behavior): `../40250/Server Client TMP4/Server/metin2`. | Library | Lines | Content | | --- | ---: | --- | @@ -63,8 +66,10 @@ code path when that path exists. ```bash godot --headless --path project --script .gd # most project tests -cmake --build build-debug -j4 && ctest --test-dir build-debug --output-on-failure -j2 +cmake --build build -j8 && ctest --test-dir build --output-on-failure -j4 ``` -If `build-debug/CMakeCache.txt` points at another checkout path, reconfigure first: -`cmake -S . -B build-debug -DCMAKE_BUILD_TYPE=Debug`. Always finish with `git diff --check`. +`build/` (Debug) is the configured tree; it also writes `project/bin/libmtgodot.*.dylib`, so rebuild +before running Godot tests after pulling native changes (a stale dylib shows up as "method not +found" parse errors). `build-debug/` is a stale tree from another checkout; do not use it. +Always finish with `git diff --check`. diff --git a/.agents/skills/metin2-40250-parity-audit/scripts/audit_ledger.py b/.agents/skills/metin2-40250-parity-audit/scripts/audit_ledger.py index cbe5c7cf..0d1697ee 100755 --- a/.agents/skills/metin2-40250-parity-audit/scripts/audit_ledger.py +++ b/.agents/skills/metin2-40250-parity-audit/scripts/audit_ledger.py @@ -11,6 +11,8 @@ from collections import Counter from datetime import datetime, timezone from pathlib import Path +import refroot # script directory is on sys.path when run directly + STATUSES = { "UNMAPPED", "PARTIAL", "MAPPED", "STATIC_VERIFIED", "TEST_VERIFIED", @@ -67,8 +69,7 @@ def resolve_files(root: Path, data: dict, section: str) -> list[Path]: files = contract.get(section, {}).get("files", []) if section != "tests" else contract.get("evidence", {}).get("tests", []) base = root if section == "reference": - ref_root = Path(data.get("_reference_root", "")) - base = ref_root if ref_root.is_absolute() else root / ref_root + base = refroot.reference_root(root) return [(base / item).resolve() for item in files] @@ -77,8 +78,7 @@ def fingerprint(paths: list[Path]) -> str: for path in sorted(paths, key=lambda p: str(p)): digest.update(str(path).encode()) if not path.is_file(): - digest.update(b"") - continue + raise SystemExit(f"fingerprint input missing: {path}") digest.update(hashlib.sha256(path.read_bytes()).digest()) return digest.hexdigest() @@ -218,9 +218,8 @@ def command_next(_root: Path, manifest: dict, args: argparse.Namespace) -> int: def command_refresh(root: Path, manifest: dict, args: argparse.Namespace) -> int: changed = 0 - ref_root = manifest.get("reference_root", "") for contract in manifest["contracts"]: - working = {**contract, "_reference_root": ref_root} + working = contract current = { "reference": fingerprint(resolve_files(root, working, "reference")), "implementation": fingerprint(resolve_files(root, working, "implementation")), diff --git a/.agents/skills/metin2-40250-parity-audit/scripts/audit_packet_registry.py b/.agents/skills/metin2-40250-parity-audit/scripts/audit_packet_registry.py index d13b0f72..beccb70f 100644 --- a/.agents/skills/metin2-40250-parity-audit/scripts/audit_packet_registry.py +++ b/.agents/skills/metin2-40250-parity-audit/scripts/audit_packet_registry.py @@ -14,6 +14,8 @@ 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", @@ -196,7 +198,7 @@ def main() -> int: args = parser.parse_args() root = args.repo_root.resolve() - reference = root.parent / "40250/Server Client TMP4/ClientVS22/source/UserInterface/PythonNetworkStream.cpp" + reference = refroot.reference_root(root) / "UserInterface/PythonNetworkStream.cpp" wire = root / "extension/src/net/classic/wire_classic.h" try: result = audit(reference, wire) diff --git a/.agents/skills/metin2-40250-parity-audit/scripts/audit_phase_dispatch.py b/.agents/skills/metin2-40250-parity-audit/scripts/audit_phase_dispatch.py index d6cbb7e5..73505b2a 100644 --- a/.agents/skills/metin2-40250-parity-audit/scripts/audit_phase_dispatch.py +++ b/.agents/skills/metin2-40250-parity-audit/scripts/audit_phase_dispatch.py @@ -14,6 +14,8 @@ 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} @@ -88,7 +90,7 @@ def reference_phase_values(root: Path, filename: str) -> set[int]: def audit(repo_root: Path) -> dict: - reference_root = repo_root.parent / "40250/Server Client TMP4/ClientVS22/source" + 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 = { diff --git a/.agents/skills/metin2-40250-parity-audit/scripts/audit_sequence_table.py b/.agents/skills/metin2-40250-parity-audit/scripts/audit_sequence_table.py index 6a2641dd..0d0ce2aa 100644 --- a/.agents/skills/metin2-40250-parity-audit/scripts/audit_sequence_table.py +++ b/.agents/skills/metin2-40250-parity-audit/scripts/audit_sequence_table.py @@ -14,6 +14,8 @@ import json import re from pathlib import Path +import refroot # script directory is on sys.path when run directly + def read_array(path: Path, symbol: str) -> bytes: text = path.read_text(errors="replace") @@ -41,7 +43,7 @@ def main() -> int: args = parser.parse_args() root = args.repo_root.resolve() - reference = root.parent / "40250/Server Client TMP4/ClientVS22/source/EterLib/NetStream.cpp" + reference = refroot.reference_root(root) / "EterLib/NetStream.cpp" current = root / "extension/src/net/classic/sequence_table.h" try: ref = read_array(reference, "s_bSequenceTable") diff --git a/.agents/skills/metin2-40250-parity-audit/scripts/audit_source_coverage.py b/.agents/skills/metin2-40250-parity-audit/scripts/audit_source_coverage.py index d864e102..a03e8aba 100644 --- a/.agents/skills/metin2-40250-parity-audit/scripts/audit_source_coverage.py +++ b/.agents/skills/metin2-40250-parity-audit/scripts/audit_source_coverage.py @@ -15,6 +15,8 @@ import re import xml.etree.ElementTree as ET from collections import Counter, defaultdict +import refroot # script directory is on sys.path when run directly + REFERENCE_DIRS = ( "UserInterface", @@ -259,7 +261,7 @@ def infer_owners(path: str) -> list[str]: def build(repo: pathlib.Path) -> dict: - reference_root = (repo / "../40250/Server Client TMP4/ClientVS22/source").resolve() + reference_root = refroot.reference_root(repo) listed, exact_owners = manifest_data(repo) active = active_project_sources(reference_root) rows = [] @@ -283,7 +285,7 @@ def build(repo: pathlib.Path) -> dict: for owner in row["owners"]: by_contract[owner][row["kind"]] += 1 return { - "reference_root": "../40250/Server Client TMP4/ClientVS22/source", + "reference_root": json.loads((repo / "audit/manifest.json").read_text(encoding="utf-8"))["reference_root"], "active_source_count": len(rows), "manifest_exact_count": counts["manifest_exact"], "inferred_review_queue_count": counts["inferred_review_queue"], diff --git a/.agents/skills/metin2-40250-parity-audit/scripts/port_map.py b/.agents/skills/metin2-40250-parity-audit/scripts/port_map.py new file mode 100644 index 00000000..0a6f09c8 --- /dev/null +++ b/.agents/skills/metin2-40250-parity-audit/scripts/port_map.py @@ -0,0 +1,295 @@ +#!/usr/bin/env python3 +"""Per-function 40250 port-map: inventory, skeleton creation, coverage and queue. + +Units are the active 40250 C++ sources (from the .vcxproj files) plus the 40250 Python +root (`../../Client/Eternexus/root/*.py` relative to the reference root). Port-map entries +live in `audit/port-map/.json`; functions without an entry count as TODO. + + port_map.py status [--lib UserInterface] coverage per library (and per unit with --units) + port_map.py queue [--limit 20] next units by priority, then by TODO count + port_map.py init [...] create or extend entries (new functions as TODO) + port_map.py show per-function status of one unit + port_map.py check stale hashes, unknown functions, tag mismatches +""" + +from __future__ import annotations + +import argparse +import fnmatch +import hashlib +import json +import re +import sys +from collections import Counter +from pathlib import Path + +import refroot # script directory is on sys.path when run directly +from audit_source_coverage import active_project_sources + +STATUSES = ("TODO", "PORTED", "ADAPTED", "N_A", "DIVERGENT", "NEEDS_LIVE") +DONE = {"PORTED", "ADAPTED", "N_A"} +PY_ROOT = "../../Client/Eternexus/root" + +# Order follows SKILL.md "Priority"; the first matching pattern wins. +PRIORITY = [ + ("P0", ["UserInterface/InstanceBase*.cpp", "GameLib/ActorInstance.cpp", "GameLib/ActorInstanceMotion*.cpp", + "GameLib/ActorInstanceRotation.cpp", "GameLib/ActorInstancePosition.cpp", "GameLib/ActorInstanceEvent.cpp", + "GameLib/ActorInstanceCollisionDetection.cpp", "GameLib/ActorInstanceSync.cpp", + "UserInterface/PythonPlayer.cpp", "UserInterface/PythonPlayerInput*.cpp", + "UserInterface/PythonPlayerEventHandler.cpp", "UserInterface/PythonCharacterManager.cpp"]), + ("P1", ["GameLib/ActorInstanceBattle.cpp", "GameLib/ActorInstanceAttach*.cpp", "GameLib/ActorInstanceFly.cpp", + "GameLib/FlyingObject*.cpp", "GameLib/FlyTarget.cpp", "GameLib/FlyingInstance.cpp", "GameLib/FlyTrace.cpp", + "UserInterface/PythonPlayerSkill.cpp", "UserInterface/PythonSkill.cpp", "GameLib/RaceData*.cpp", + "GameLib/RaceMotionData*.cpp", "GameLib/ActorInstanceData.cpp", "GameLib/WeaponTrace.cpp"]), + ("P2", ["UserInterface/PythonNetworkStream*.cpp", "UserInterface/AccountConnector.cpp", + "UserInterface/ServerStateChecker.cpp", "EterLib/NetStream.cpp", "EterLib/NetPacketHeaderMap.cpp"]), + ("P3", ["UserInterface/PythonItem*.cpp", "UserInterface/PythonExchange*.cpp", "UserInterface/PythonSafeBox.cpp", + "UserInterface/PythonShop.cpp", "UserInterface/PythonQuest.cpp", "UserInterface/PythonGuild.cpp", + "UserInterface/PythonMessenger.cpp", "UserInterface/PythonChat*.cpp", "UserInterface/PythonTextTail.cpp", + "UserInterface/PythonMiniMap.cpp", "GameLib/ItemData.cpp", "GameLib/ItemManager.cpp", f"{PY_ROOT}/*.py"]), +] +DEFAULT_PRIORITY = "P4" + +# Column-0 definition: ` [Class::]name(` where the body `{` follows on this or the next line. +_DEF = re.compile(r"^(?!return\b|else\b|if\b|while\b|for\b|switch\b|case\b|#|//|/\*|\s)" + r"[^;(){}=]*?\b((?:[A-Za-z_]\w*::)*~?[A-Za-z_]\w*)\s*\(") +_PY_CLASS = re.compile(r"^class\s+(\w+)") +_PY_DEF = re.compile(r"^(\s*)def\s+(\w+)\s*\(") +_TAG = re.compile(r"(?://|#)\s*40250:\s*([A-Za-z_][\w:.~]*)") + + +def sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def cpp_functions(path: Path) -> list[str]: + lines = path.read_bytes().decode("latin-1").splitlines() + names: list[str] = [] + for i, line in enumerate(lines): + m = _DEF.match(line) + if not m or line.rstrip().endswith(";"): + continue + name = m.group(1) + if name.isupper() or name.split("::")[-1].isupper(): # macros + continue + rest = "\n".join(lines[i:i + 8]) + body = rest.find("{") + semi = rest.find(";") + if body < 0 or (0 <= semi < body): + continue + if name not in names: + names.append(name) + return names + + +def py_functions(path: Path) -> list[str]: + names: list[str] = [] + cls = "" + for line in path.read_bytes().decode("latin-1").splitlines(): + if m := _PY_CLASS.match(line): + cls = m.group(1) + continue + if m := _PY_DEF.match(line): + indent, fn = m.groups() + if not indent: + cls = "" + name = f"{cls}.{fn}" if cls and indent else fn + if name not in names: + names.append(name) + return names + + +def inventory(ref: Path) -> dict[str, Path]: + units = {p: ref / p for p in sorted(active_project_sources(ref)) if p.endswith(".cpp")} + py_dir = (ref / PY_ROOT).resolve() + if py_dir.is_dir(): + for p in sorted(py_dir.glob("*.py")): + units[f"{PY_ROOT}/{p.name}"] = p + return units + + +def functions(path: Path) -> list[str]: + return py_functions(path) if path.suffix == ".py" else cpp_functions(path) + + +def priority(unit: str) -> str: + for level, patterns in PRIORITY: + if any(fnmatch.fnmatch(unit, pat) for pat in patterns): + return level + return DEFAULT_PRIORITY + + +def entry_path(repo: Path, unit: str) -> Path: + rel = unit.replace("../../Client/Eternexus/", "Client/") + return repo / "audit" / "port-map" / (rel + ".json") + + +def load_entry(repo: Path, unit: str) -> dict | None: + path = entry_path(repo, unit) + return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else None + + +def lib_of(unit: str) -> str: + return "Client/root" if unit.startswith(PY_ROOT) else unit.split("/")[0] + + +def unit_counts(repo: Path, unit: str, path: Path) -> Counter: + entry = load_entry(repo, unit) or {"functions": {}} + recorded = entry["functions"] + counts = Counter() + for fn in functions(path): + counts[recorded.get(fn, {}).get("status", "TODO")] += 1 + return counts + + +def resolve_unit(units: dict[str, Path], name: str) -> str: + if name in units: + return name + hits = [u for u in units if u.endswith("/" + name) or Path(u).name == name] + if len(hits) != 1: + raise SystemExit(f"unit {name!r}: {'ambiguous: ' + ', '.join(hits) if hits else 'not an active 40250 unit'}") + return hits[0] + + +def cmd_status(repo: Path, units: dict[str, Path], args) -> int: + per_lib: dict[str, Counter] = {} + rows = [] + for unit, path in units.items(): + lib = lib_of(unit) + if args.lib and lib != args.lib: + continue + counts = unit_counts(repo, unit, path) + per_lib.setdefault(lib, Counter()).update(counts) + rows.append((unit, counts)) + total = Counter() + print(f"{'library':<16}{'funcs':>7}{'done':>7} " + " ".join(f"{s:>10}" for s in STATUSES)) + for lib, c in sorted(per_lib.items()): + total.update(c) + n = sum(c.values()) + done = sum(c[s] for s in DONE) + print(f"{lib:<16}{n:>7}{done:>7} " + " ".join(f"{c[s]:>10}" for s in STATUSES)) + n = sum(total.values()) + done = sum(total[s] for s in DONE) + print(f"{'TOTAL':<16}{n:>7}{done:>7} " + " ".join(f"{total[s]:>10}" for s in STATUSES) + + f" ({100.0 * done / max(n, 1):.1f}% done)") + if args.units: + for unit, c in rows: + if sum(c.values()) != c["TODO"]: + print(f" {unit}: " + ", ".join(f"{s}={c[s]}" for s in STATUSES if c[s])) + return 0 + + +def cmd_queue(repo: Path, units: dict[str, Path], args) -> int: + rows = [] + for unit, path in units.items(): + c = unit_counts(repo, unit, path) + open_ = c["TODO"] + c["DIVERGENT"] + if open_: + rows.append((priority(unit), -open_, unit, c)) + for level, neg, unit, c in sorted(rows)[: args.limit]: + print(f"{level}\t{-neg:>4} open\t{unit}\t(TODO={c['TODO']} DIVERGENT={c['DIVERGENT']})") + return 0 + + +def cmd_init(repo: Path, units: dict[str, Path], args) -> int: + for name in args.units: + unit = resolve_unit(units, name) + path = units[unit] + entry = load_entry(repo, unit) or { + "reference": unit, "reference_sha256": sha256(path), "priority": priority(unit), + "contracts": [], "functions": {}, + } + added = 0 + for fn in functions(path): + if fn not in entry["functions"]: + entry["functions"][fn] = {"status": "TODO"} + added += 1 + out = entry_path(repo, unit) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(json.dumps(entry, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + print(f"{out.relative_to(repo)}: {added} function(s) added, {len(entry['functions'])} total") + return 0 + + +def cmd_show(repo: Path, units: dict[str, Path], args) -> int: + unit = resolve_unit(units, args.unit) + recorded = (load_entry(repo, unit) or {"functions": {}})["functions"] + for fn in functions(units[unit]): + info = recorded.get(fn, {"status": "TODO"}) + impl = ", ".join(info.get("impl", [])) + print(f"{info['status']:<11}{fn}" + (f" -> {impl}" if impl else "") + (f" # {info['note']}" if info.get("note") else "")) + return 0 + + +def scan_tags(repo: Path) -> dict[str, list[str]]: + tags: dict[str, list[str]] = {} + for base, pattern in (("project", "*.gd"), ("extension/src", "*.[ch]*")): + for path in (repo / base).rglob(pattern): + if "/addons/" in path.as_posix(): + continue + for i, line in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), 1): + for m in _TAG.finditer(line): + tags.setdefault(m.group(1), []).append(f"{path.relative_to(repo)}:{i}") + return tags + + +def cmd_check(repo: Path, units: dict[str, Path], args) -> int: + problems = 0 + recorded_ported: set[str] = set() + for entry_file in sorted((repo / "audit" / "port-map").rglob("*.json")): + entry = json.loads(entry_file.read_text(encoding="utf-8")) + unit = entry.get("reference", "") + label = entry_file.relative_to(repo) + if unit not in units: + print(f"ERROR {label}: reference {unit!r} is not an active 40250 unit") + problems += 1 + continue + if entry.get("reference_sha256") != sha256(units[unit]): + print(f"STALE {label}: reference changed; recheck non-TODO functions, then update reference_sha256") + problems += 1 + known = set(functions(units[unit])) + for fn, info in entry.get("functions", {}).items(): + status = info.get("status") + if fn not in known: + print(f"ERROR {label}: {fn} is not defined in {unit}") + problems += 1 + if status not in STATUSES: + print(f"ERROR {label}: {fn} has invalid status {status!r}") + problems += 1 + if status in {"PORTED", "ADAPTED", "NEEDS_LIVE"} and not info.get("impl"): + print(f"ERROR {label}: {fn} is {status} without impl") + problems += 1 + if status in {"ADAPTED", "N_A", "DIVERGENT", "NEEDS_LIVE"} and not info.get("note"): + print(f"ERROR {label}: {fn} is {status} without note") + problems += 1 + if status == "ADAPTED" and not info.get("test"): + print(f"ERROR {label}: {fn} is ADAPTED without test") + problems += 1 + if status in {"PORTED", "ADAPTED"}: + recorded_ported.add(fn.split("::")[-1] if "::" not in fn else fn) + recorded_ported.add(fn) + tags = scan_tags(repo) + untracked = sorted(t for t in tags if t not in recorded_ported) + for tag in untracked: + print(f"WARN tag `40250: {tag}` at {tags[tag][0]} has no PORTED/ADAPTED port-map entry") + print(f"check: {problems} error(s), {len(untracked)} untracked tag(s), {len(tags)} tag(s) scanned") + return 1 if problems else 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + sub = parser.add_subparsers(dest="command", required=True) + p = sub.add_parser("status"); p.add_argument("--lib"); p.add_argument("--units", action="store_true") + p = sub.add_parser("queue"); p.add_argument("--limit", type=int, default=20) + p = sub.add_parser("init"); p.add_argument("units", nargs="+") + p = sub.add_parser("show"); p.add_argument("unit") + sub.add_parser("check") + args = parser.parse_args() + repo = refroot.repo_root() + units = inventory(refroot.reference_root(repo)) + return {"status": cmd_status, "queue": cmd_queue, "init": cmd_init, "show": cmd_show, + "check": cmd_check}[args.command](repo, units, args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.agents/skills/metin2-40250-parity-audit/scripts/refroot.py b/.agents/skills/metin2-40250-parity-audit/scripts/refroot.py new file mode 100644 index 00000000..9806c3b6 --- /dev/null +++ b/.agents/skills/metin2-40250-parity-audit/scripts/refroot.py @@ -0,0 +1,30 @@ +"""Locate the 40250 reference source tree for the audit scripts. + +Order: `MT_40250_SOURCE` env var, then `reference_root` in `audit/manifest.json` +(relative to the repository root). A missing tree is an error, never a silent fallback. +""" + +from __future__ import annotations + +import json +import os +from pathlib import Path + + +def repo_root() -> Path: + return Path(__file__).resolve().parents[4] + + +def reference_root(repo: Path | None = None) -> Path: + repo = (repo or repo_root()).resolve() + raw = os.environ.get("MT_40250_SOURCE") + if not raw: + manifest = json.loads((repo / "audit" / "manifest.json").read_text(encoding="utf-8")) + raw = manifest.get("reference_root", "") + if not raw: + raise SystemExit("40250 reference root not set: export MT_40250_SOURCE or set manifest reference_root") + path = Path(raw) + path = (path if path.is_absolute() else repo / path).resolve() + if not (path / "UserInterface").is_dir(): + raise SystemExit(f"40250 reference root not found or incomplete: {path}") + return path diff --git a/audit/manifest.json b/audit/manifest.json index c3241bf1..814966c7 100644 --- a/audit/manifest.json +++ b/audit/manifest.json @@ -118,13 +118,21 @@ "tests": [ "project/live_smoke_test.gd", "project/app_flow_lifecycle_test.gd", - "project/app_flow_lifecycle_test.gd (login failure UI owner)", "build/extension/net_classic_session_test", "build/extension/net_classic_stream_test", "build/extension/net_loopback_test", "project/p10_test.gd", "project/disconnect_world_reset_test.gd", "build/extension/net_text_codec_test", + "build/extension/net_classic_e2e", + "build-debug/extension/net_classic_e2e", + "project/auth_connect_bridge_test.gd", + "project/auth_peer_close_bridge_test.gd", + "project/char_create_delete_test.gd" + ], + "last_test_result": "PASS", + "commands": [ + "project/app_flow_lifecycle_test.gd (login failure UI owner)", "ctest --test-dir build --output-on-failure -j2", "build/extension/net_classic_e2e ", "build/extension/net_classic_e2e (expected login callback exit 5)", @@ -185,8 +193,7 @@ "net_classic_session_test pre-fix PHASE_DEAD no-op time-sync owner (FAIL: PHASE_DEAD enabled time-sync and changed the next handshake response; 1 check)", "net_classic_session_test pre-fix PHASE_DEAD close-owner preservation (FAIL: PHASE_DEAD cleared the explicit PHASE_CLOSE owner marker; 1 check)", "net_classic_session_test post-fix PHASE_DEAD no-op time-sync owner (PASS: PHASE_DEAD keeps the plain handshake owner and preserves the explicit close marker)" - ], - "last_test_result": "PASS" + ] }, "remaining": [ "network.login.phase_flow/auth-game-phase-matrix/live-package-order:真实服务端成功认证、四槽角色列表、选角、Loading 到 PHASE_GAME、错误密码 WRONGPWD 回调、game connect failure 和一次建立连接后的 peer close 已验证;认证成功后 game transport 先于 auth transport 清理的 40250 handoff 顺序已有 loopback 修复前失败/修复后通过证据;/logout 探针确认当前服务端 build 只产生 peer close,未产生客户端可观察的 GC_PHASE(PHASE_CLOSE),因此服务器主动 PHASE_CLOSE、空角色、完整服务器包序矩阵和跨阶段 retry 仍缺 live fixture;半包、重复 phase、PHASE_CLOSE 旧 world 和 PHASE_CLOSE/peer-close 不发送旧连接登录包的本地测试已覆盖", @@ -198,9 +205,9 @@ ], "notes": "按 40250 SetLoadingPhase/SetGamePhase 修复 Loading 遮罩阶段归属和无 SceneTree 选人确认吞包;按 ClearLoginInfo 修复 CG_LOGIN/CG_LOGIN3 成功发送后的密码生命周期;auth-game-phase-matrix 修复 PHASE_CLOSE 回 LoginPhase、Loading/Game phase-leave world 清理、宿主 world_reset 通知、重复 phase handler、auth/game 登录拒绝保留 phase 和登录失败回调;本轮 auth-success handoff 按 CAccountConnector::__AuthState_RecvAuthSuccess -> SetLoginKey -> MainStream.Connect -> AccountConnector.Disconnect 固定为 game transport 先连接、auth transport 后清理,并以 loopback 失败前/后顺序回归;login-failure-retry-cleanup 现已补齐认证端 peer close -> Offline owner、认证 peer-close bridge 保留非终态 LoginPhase owner、认证 PHASE_CLOSE 静默保留 AUTH owner、游戏 peer close 前置通知、旧 world 清理、DirectEnter/普通 LoginPhase game connect failure 的独立 owner、认证 connect_failed bridge、显式重试请求保护、普通 Select/Game -> Login 的旧角色/world 清理、SELECT/Loading 的 phase-only LoginPhase owner、LOGIN 阶段失败保留原 LoginPhase UI、LoginPhase retry 只替换认证 transport 且不触发重复全局 world_reset、PHASE_AUTH 前登录包的阶段隔离、active Loading/Game 未知头按 RecvErrorPacket 清理缓冲并保留 phase owner、游戏态 PHASE_CLOSE/peer-close 后 AppFlow 释放 GameScene 并切换为唯一 LoginPhase owner、显式 PHASE_CLOSE 不再伪造 transport disconnected、Loading PHASE_CLOSE leave owner/world 清理、GC_WARP/快速重连同步连接失败后的 retry owner,以及空重连角色列表回到 SELECT 且不发送过期槽位;同步增加 ClassicSession 析构前 stream 回调解绑,避免最终 Offline 通知写入已析构字段;本轮 legacy-text-wire-encoding 固定 UTF-8 内部与 GB2312/兼容 wire 边界,修正 24-byte 角色名容量和 GCLoginSuccess4 329-byte 布局;本轮 live-package-order 让实时探针观察应用层登录失败和 game remote-disconnect 回调,不把 40250 的失败回调误报为 timeout;本轮 ticket-slot-warp-order 修复 GC_WARP 复用 session 时继承旧 PHASE_GAME 在线标记,并用回归测试核对 DirectEnter/GC_WARP 的 CG_LOGIN2 包序和 login key;本轮 direct-enter-loading-select-packet 按 40250 introloading.LoadingWindow.Open 的实际调用链补发 DirectEnter 的 CG_PLAYER_SELECT,实时探针已到达 PHASE_GAME;本轮 gc-warp-live-package-order 新增 e2e/Godot live 验证入口,确认真实 GC_WARP 的 world_reset、replacement CG_LOGIN2/CG_PLAYER_SELECT、Loading 和第二次 PHASE_GAME 成功,未发现生产连接逻辑差异;本轮 optional-security-branches 为静态可选包和 HybridCrypt 动态包增加显式 deployment guard/optional_security_required,保持 phase owner 但不伪装成已实现 vendor runtime;本轮 peer-close-login-owner 依据真实 /logout peer-close 证据,移除 AppFlow 的自动重连遮罩/旧 GameScene 保留,回归 LoginPhase owner;本轮 phase-close-server-fixture 分离显式 PHASE_CLOSE 与 OnRemoteDisconnect,不再由 native bridge 发出伪造的 disconnected 信号;本轮 empty-role-live-fixture 为真实空角色包序增加 MT_CLASSIC_EXPECT_EMPTY=1 探针模式,当前仍缺全空账号正向 live 证据;本轮 login-form-retry-owner 按 40250 LoginWindow.Connect/AccountConnector.Connect 增加 LoginPhase retry owner 和只替换认证 transport 的 native/UI 回归;本轮 game-connect-immediate-failure-owner 按 CPythonNetworkStream::OnConnectFailure 分离普通 LoginWindow.OnConnectFailure 与 DirectEnter ClosePhase,并新增 M2Client connect_failed signal,GC_WARP 失败不再伪造 disconnected;本轮 auth-connect-bridge-owner 按 CAccountConnector::OnConnectFailure -> Offline -> LoginWindow 分离认证连接失败和凭据失败,保留 ClassicSession 供 retry_login 复用,并以 auth_connect_bridge_test.gd 固定修复前失败/修复后通过证据;本轮 auth-peer-close-bridge-owner 按 CAccountConnector::OnRemoteDisconnect -> Offline 让认证 peer close 不再伪造 login_failed/connect_failed,并以 auth_peer_close_bridge_test.gd 固定旧/新认证 transport 的回归证据。ClassicStream 的 TCP connect deadline、半包、本地 loopback、文本编码回归、真实成功包序、真实失败回调、DirectEnter CG_PLAYER_SELECT 到 PHASE_GAME、真实 GC_WARP 成功 round trip、auth-peer-close owner、auth-peer-close bridge、登录 UI owner、PHASE_CLOSE 旧 world owner、跨阶段 LoginPhase owner、显式 PHASE_CLOSE transport-owner 分离、空角色本地 SELECT owner、auth handshake owner 隔离、认证 PHASE_CLOSE 静默 owner、Loading unknown-header owner、HybridCrypt guard、Loading PHASE_CLOSE leave owner、warp connect failure retry owner、空重连 SELECT owner、pre-game LoginPhase owner、本轮 peer-close LoginPhase owner、login-form-retry-owner、game-connect-immediate-failure-owner 和 auth-connect-bridge-owner 已有本地证据;空角色全空 live fixture、服务器主动 PHASE_CLOSE live 包序、认证其它完整副作用、跨阶段完整 UI retry/真实重复进入包序、GC_WARP 失败回退包序、真实 Windows LoginWindow.OnConnectFailure 和可选安全 runtime 仍使合同保持 PARTIAL。", "fingerprints": { - "reference": "79759dc3fcc9e1a4d9b779e53484b1d3abfb2b822e26d1c45d7364e9dba5f7a3", + "reference": "d5551c625ebdda0a4887cf2f95704d9220b8affb72a63c868b476825a63c7652", "implementation": "be1421c9c311c980829da8db8edf39ca930661fca905813b29549598bd457c17", - "tests": "d6d994c08fabd5100f5d9f84e32948ff16aaea4ec2f7b4b8407ff7f2136c9b17" + "tests": "e4f822302653d466b2156eadfb3894fd70f1de6091126e6a808cd659c8d256d5" } }, { @@ -282,8 +289,8 @@ "输入事件到移动请求、停止请求和网络包的完整时间线" ], "fingerprints": { - "reference": "20c74407914aba18dde8f76f7b86917372a56cc7333b2ca8bfeee76c39e357e4", - "implementation": "ce5cc765134cfa04f7cac39ca268cd7bec773f72ea17b3b529f276fb7226b580", + "reference": "9aa911197f1b86861f389d86ac2ab66e7b60d6f88a5c672e7378d8018ea9aa4d", + "implementation": "e6088c82bc450c85466f9f2527f6a0c8e597d086ef88b90f81a87205199150ca", "tests": "2c79ef6e727717527f54e611629ce15154cb78c785b37ada4970d1156caf1bfd" } }, @@ -391,9 +398,9 @@ ], "notes": "本轮已按 40250 SendClickItemPacket/GetCloseItem/__OnPressItem 修复 party/anti-flag 所有权、DISTANCE_APPROX/300cm 选择、最高 VID tie-break、骑马固定边界和 150cm 点击入口距离门;并修复 RecvCreateFlyPacket 在本地主角 EntityStore 行暂缺时通过 GetMainInstancePtr 语义回退 _local_node,同时要求远端起点/终点实例存在后才创建 indexed fly。相关回归通过;真实服务端死亡包序、重复/乱序/重连/拾取竞争、死亡资源生命周期和跨图清理仍保持 PARTIAL。", "fingerprints": { - "reference": "6a3545f5d1f424e1404c3dd6a66f49dfa28a105b639fce5f8009860fa50928b6", - "implementation": "2fd306a9f4b85b78fd75fbffb3512cf1939fed11a3ac11a29f5808d890936565", - "tests": "f066c45415ecd31e61da172c633c1fd7d29f89a8d6a8c5a7a13ca2f47b946af8" + "reference": "153c730059c1f648861675a4a6c20d10d473da4c9bf5636fea5a251a1f230536", + "implementation": "e04d0454342c612e96cc9fec72b2d86b55a698d805a78b52a0b75716d63d37a3", + "tests": "bc8e463dd84921b8bdadc7e3b6d2104ea4b7b5955ae57f37428406261088a127" } }, { @@ -491,9 +498,9 @@ "逐技能 MSA 事件到 MSE 的挂点、颜色、混合、飞行/特殊攻击和服务端结果语义" ], "fingerprints": { - "reference": "e390db94ec7e4c5d0d6518beff0319fe512af840ebe31d07d6136d48dd21687e", - "implementation": "b46796eab17505be66bb99b605f082d781fbda00b06bde98297dd9cd6faea565", - "tests": "ec3c2c7f212a4d0af4da04590dd61ebefc8e200258877c9cdf45195f8913b107" + "reference": "651b0e0b0757f5fd8b69ba48615bae16eec646d769369014a25450dbc0526a71", + "implementation": "b74becb9be86246df1972767caabf4f4539f0d55b4e4f17d72393665b9bc68d6", + "tests": "7943c3f2372e06b7c1f6d34e8f7ad27b7d7e475236685bd41ea08216cc6a5c2a" } }, { @@ -575,7 +582,7 @@ "重复打开关闭、切换目标、空装备和远程更新测试" ], "fingerprints": { - "reference": "7dd24059f8bdd75e76798702c0b2e951452232ab65ce20eb91ad155312087026", + "reference": "b0e387fadfd0c1146d0047af68152bb019b5538ce7acc5e50b1e0f476aae14c6", "implementation": "db411e1ce47a22608c1d3dbab7ee77105ec68298f9a159611ec66c3bc6ce7fc2", "tests": "5de299be840bc0d46f5eea3acd55af332dd3e1c71472e9ae428e7762bc7f72b8" } @@ -698,8 +705,8 @@ ], "notes": "本轮完成启动、阶段、Loading、DEAD/CLOSE、焦点/后台、旧 login.gd 入口和销毁调用链审计;按 40250 修正 DeathUI 的 PHASE_DEAD 错误依赖、classic Loading 旧 world 清理、两条入口的 close_requested 消费和幂等退出,以及 _goto_game 重复夺取网络 pump。完整清理 owner、CLOSE 状态语义、跨节点阶段时序、旧入口和销毁顺序仍使合同保持 PARTIAL。", "fingerprints": { - "reference": "7538cd86aa2937e6545ddf2d16bd75eba40cefcd4c9a2aa87b8e367906bfb3a5", - "implementation": "0845a372bd9f71cf3b2537dd537b85ff7c18c6af6a0efe81c67c0c646e1b15ca", + "reference": "a85227931ded79ea5c9e92eaf647a076c9abbabd794ca12ad30e918259af24f4", + "implementation": "976df522985481aa237c980165c59d0fda2e926a3a4af51c8558defcdcf5db0e", "tests": "e865f9782600d3d2027db84beb10913a22a22c751938b707768e7c838d894a1a" } }, @@ -809,7 +816,7 @@ ], "notes": "按 40250 固定槽位、帝国写入时点、未知改名 PID、DirectEnter SelectPhase 分支、角色 CRUD 阶段门禁和中文 GB2312 fixed-name bytes 修复 classic 实现;native 角色/网络回归、角色 UI、选人和生命周期回归通过,非中文 locale、异常双窗口、重复进入、角色实例管理器和真实包序仍使合同保持 PARTIAL。", "fingerprints": { - "reference": "19428229f676c622ba55bf9b59d788730fc0751b9cf10ecf3006fe5b88df66e4", + "reference": "c22be1a3fb1e435df139e05df8650840d229d36509af0e58c78ee39fcd189a3b", "implementation": "9b8136a0062b3268f0bda3d9f13e52e3a6a5aa2d105c22fb16cd38afb56e5c18", "tests": "f56863d12c0cb2b8dcf535c2db8cab9b432d2dadd22f2bf133f0ec0630d7dafd" } @@ -897,9 +904,9 @@ ], "notes": "已按 40250 固定头+文本尾语义修复 GC_WHISPER;ClassicSession 已收口 Login/Select/DirectEnter/Loading/Game 的阶段门禁,且由 audit_phase_dispatch.py 证明四阶段 payload case 按数值一致;Loading 的 ITEM_SET2 通过 GamePhase fallback,HybridCrypt 由 ClassicStream 消费后返回,phase/握手/初始密钥协商后 return,KEY_AGREEMENT_COMPLETED 会解密并继续派发同一缓冲区的密文,错误阶段包在当前帧消费后清空剩余 buffer;ClassicStream 已加入 Game 阶段小 burst 的 MAX_RECV_COUNT 边界、已知 handler 失败后的 RecvErrorPacket 清缓冲/owner 保留、CheckPacket 前导零语义,并补齐旧版 85 字节主角色包。按实际非 GAIDEN 条件,主游戏注册表数值/类别已覆盖 117/117,移除 6 个未注册普通流兼容包头,guild-mark 的 SYMBOL_DATA 保留为独立适配;动态 parser 对 SCRIPT、QUEST_INFO、MESSENGER、SHOP、GUILD、DUNGEON、NPC_POSITION、LAND_LIST、CHAT 的截断字段改为失败并复用该清理路径。wire/stream/session/encstream/phase-matrix 回归通过;逐项 sizeof/byte layout、8192 字节阈值、大量 burst 和 QUEST_INFO/GUILD 特殊边界仍使合同保持 PARTIAL。", "fingerprints": { - "reference": "70235b4c609e89c03f033249fcd53dd4e775f579f4fae8c9cc86cc1dad3a77d3", + "reference": "12dba8cb33b427a4b226415919450d863c4f768bf419732be25e9b4a75e1c3e3", "implementation": "0be8e78eb0a0f6da7e1af10a3aa77fa43e93915fefc1bd7fc60c117174a57404", - "tests": "b45313ce3f4ab491face17e762d6715ca8d2d802175eb28c37ab138be844ed46" + "tests": "b69b109040b958aaa09d1f1aea549884b286f0e4d04acd4574c8b322169be8e9" } }, { @@ -1007,7 +1014,7 @@ ], "notes": "已按 40250 handshake/handshake_ok 修复后续 server clock 二次校准,并通过 sequence table 32768 项逐字节核对;DH2/CTR fixture、全局 clock owner、legacy/optional security 分支、Windows codepage 和完整 bSeq 矩阵仍使合同保持 PARTIAL。", "fingerprints": { - "reference": "40701de0c80d0226ca751b34ef81a6f96f2cb005f8012a5c1f7ed5148ec5e4d6", + "reference": "dc1fd5bb77c7730498d75a566afca07f34a91771c4ebaf5983aff83ee1e6ad4a", "implementation": "b1a5d95818f5d0e4e21da62f9262cbd560f452ff42a9b0a2db5ac0e4120d5f18", "tests": "f6a16e75e4a98ee53f2757dd3ecc05b715cc353ee98c40365e624f7e9386039d" } @@ -1092,8 +1099,8 @@ ], "notes": "已按 40250 同一 CNetworkStream Clear->Connect 语义修复 classic generic reconnect 的 session/selected slot/login key 保留,加入 3000ms 连接 deadline、连接中幂等 guard,并修复多 warp cue 在第一个重连 return 后被整批清空的问题;显式 disconnect 现在先 reset 活动 EntityStore、发 world_reset,再释放 transport,并有独立回归。真实阻塞连接、旧新 socket 竞态、包序和清理 owner 时序仍使合同保持 PARTIAL。", "fingerprints": { - "reference": "3797fce63b45cc47556f83c588699d6bf04000ff1bf821fa3b568b20006fac6c", - "implementation": "de467a517c36ed61f0fd0bb94a6f2df85644d57690c50e198ec6c9e5c4e8ddd0", + "reference": "3fc038ea59c95ea4d3038fe2737d8254966212f41125b8657728aa085a308404", + "implementation": "63b26bc8ff3ed0e51d2ecb3475ef5c081eff35df39274660682693f92523ca15", "tests": "9dfa17b7705966830888646381d7abe70653a2b381383144489c2f0d456899e5" } }, @@ -1127,10 +1134,12 @@ "tests": [ "project/app_flow_lifecycle_test.gd", "project/channel_status_test.gd", - "MT_PROTOCOL=classic godot --headless --path project --script channel_status_test.gd", "extension/tests/net_loopback_test.cpp" ], - "last_test_result": "PARTIAL" + "last_test_result": "PARTIAL", + "commands": [ + "MT_PROTOCOL=classic godot --headless --path project --script channel_status_test.gd" + ] }, "remaining": [ "补齐 auth failure、remote close 和 channel status 完整矩阵", @@ -1139,9 +1148,9 @@ ], "notes": "本轮按 40250 修复认证密码生命周期、两套 transport 的连接 deadline/可写完成判定,以及 ChannelStatus 旧 probe 与重入 callback 清理;频道检测仍保留当前端逐频道 TCP fallback,认证失败/远端关闭/完整恢复与边界矩阵仍未闭合,合同保持 PARTIAL。", "fingerprints": { - "reference": "cbfb26c28228d50ea6af675e69f7766d06a6292574bd913e5db5d56979bedd4e", + "reference": "9cfeebabb54d8e929d21b8c0aaa8d2bd99d590a2cb3c4c78d4a6e1b87134bebe", "implementation": "c203e25d94a95cf8c342692525a4f8a2bf8d5a1885f209b52e4e884be291bf4d", - "tests": "0977a59547cbe394ab546237bd1ababe4e3069d946487b91ede25ea3e4dc5778" + "tests": "00827ebf36e6201623153a716a44e6e0c7cf7d8a01c0383fc1b874b23e35e3e6" } }, { @@ -1247,8 +1256,8 @@ ], "notes": "本轮补充了同 VID 普通重建/上下马位置保留、淡出期间同 VID 立即重建、单槽 pending、逐帧可见性、主角 MainSet 通知次数,以及 classic/generic Add/Add2/AdditionalInfo/Update 的完整字段、隐身过滤、unknown VID 和事件粒度回归;net_entity_test、net_classic_session_test、net_world_respawn_test 与 net_world_vis_test 通过,Delete/map reset 后迟到包、旧 dead-list/tween 跨帧并存、主 VID 清理和 Godot 渲染副作用顺序仍未闭合,当前不能视为 40250 等价。", "fingerprints": { - "reference": "e59399f499670d966befae34478d84bb3ae4699da942c4707c1bc059c84c4788", - "implementation": "80a2daf3ea649b43f2e882068103c04774fdd4bec021fca67f6a1b0d15f85b93", + "reference": "4960a363710b99cbed12a559744231a7a7d7da56b703c34af07685bcdb8511d2", + "implementation": "f48330ecd0917ecc0dbfd99db1886ba3a4641c7ffb18e5a349c933f738d6c0b7", "tests": "720b1c6a90f62614e9867f51a9dd039b695c4141d2f8d568e9298637620a1779" } }, @@ -1350,8 +1359,8 @@ ], "notes": "本轮补充了 LoadMap/UnloadMap/UpdateMap 与当前 Metin2World 的 Leave/Enter、初始同步建队列、延迟回收、map-name 校验、双 world 和旧请求 generation 差异;gamescene/p9/environment 测试通过但不覆盖这些边界,合同保持 PARTIAL。", "fingerprints": { - "reference": "e985648a82b4de1f2d28872b21ffc2b885622ee94f9eb0dc03aa1d6dfc4ff3b8", - "implementation": "7c308d9ca6c888fa85f23d8d3465a6f41c6d8caae1092c0fac72271a7b9a64d7", + "reference": "cfe7d501420c271736030771877026db2c7859559dbab56d2dd82574b2785c71", + "implementation": "37188b2eb103bdc121f84f62b653ce044b17333e62791cf7e315222248f3ef0d", "tests": "67a02ec9abd6ee1ead261c0e1353115e69b34934975dbb071b2faab05c436e57" } }, @@ -1473,7 +1482,7 @@ ], "notes": "已完成 40250 actor/ground/shop-sign 调用链和当前端 parser/store/view 对照;合同保持 PARTIAL。net_world_vis_test 复现 4 项可见性生命周期失败,本轮仅登记审计结果,未修改实现。", "fingerprints": { - "reference": "3bf9532d6c9f258fbbc3dba6864171160594f94c7cd708e056c250f88be126ff", + "reference": "97dfe247272a6098ce623ceae986732205de3273cb1199377f969faeec8d0ba0", "implementation": "3a7d87cad0ba405495ba51e13feab294af9ebddbde2fb18b5df9c606bbe3af08", "tests": "9783566cc5ee2f17b89509082c9fd8d9334154f9b03fb499c1616613bba3a2ab" } @@ -1493,8 +1502,8 @@ "UserInterface/PythonEventManager.cpp", "GameLib/MapManager.cpp", "UserInterface/NetworkActorManager.cpp", - "UserInterface/FlyingObjectManager.cpp", - "EterLib/EffectManager.cpp" + "GameLib/FlyingObjectManager.cpp", + "EffectLib/EffectManager.cpp" ], "symbols": [ "CPythonNetworkStream::SetLoadingPhase", @@ -1521,7 +1530,6 @@ "extension/src/net/m2_client.cpp", "project/net_world.gd", "project/fly_object.gd", - "project/ground_items.gd", "project/bgm_director.gd" ], "symbols": [ @@ -1580,8 +1588,8 @@ ], "notes": "本轮按 SetLoadingPhase、CMapManager::UnloadMap、CSoundManager::Destroy 和 CPythonApplication::Destroy 的清理边界,新增 net_world_reset_test,修复 _upgrade_queue、_chat_tails、target/hover VID、target effect 残留,并将 GameScene 换图改为旧 world 先 unload 再创建新 world;AppFlow、GameScene、Audio 与 GDExtension 退出路径清空模型/动作/NPC/UI/音频缓存,rendering_scenario 的 139/54 泄漏和 Debug 下 2 个 AudioStream 泄漏已修复。FlyInstance handler 自捕获环也已解除,fly_reset_test 的 5 个 ObjectDB 泄漏已清零;显式 disconnect 现在先 reset 活动 EntityStore、发 world_reset 再释放 transport,test_exp_fly_parity 仍有 3 个旧 fixture 警告。真实重连包序、统一 generation 和通用 EffectManager 仍使合同保持 PARTIAL。", "fingerprints": { - "reference": "30cb04eed7ba1b82165b30583723a2aaf89f98d1a7a2e7e17b327bb16be10605", - "implementation": "260e6182daa7f44706bebb081eea849b0d4d58c57e5cea129c2920d715c47743", + "reference": "a992eac9ed117a1da3e0ae27324208a08be18fe1f9ce6855cfacdccf692c4c47", + "implementation": "191f1b1b5a9dd817e90204e3bf16d6305bc2b3c13096c2bf06348fdc3fb831cb", "tests": "d3cc153e1d36bb4f390b88efc623e3cd56d1703626d7b9f325f9ca3c457adf1d" } }, @@ -1682,9 +1690,9 @@ ], "notes": "本轮补充核对了 Smart press/hold/release、click miss 的停止语义、运行时距离门、真实 MSM 碰撞球、第二碰撞 BlockMovement 和对象 picking 的差异;四组移动/点选回归均通过,但 click/target/fly 测试有 3 个 ObjectDB 泄漏和 1 个资源退出警告。当前仍不能用固定半径替代真实 MSM sphere,合同保持 PARTIAL。", "fingerprints": { - "reference": "15850029efadc3a50e52511fb45f51da5ad77e6ce0841a41a53d66fd6306d03d", - "implementation": "d2e8fc11d36e5f898a4aaea356ce4bee03d1d43b7dea7101c5c610a74adefd76", - "tests": "cdee05a904f96b260c201553915af522bd3623eed78ffb5b23d16ae8e0b2a88a" + "reference": "5f27304415d1e0539673c738ed20ad44995be7e4787f87d2b2edd5232b7cb73b", + "implementation": "546223927ade40b886e896af686ffb102e8fb827df95a8e748900d576858fbf4", + "tests": "091582b21c6be94f4923de7a58e6ea55c614808bfb643622ee4651c5dd465861" } }, { @@ -1770,9 +1778,9 @@ ], "notes": "主队列和时钟链已逐分支审计;远端 FUNC_MOVE 已按 40250 统一 PC/NPC/monster 的穿越 Dst 分支,TEMP_Push 已补 correction serial、100 点碰撞探测、1 秒表现缓动和大位移击倒门。因技能数据权威来源、双人情绪目标、真实资源/地图碰撞和服务器时序差异保持 PARTIAL。", "fingerprints": { - "reference": "2dc90fd41d26f4a61085fdd57d961ffd8c83f5b1ef4d359559c3104dcbabe801", - "implementation": "99d49c732d9d141818db12ef3b2f98f081980fac372d34692fdb50ff775d9dca", - "tests": "f3fdfa1a6d0ebed1c8fb78588e44d92fb79ff6b1059d77453d9e7cd5f962e15c" + "reference": "6a1609a0a2ae34ef4725e7e274b37881638b6b0757e0759e08cbaed9eb65602b", + "implementation": "b0c049af7c1475a521a38f18764973e9f0148d3b03511cb0cd86eb71707f7b50", + "tests": "9ec8668cc4acb5790f16b6140e5b5639196c153063631b08b36745f64397fcd2" } }, { @@ -1900,8 +1908,8 @@ ], "notes": "已完成 40250 RaceData/RaceMotionData/ActorInstanceMotion/ActorInstanceMotionEvent 与当前 MSA/MSM/AnimPlayer/视图/NetPlay 对照;本轮接入 playersettingmodule 驱动的 PC motion registry、复刻注册权重和 HORSE 父模式查找,并修正击倒/起身受击早退;MSA MotionDuration、LoopData 与热切换缓存回归仍通过,合同保持 PARTIAL。", "fingerprints": { - "reference": "f59276d75e5cdf0fc54c95fdce5ca85c794080b94ba7fdee540870273004a471", - "implementation": "01ca0b13b2db5489c1fa37dc48d555d68da096dc2d2370d9d64b2cf78c8f9fc7", + "reference": "d344ff65fa4c8a72a2c268a16fb268e6526121fa05e2d4359b7c40df4eb75af4", + "implementation": "87eac65e93353d15b18a912d63093ecd920b3ba97c722925f69c7fd4c87d122c", "tests": "5815d9275c50109a98b6f21175c4ab274a0eb7cd4b4849dcf4b973a0b4c395d6" } }, @@ -1960,8 +1968,8 @@ ], "notes": "已按 40250 isValidAttacking/HitDataContainer 删除缺失动作攻击数据、空命中窗和缺少 .msm 防御球时的本地合成命中;状态包、命中窗、去重、受击、弓箭 FLY、本地主角伤害飘字和 WeaponTrace 多部件生命周期有回归证据。WeaponTrace 目前通过 weapon_trace.gd 适配层接入,包含武器类型过滤、每个 Weapon/Shield 部件独立 trace、双轨迹时间队列、cubic spline triangle strip、alpha/texture 材质、攻击动作开关、纹理控制入口、bound-box 最大长度/局部 Z/reach scale 和换装清理;AFFECT_GEOMGYEONG 的 1.5/1.0 reach scale 已同时接入刀光与普通攻击扫掠。native 骨骼矩阵坐标语义、远端/怪物覆盖、真实视觉资源及服务端伤害闭环仍不等价。强化配置中的 has_trail 仍是独立未消费字段。", "fingerprints": { - "reference": "3b79deef2e50cb16c6b7226e8900e4d96c82f5dbe4e409f40609b6a656586608", - "implementation": "fa633ddfb631df0d1b472ad674d558869399079d9789b54b6c6775e45b8310ac", + "reference": "abf0577e3430979fbf4e1ea85fa637b2f59e798bf57e257060528940045eef43", + "implementation": "ece1d3d9f63314730c70f00d2b3c8eb305895eb1d7896eeac0572eab6865580c", "tests": "228e1bb5ebb0ef52822956e3d33d9c51d55cc2090c323300b466f937249e5d3c" } }, @@ -2062,9 +2070,9 @@ ], "notes": "已完成目标攻击资格、目标可选中、目标框和安全区的逐分支静态审计;目标入口已统一接入 CanChangeTarget/CanPickInstance、重复目标窗口和 CG_TARGET(0) 清理,合同保持 PARTIAL。", "fingerprints": { - "reference": "818e201fffe6335de4b2c79b505d8749b7ef00d61267865861a8fefc21c5bdf5", - "implementation": "07fcaf36da0b7322ac63c5d2b3ec1001b6fff2c1810dd637c54b3fecfba430fb", - "tests": "9ad10ac749fbc12f4537fe489f592ac8e2159446c6bb1ab01855076c3b6c39ec" + "reference": "d1c911b8bba1ccd48228a794568e3bc13ffa77ecd7d124d4e442ed9fecfdfb85", + "implementation": "a22b2bf8afb7f6e30b724ab05e39015872b566316e4b3ef94e15ffce7655e878", + "tests": "2cf38a10e34184c9477f0e7fc7316b6ba201c0adaa4db43a0ce8cb7d6936c1a4" } }, { @@ -2167,8 +2175,8 @@ ], "notes": "已完成飞行物数据、目标、弹道、背景碰撞、命中/穿透、网络目标追加和地图清理的逐分支静态审计;本轮按 PythonEffectModule/playersettingmodule 接入 .msf 解析、索引资源映射并修正 FLY 枚举,同时解除 FlyInstance handler 自捕获环,fly_reset_test 退出已无 ObjectDB 泄漏。动态 Actor 碰撞、动作事件 FLY 起点/owner/skill、test_exp_fly_parity 旧 fixture 清理和完整特效生命周期仍使合同保持 PARTIAL。", "fingerprints": { - "reference": "78aa9c45552beea0f552c072bdf663049fb14d1e7ca8bd7c916bdfda7f6d8339", - "implementation": "c9ef1ee8219d239c2b1abe821e8fac5a4d562ba407cabfb4054135a8c768ab7c", + "reference": "dcbded9cf3a69f372d4d507d3b8cced19a0d89bfe4dde01cb46a9d10211398b7", + "implementation": "5d0e20950b759b6ffeeae8cd3010bf40c7c6f59c3401b1beed973ab7c70e49c0", "tests": "b6106f8af47ea86f4c289e56455da78642f2ee35e074dbd073a640171e55aec8" } }, @@ -2274,8 +2282,8 @@ ], "notes": "已完成 40250 AffectFlagContainer、Actor affect/受击动作、damage/affect 协议和当前 native/GDScript 链的逐分支审计;确认本地状态结算器的数据权威错误,且 hit_view_test 有 1 项失败。合同保持 PARTIAL,本轮仅登记审计结果,未修改实现。", "fingerprints": { - "reference": "a17de5fbf004c1c6b799300ee026c44250c7be01839e79f4e2df927c65748301", - "implementation": "c0d2d09d88eddafb7e3586b6a5894e95d3d143182f486f7572c790b9c031bd4f", + "reference": "92ddbb559ef4966bcecc9358beb85f7fd8edec830f833cde7067f0c35eaa2aee", + "implementation": "030d5168f47849b432cf9d266f14e2b581c15ec14a0b0ded1d077a60ec748b59", "tests": "c8c6a05be637f15e1ca812a4a59cd5f3be900104ce325e729de3690beb594d3b" } }, @@ -2385,9 +2393,9 @@ ], "notes": "已完成 40250 技能数据、三层施放校验、CanUseSkill、casting-speed 冷却公式、快捷栏、普通/公会技能发送入口、skill/group/cooldown 协议和 FUNC_SKILL→CG_USE_SKILL 动作/意图包序的逐分支审计;CanUseSkill 五类动作门、POINT_CASTING_SPEED 桥接和公会技独立路径已实现,但真实拒绝/乱序/跨生命周期 fixture、完整动作资源包序和服务端 damage 顺序仍使合同保持 PARTIAL。", "fingerprints": { - "reference": "41123a16a7c43e570f57854c4165bc04bb2dfb875efb0bf34d9f5e6afd08e383", - "implementation": "bad2f21e5c0ea4de37508d94ed1724931696d9409db5f487a708da0bdfbd3cc7", - "tests": "0746ef57d85bea05245ba44d178e24790d28623c40559520508db7acbd7970f0" + "reference": "c38e930585edcd35a8e0911f10f3fb6694a39bc7735a3bac0b3d4eea157c9bcb", + "implementation": "e29ce4904ebb11d2f78d3e0693ebac2744feba8f7a303cc89828f55e2064afa0", + "tests": "150fa0554dbc9f55008d313a3df773859311240e8122104d18d1ec6c62534903" } }, { @@ -2515,7 +2523,7 @@ ], "notes": "本轮回归确认非法 cell/空槽 Update/native 行动门/装备分类/Set2 highlight/未知 vnum 校验和 item_proto 音效分类未回归;完整 sequence、旧监听者中间快照、空槽字段 API、扩展槽位乱序、服务器拒绝回滚和断线回滚仍保持 PARTIAL。view_equipment_ui_test 与 netbridge_test 通过但各有 5 个 ObjectDB 泄漏。", "fingerprints": { - "reference": "1278f3e04f31a69efd71c1d5542e3d697e279575675031bdf96d30aaa0d1ff75", + "reference": "319ef546037833afca533d60488f0f5e9618e9da26bd67699ad16cefa413986a", "implementation": "b8bc1a86bd1781601e4faa66724fe3674bb5b6658d82f6e8c8b57bbae83b261e", "tests": "b486458e209c415e1373ae96e83a6be7c558aca8afb99822baba350c89fc24be" } @@ -2641,9 +2649,9 @@ ], "notes": "已将拆分、快捷出售和快捷栏增删交换统一到 request→server/GC 确认路径,并修复购买模式误发出售请求;快捷栏使用、消耗品真实反馈包序、音效/sequence 和断线清理仍使合同保持 PARTIAL。", "fingerprints": { - "reference": "46e51b7a21012ae531c21f8a752fde5592659081feec6d67edfa86c19f7c2082", - "implementation": "0139c7d3d92f8d812eaef51d3d8dfb70f0143f0b4660fa21cbca4c2f16284bfd", - "tests": "bc8250918b7945d30b8174a2206ad3f95f9f9fe6523217e312e9090317a03f0c" + "reference": "f364d4bbba79ef514f5f8aeccbf34a3d6adea8fcacf4cb82664476b6c29ebbfe", + "implementation": "dc91b77439557e03da1bdbbff52425940448bf3b9a6a53a9f3e17a3f8755665c", + "tests": "5fe55c9e41140a0d3deb270df8cecb785d45d8eed3e6769d5a24b907d772e66c" } }, { @@ -2753,8 +2761,8 @@ ], "notes": "本轮完成掉落/拾取/所有权/名条/协议逐分支审计;基础回归通过但资源、权限和反馈链仍不等价,合同保持 PARTIAL。", "fingerprints": { - "reference": "18e94abecd2cbd63407ec94fe4f2b692505b0ea156e3149fcfa426920a10856b", - "implementation": "664112e5403d65b499075c104656efec9e2920da2f5dca0edec66339e9005b66", + "reference": "d63b259e06f98b6ebb425576e8d428da4bf96bae3c297cea11f2cb5606132dfd", + "implementation": "06f6b263ed84d7a2c5833a54986ae30b9bf06486ff27f79d2b36fbe274403e63", "tests": "0713f0aeef0e53fdf447537c2a8e785559269b45ca63825c4b507382fbc95019" } }, @@ -2883,7 +2891,7 @@ ], "notes": "本轮已修复 M2Client::refine 对 40250 取消 sentinel(255,255) 的错误拦截,并补充边界/EntityStore 回归;属性、socket、时装和幻化仍绕过服务器权威回包,合同保持 PARTIAL。", "fingerprints": { - "reference": "e76ca9bb01f7b7838943820f3fe5633f68a32364c3f3ece48cbc66bf74a6a247", + "reference": "6cc67309be64986008984215add763db78e2990b665274500cf4e76661b782e8", "implementation": "176ad923087d84b8f11dee97d2bd198c495d91014d8b720d3bf58acd0bd26fb8", "tests": "6bb13ce9e81271dfa30eaaab03c5cf5e801ef22965c32d4b4d38af95f6874bfb" } @@ -3000,8 +3008,8 @@ ], "notes": "已完成 40250 与当前端的 NPC 点击、脚本对话、普通/多货架商店、买卖、个人商店招牌和摆摊提交逐分支/协议/状态审计。协议骨架和本地 UI 测试存在,但多货架更新、个人商店点击/状态、动作门控及本地直改旁路使合同保持 PARTIAL。", "fingerprints": { - "reference": "75f3901cc3c0f8cdb60bb5d5c5c04e695776ce365d9988ce83e4ea64f24e4dee", - "implementation": "3396b617791fddb530e44180457248d2222fd3e971a1bd2b08503486de73213d", + "reference": "2fa18ff8979d5c03e2daf44129702e3a1b26bb9e6f7b3b585f6fa55ac898e765", + "implementation": "f68ffad1dc2b2f069ea0d0566689bde075f85e8728953d7fda49ca0340857bd1", "tests": "3829ff07408c019b3a4a65fa8d4f283fe723342bcc2d1ed51ec4993d7f9c1874" } }, @@ -3019,7 +3027,6 @@ "UserInterface/PythonTextTail.h", "UserInterface/InstanceBaseEffect.cpp", "UserInterface/PythonNetworkStreamPhaseGame.cpp", - "UserInterface/PythonNetworkStreamPhaseGame.h", "UserInterface/Packet.h", "../../Client/Eternexus/root/game.py", "../../Client/Eternexus/root/uiwhisper.py", @@ -3119,7 +3126,7 @@ ], "notes": "已完成 40250 与当前端的公共聊天、私聊、忽略、聊天气泡、名字/称号/等级/公会名尾标及文本布局逐分支/协议/状态审计。已修正 whisper mode 映射、截断 GC_WHISPER 拒绝逻辑,并将 live ChatUI 的普通/GM whisper 接入独立会话和 ignore 判断;基础网络与静态尾标测试存在,但窗口通知、公共提示/过滤/表情/颜色和 Godot 字体布局差异使合同保持 PARTIAL。", "fingerprints": { - "reference": "82802154cb44736cfa53a3dc2de5879c1fb9ccb4f47037b70827a2ba8262c9f8", + "reference": "2b70c64e18344d4f465354239899289d5df2f7b9c38e165c66d553f790e2859e", "implementation": "cd02d44596b377534a4200e0ef7002b3c6de8e7c328fbdd96afc7e9ed8a6cb8f", "tests": "c86122ea9b1448a74f4af2a90bc25382ae7b477d6a2cc96b27c5ac3b48d97e39" } @@ -3187,7 +3194,6 @@ "extension/src/net/classic/classic_session.cpp", "extension/src/net/classic/classic_session.h", "extension/src/net/classic/classic_parser.cpp", - "extension/src/net/classic/classic_mark_client.cpp", "extension/src/net/classic/classic_mark_client.h", "extension/src/net/classic/wire_classic.h" ], @@ -3262,8 +3268,8 @@ ], "notes": "已完成 40250 与当前端的组队、公会、好友/Messenger、会徽/战旗及相关 UI/native 回包链审计。live native 主路径较完整,但本地队伍/技能/公会战/Messenger/会徽 helper 仍存在直接状态旁路,真实权限拒绝、mark 独立连接和重连清理证据不足,合同保持 PARTIAL。", "fingerprints": { - "reference": "6223e48f6be2d1471133c76658c264f2eaf5fcef7e2870ee21f538ff81e0d0ec", - "implementation": "df7b3599aa8255d8134668158fda22e2914854e30b763adc1deceb9c345a6873", + "reference": "c0ba3dce2da7c2bd6c1f934ebd66eedaf0d37bc0d3bf99b22e401b939a150bc4", + "implementation": "bf62ad6be63031d6630d7f4d1be497469f4a42ea8137be95b066864cd566f55c", "tests": "89f4d155ee7d3b4f3457ef2114aed642ef6030a7a7c226d5731fcc20cd2ba2dd" } }, @@ -3279,9 +3285,9 @@ "UserInterface/PythonSafeBox.cpp", "UserInterface/PythonNetworkStreamPhaseGame.cpp", "UserInterface/PythonNetworkStreamPhaseGameItem.cpp", - "Client/Eternexus/root/uiexchange.py", - "Client/Eternexus/root/uisafebox.py", - "Client/Eternexus/root/game.py" + "../../Client/Eternexus/root/uiexchange.py", + "../../Client/Eternexus/root/uisafebox.py", + "../../Client/Eternexus/root/game.py" ], "symbols": [ "CPythonNetworkStream::RecvExchangePacket", @@ -3360,7 +3366,7 @@ ], "notes": "已完成交易、仓库、安全箱、商城和金币确认的逐分支/协议/状态审计;native 主链存在但与 40250 仍是 PARTIAL,本轮未修改实现。", "fingerprints": { - "reference": "32d03a88d33723e087656bdfcbe806507b2e88e5ef20e36211d9f2cbab26a745", + "reference": "8497886941be7e41b6b1960b22fc8e07b1c0fe65c4a032b13d662191b06a0404", "implementation": "d2cad2b8bdf7fc421478157ea0085bef07a4979d3d3799ff81823e1dd16216ab", "tests": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } @@ -3379,11 +3385,11 @@ "UserInterface/PythonNetworkStreamPhaseGame.cpp", "UserInterface/PythonNetworkStreamEvent.cpp", "UserInterface/PythonMiniMap.cpp", - "Client/Eternexus/root/uiquest.py", - "Client/Eternexus/root/interfacemodule.py", - "Client/Eternexus/root/uicharacter.py", - "Client/Eternexus/root/uiminimap.py", - "Client/Eternexus/root/atlasinfo.txt" + "../../Client/Eternexus/root/uiquest.py", + "../../Client/Eternexus/root/interfacemodule.py", + "../../Client/Eternexus/root/uicharacter.py", + "../../Client/Eternexus/root/uiminimap.py", + "../../Client/Eternexus/root/atlasinfo.txt" ], "symbols": [ "CPythonNetworkStream::RecvQuestInfoPacket", @@ -3475,7 +3481,7 @@ ], "notes": "已完成任务状态、事件脚本、对话、HUD、地图导航和过场链的逐分支/协议/时序审计;GC_QUEST_INFO END、script-button 任务索引 sentinel、DONE close-only/DoneEvent、ESC/CANCEL answer 分支和 RUN_CINEMA 缺失资源分支已按 40250 修复,其他明确差异继续保持 PARTIAL。", "fingerprints": { - "reference": "ba46c9195d60a257062b207f81d4fbc730b71d8098d6b2a2a531640f05debe38", + "reference": "e05974ea414ca0d1bdcec3298119380ec35bfd20637a9c8e48588246347fc364", "implementation": "5aa1707764f5a74db510210bfd8803a9e90e0a4d989202795fe506d1f29a8395", "tests": "a65c31ca4ade9fe4359f1238ed7080a5a50c3a336b75a9d9ee2a1e2f7faeb02a" } @@ -3492,7 +3498,6 @@ "GameLib/GameEventManager.cpp", "GameLib/GameEventManager.h", "UserInterface/Packet.h", - "UserInterface/PythonNetworkStreamPhaseGame.h", "UserInterface/PythonNetworkStreamPhaseGame.cpp", "UserInterface/PythonPlayer.cpp", "UserInterface/PythonPlayer.h", @@ -3560,7 +3565,7 @@ ], "notes": "已完成 GC_DUNGEON 目标点、结果窗口、地图事件及本地 Boss/排行/副本 helper 的逐分支、协议、资源和时序审计;目标状态算法接近参考,但 compass 副作用未接通,本地 authority 不能视为 40250 客户端实现。", "fingerprints": { - "reference": "45e83143617b16eda5b4ad7cc38e701fa9590b02a4c36d5adb2dd8a3bae77b7f", + "reference": "66b5e63a273e45873bf4c4b90c8b658a9f073ba425de6f275d622b3c47fbac99", "implementation": "c4885c44a33a107d3095a20b988f26a5a62d50814514e34ffb03c86bfa7a745d", "tests": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } @@ -3654,8 +3659,8 @@ ], "notes": "已完成钓鱼输入/水面扫描、GC_FISHING、GC_DIG_MOTION、地图水面和当前端生活技能 helper 的逐分支、协议、状态与资源审计;协议骨架和部分算法接近参考,但 live 动作/门控/清理证据不足,本地收益脚本不是 40250 客户端 authority。", "fingerprints": { - "reference": "bbc26abb3d3a2cdca5abae515be1dbd0a83188cdf44c5e55738e2a3b664a0ee4", - "implementation": "c72913ee472ed61dfa156b67ba7468f8098512972e49c4a1e307aa8740152fca", + "reference": "770f14f2d2bfdf025b471dbc90280f891671ea898faef52d4112684934f5bdb7", + "implementation": "998dfc8e3cb21d6edbe3884bd50f15b0185138ab17055d0fcc478170d50f320c", "tests": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } }, @@ -3745,8 +3750,8 @@ ], "notes": "已完成 mount_vnum/GC_MOUNT、骑乘动作/攻击门、变身和当前端宠物/马匹 helper 的逐分支、协议、资源和时序审计;网络字段与部分门控存在,但实际坐骑模型挂接和变身 native 闭环缺失,本地宠物/成长/技能规则不是 40250 客户端 authority。", "fingerprints": { - "reference": "deefe3ad38347b0ab207653391cd54b5f06811f6892ee15a8650cc42e8637e98", - "implementation": "333ea5a5c89e7628044ea4b548354e73d27e602b6348eb46cedd24f8a85d7568", + "reference": "5ef243a1ff134dfc8afff02136d104daf0a0ac12eca781a1c476b53f7da9ff19", + "implementation": "0ef7dbf779fa54aee0ea1bbb46566a65d9bb29c0fece5eb40db2bd8013218dfe", "tests": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" } }, @@ -3772,8 +3777,7 @@ "GameLib/RaceData.cpp", "GameLib/RaceData.h", "GameLib/ItemData.cpp", - "UserInterface/InstanceBase.cpp", - "UserInterface/InstanceBaseRender.cpp" + "UserInterface/InstanceBase.cpp" ], "symbols": [] }, @@ -3829,7 +3833,7 @@ ], "notes": "已完成 40250 ThingInstance/LODController/ModelInstance/ActorInstanceAttach/RaceData/ItemData 与当前 GR2/Godot 模型链的首轮逐分支审计;确认默认 LOD 常量、LOD 子模型生命周期和 03150 双手武器姿态存在真实差异,合同保持 PARTIAL,未修改实现。", "fingerprints": { - "reference": "553e2ce8e6b51e4acf8192cc1f7983865f52a2d68ae2d7c2f1d8aee9fa63f349", + "reference": "b99b2b4f028e0e5d107b3b1726f3985f08cf08fa46497aa14bd3046d52f4008f", "implementation": "2fedab89ead277ee74f94bda5ee2e5bdc4810fe3a2897f250e5911c0be81df04", "tests": "74ebcaa45ad2a714c654a1e76045df3594d858b3c83f265789e39cf6d8bba1e1" } @@ -3913,7 +3917,7 @@ ], "notes": "基础 MDE/MSE/粒子/挂点已覆盖,但 40250 ActorInstanceMotionEvent 的完整分支和所有 Actor 消费链仍不等价;保持 PARTIAL。", "fingerprints": { - "reference": "0f027842dc3a7991870c155a9a9453babb4c0f8fc55ac2c4c7f0bb75aaa445c8", + "reference": "49b3c3d00945f76e589ca5e99bdf4077edde0885da1a5eadc4a1d5659dfff3c9", "implementation": "1e7106b1936bf6ac79849544cc8dd473bb0a421b3666d98e99721f0f84fc4825", "tests": "cb6e83d8a0c3e1696d6638eb582a6c95f48fc3c19e3fbfb9c285ba5906f3aa52" } @@ -4014,7 +4018,7 @@ ], "notes": "已完成地形/水面/碰撞/拾取首轮逐分支审计;高度、桥面和水面 happy path 已验证,但对象 ray picking、水层高度、高半字节属性和 streaming/清理边界保持 PARTIAL。", "fingerprints": { - "reference": "46da799d71b3ad4ec459ebaa5364dac82a1cd503a30c55692ccadaf183ade1cc", + "reference": "8c4f137698ddf9d9347d11e257673bfb92c30afd72f1000174355ef051bbe698", "implementation": "0492040f92b685f42e6d553cb50bdf6e67ff22bf757a886f22b9ee230df6912b", "tests": "a2a38be2cfb31518ed7f30aab7a2aeb324b71dce353271fa4ea1f330d21195f3" } @@ -4027,8 +4031,8 @@ "status": "PARTIAL", "reference": { "files": [ - "../Client/Eternexus/root/game.py", - "../Client/Eternexus/root/servercommandparser.py", + "../../Client/Eternexus/root/game.py", + "../../Client/Eternexus/root/servercommandparser.py", "GameLib/MapOutdoorRender.cpp", "GameLib/MapOutdoorRenderHTP.cpp", "GameLib/MapOutdoorRenderSTP.cpp", @@ -4111,7 +4115,7 @@ ], "notes": "已完成 .msenv、天空/云、双方向光、fog、天气粒子、服务器 xmas_snow 命令和正式场景过滤的逐分支审计;命令链与四张排除地图已有回归,但 SnowEnvironment 池、DayMode、filter/lens flare/wind、线性 fog 仍与 40250 不一致或缺失,故保持 PARTIAL。", "fingerprints": { - "reference": "0a076b8a9779c989184c20ef617cddf5538720e8ac7e2e0773d63af3d72e6eb0", + "reference": "1cffc55eb356a522049e4f7bc36595ea968ad1e0ccd7780486064a3e250cc36b", "implementation": "dfc0fe4b13645b06daeea26f517d813a8c8ebb4cfcb2db19ebe2ee7e8f573c7c", "tests": "05fd8d3202d9774f7043311e8615e1210a09cc579f1346eb2fb9ebbfec339b5b" } @@ -4194,7 +4198,7 @@ ], "notes": "已完成首轮逐分支审计;移动端基础适配测试通过,但相机默认/恢复、鼠标统一分派、光标状态和 IME 仍未达到 40250 等价。", "fingerprints": { - "reference": "11181965574e3b0c3e235b1403e17494f8fc1b8777a2b18b955f53a84e0bae54", + "reference": "b48b2a0d06fb8cd312626fbd8c91381ff2f385744a32d15a771c83a96c7d687f", "implementation": "d67243d6e4c881a79c37af554eeaeca5374c3a73caf7d6e96f8e8abdcabce9bf", "tests": "eb158440ac1fcfce57259aad60a090493d2cebaeab7e380c3513cb34563a90fd" } @@ -4219,9 +4223,9 @@ "UserInterface/InstanceBase.cpp", "UserInterface/InstanceBaseEffect.cpp", "UserInterface/PythonNetworkStreamPhaseGame.cpp", - "UserInterface/uitaskbar.py", - "UserInterface/uicharacter.py", - "UserInterface/uitarget.py" + "../../Client/Eternexus/root/uitaskbar.py", + "../../Client/Eternexus/root/uicharacter.py", + "../../Client/Eternexus/root/uitarget.py" ], "symbols": [ "CPythonTextTail::UpdateAllTextTail", @@ -4299,8 +4303,8 @@ ], "notes": "已完成 PythonTextTail/PythonMiniMap/PythonPlayer/TargetBoard/TaskBar 与当前 GameScene/UI 链的首轮逐分支审计;确认 TargetUI 未在正式 GameScene 注入、HUD 与新 Minimap 重复创建、Minimap scale 常量与 40250 不同,合同保持 PARTIAL,未修改实现。", "fingerprints": { - "reference": "a948059544c578dfb6f67d4e64ed41322095fb5734835aaa60aa4dd53567d152", - "implementation": "6b31f5832b14c75d5210ed1bf312c88f550c8d03eadbcf5b9f57b1983c4ec2c4", + "reference": "837dea4d51a3f13d195eae67568bdc4788e62d09cf54bfee010807d1d3bc0941", + "implementation": "7a4cc12dea041a8ac69ca9ce634d2c0eeac103c4ea457aff4e992089e77fd679", "tests": "fd53e0750b11c9d06bfc1613b53ed864f712732719c2c495a19b30fed6663c9c" } }, @@ -4419,7 +4423,7 @@ ], "notes": "proto 和格式解析已核对并有测试;主资源入口、EterPack 原格式、locale 编码和失败回退存在材料差异,保持 PARTIAL。", "fingerprints": { - "reference": "772c0563544d7e07bc38ae2c2a3684b5f1a248d22200dfaa108b2967e26e3ca8", + "reference": "c7172ae239ffed8543153c95496d53d8c92dcd6b94d7addacd324f7dbdf61ee7", "implementation": "313cb99fea6b2d3c7cdec918aad2c328d81b42fd75ee582bf301241b6507621d", "tests": "40416fb46b72679bfba5de491b7945b0cf95d08f524c9786589625f3641e6da3" } @@ -4513,7 +4517,7 @@ ], "notes": "已完成首轮逐分支审计;基础音量/监听器/环境声/动作音效路径存在,后台 BGM/SFX 同时静音恢复与退出 Audio 缓存清理已有回归,但 Miles 实例池和 BGM 逐帧状态机仍不等价。", "fingerprints": { - "reference": "2d3182776cf583a5e474701489a417e87ff2f8d7952a4ce03a4523dd570528cd", + "reference": "1a8a9232bcc63724814d15df99f211ddfe67a28fcb2052bb4c84ad79373bdfef", "implementation": "f4c3fed1f7cbf08c1032c536927a689a9a1b71b1c431de2afb3c0041ee577315", "tests": "a7ca9e4555b71f92776b56669a0b74e443dc2aaab78a2f208c86f9fc4bc75bfc" } @@ -4582,11 +4586,13 @@ "tests": [ "project/app_flow_lifecycle_test.gd", "project/bgm_test.gd", - "project/dds_lifecycle_test.gd", + "project/dds_lifecycle_test.gd" + ], + "last_test_result": "PARTIAL: focus activation/audio save-restore, AppFlow, DDS, editor validation and macOS export passed; Windows export/window/CRC/crash paths absent", + "commands": [ "godot --headless --path project --editor --quit", "godot --headless --path project --export-release macOS /tmp/mtgodot-platform-audit.app" - ], - "last_test_result": "PARTIAL: focus activation/audio save-restore, AppFlow, DDS, editor validation and macOS export passed; Windows export/window/CRC/crash paths absent" + ] }, "remaining": [ "增加 Windows 导出预设、GDExtension 构建和整包启动/退出验证", @@ -4596,9 +4602,9 @@ ], "notes": "已完成首轮逐分支审计;AppFlow、AssetPack、焦点激活音量保存恢复和 macOS/Android 适配存在,但 Windows 原生窗口/导出、全屏与 resize、CRC 更新检查、Eter 异常处理和完整发布门禁仍缺失。", "fingerprints": { - "reference": "ddccccedfb929112d89c6c14b3746569a64493245a0e8b70145df3979dfd611d", + "reference": "eadc7263dd115a86e31792b5ca4b90175287aef13f3c47e0aa7870081868a8bb", "implementation": "84417dac55504e5f626731fd2d3c78decec8808187fa70b427fdf83f66adf040", - "tests": "10987358f838e043daaed30ccb4639f738a6bbd19522783091a9916fbb5e2bc1" + "tests": "4d5d05a6a2b8c7c40d273ed26615013c3f3cfc0dd2dc15d947c9894bcb961ad3" } }, { diff --git a/audit/port-map/UserInterface/PythonPlayerEventHandler.cpp.json b/audit/port-map/UserInterface/PythonPlayerEventHandler.cpp.json new file mode 100644 index 00000000..3e0e9faf --- /dev/null +++ b/audit/port-map/UserInterface/PythonPlayerEventHandler.cpp.json @@ -0,0 +1,96 @@ +{ + "reference": "UserInterface/PythonPlayerEventHandler.cpp", + "reference_sha256": "2ef7f3cbb7fb6e9c86815b39a6a85cd83bd793ef460c8e0d8c532e2e585927f5", + "priority": "P0", + "contracts": [ + "movement.keyboard.motion" + ], + "functions": { + "CPythonPlayerEventHandler::GetSingleton": { + "status": "N_A", + "note": "singleton/ctor/dtor plumbing; NetPlay is a scene node" + }, + "CPythonPlayerEventHandler::~CPythonPlayerEventHandler": { + "status": "N_A", + "note": "singleton/ctor/dtor plumbing; NetPlay is a scene node" + }, + "CPythonPlayerEventHandler::OnClearAffects": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::OnSetAffect": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::OnResetAffect": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::OnSyncing": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::OnWaiting": { + "status": "DIVERGENT", + "note": "reference fires only inside CInstanceBase::Transform() while IsWalking() and this frame's displacement <=1.0 (InstanceBase.cpp:1922-1930), 100ms throttle vs m_kPPosPrevWaiting; not ported. The old idle-standing resend (_tick_on_waiting) was removed 2026-09-22." + }, + "CPythonPlayerEventHandler::OnMoving": { + "status": "PORTED", + "impl": [ + "project/net_play.gd:_on_local_moved" + ] + }, + "CPythonPlayerEventHandler::OnMove": { + "status": "PORTED", + "impl": [ + "project/net_play.gd:_on_local_moved" + ], + "note": "first moved frame after _was_moving=false; resets the 300ms moving window" + }, + "CPythonPlayerEventHandler::OnStop": { + "status": "PORTED", + "impl": [ + "project/net_play.gd:_on_anim_state" + ] + }, + "CPythonPlayerEventHandler::OnWarp": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::OnAttack": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::OnUseSkill": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::OnUpdate": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::OnChangeShape": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::OnHit": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::FlushVictimList": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::CPythonPlayerEventHandler": { + "status": "N_A", + "note": "singleton/ctor/dtor plumbing; NetPlay is a scene node" + }, + "CPythonPlayerEventHandler::CNormalBowAttack_FlyEventHandler_AutoClear::OnSetFlyTarget": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::CNormalBowAttack_FlyEventHandler_AutoClear::OnShoot": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::CNormalBowAttack_FlyEventHandler_AutoClear::Set": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::CNormalBowAttack_FlyEventHandler_AutoClear::SetTarget": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::CNormalBowAttack_FlyEventHandler_AutoClear::OnExplodingAtAnotherTarget": { + "status": "TODO" + }, + "CPythonPlayerEventHandler::CNormalBowAttack_FlyEventHandler_AutoClear::OnExplodingAtTarget": { + "status": "TODO" + } + } +} diff --git a/audit/remediation-roadmap.md b/audit/remediation-roadmap.md index f99ed43a..f8678164 100644 --- a/audit/remediation-roadmap.md +++ b/audit/remediation-roadmap.md @@ -1,335 +1,36 @@ -# 40250 客户端差异修复路线 +# 40250 1:1 移植队列 -本文件记录当前修复顺序和调度规则。它是给人和其他 AI 工具读取的当前路线,不替代机器可读的 `audit/manifest.json`;合同状态、剩余分支和证据以账本为准。 +工作方法见 `.agents/skills/metin2-40250-parity-audit/SKILL.md`。本文件只放批次和队列,不写每轮叙述(每轮结果记在 +`audit/port-map/**` 和 `audit/history.jsonl`)。2026-09-22 之前的按分支修复记录见本文件的 git 历史。 -## 当前基线 +实时剩余量与下一批候选: -统计快照:2026-09-21。 +```bash +python3 .agents/skills/metin2-40250-parity-audit/scripts/port_map.py status +python3 .agents/skills/metin2-40250-parity-audit/scripts/port_map.py queue --limit 20 +``` -| 项目 | 数量 | -| --- | ---: | -| 总合同 | 45 | -| 已排除 | 1 | -| 未完全闭合合同 | 44 | -| 未闭合分支条目 | 224 | -| P0 分支条目 | 93 | -| P1 分支条目 | 118 | -| P2 分支条目 | 13 | -| P3 分支条目 | 0 | +## 批次 -这些数量是快照,不是手工修改的真相。每次修复前后都要运行账本刷新和报告脚本,以 `audit/manifest.json` 的当前内容为准。 +| 批次 | 内容 | 状态 | +| --- | --- | --- | +| 0 | 工具:参考根解析、缺失文件报错、`port_map.py`、构建/测试入口 | 完成 2026-09-22 | +| 1 | 删除只被自身测试引用的 `project/*_system.gd` 与其 `test_*_parity.gd`,清除 manifest 中对应证据 | 待确认删除清单 | +| 2 | P0 角色/移动单元(并行 worktree) | 未开始 | +| 3 | P1 战斗/技能 → P2 游戏阶段封包 → P3 物品与 Python UI | 未开始 | +| 4 | `NEEDS_LIVE` 真服验证批 | 未开始 | -## 修复阶段 +## 批次 2 单元(互不共享实现文件的可并行) -### 阶段 0:账本和调度卫生 +- [ ] `UserInterface/PythonPlayerEventHandler.cpp`(已做 OnMove/OnMoving/OnStop;OnWaiting DIVERGENT)— `net_play.gd` +- [ ] `UserInterface/InstanceBaseMovement.cpp` + `UserInterface/InstanceBase.cpp`(同一 owner,串行)— `player_controller.gd`、`entity_store.cpp` +- [ ] `UserInterface/PythonPlayerInput.cpp` + `PythonPlayerInputKeyboard.cpp` + `PythonPlayerInputMouse.cpp` — `player_controller.gd`(与上一项串行) +- [ ] `GameLib/ActorInstanceMotion.cpp` + `ActorInstanceEvent.cpp` — 动作/事件 +- [ ] `GameLib/ActorInstance.cpp` + `ActorInstancePosition.cpp` + `ActorInstanceRotation.cpp` + `ActorInstanceCollisionDetection.cpp` +- [ ] `UserInterface/PythonCharacterManager.cpp` — `net_world.gd`、`entity_store.cpp` +- [ ] `UserInterface/PythonPlayer.cpp` -- 读取本文件、`audit/manifest.json`、相关合同和最近的 `audit/history.jsonl`。 -- 先运行 `audit_ledger.py refresh --write`、`report --write` 和 `validate`。 -- 将一个合同中的每个剩余分支作为独立的修复目标;修复后必须同步合同、账本和历史。 -- 问题/分支 ID 使用稳定格式 `/`;历史中只有合同 ID 的旧记录不能证明该合同的所有分支都已完成,首次处理某个分支时要补建具体 ID。 -- 如果只是缺少真实服务端、Windows 客户端或视觉运行证据,不要伪装成代码已完成,记录为验证阻塞。 +## 已知参考位置 -### 阶段 1:P0 在线核心链路 - -按完整的用户可玩链路推进: - -1. 登录、认证、选人、Loading、进入地图和失败重试:`network.login.phase_flow`、`network.character_select.create_delete`。 -2. WASD、停止、转向、远程同步、传送和地图切换:`movement.keyboard.motion`、`movement.remote_sync_state`、`world.map_load_transition`、`world.reset_cleanup`。 -3. 攻击、受击、死亡、经验、掉落和拾取:`combat.attack_combo_damage`、`combat.death.exp_drop`、`item.drop_pickup`。 -4. 技能释放、动作事件、特效和服务端结果:`skill.cast.effect_timing`、`skill.player_ui_cooldown`。 -5. 协议分派、重连、安全分支和资源/协议加载:`network.packet_dispatch_protocol`、`network.reconnect_warp`、`network.security_encoding`、`resource.pack_locale_proto`。 - -阶段 1 未完成前,不主动扩大 P1 的视觉或外围功能修复范围。 - -### 阶段 2:P1 游戏功能 - -依次处理背包/装备、NPC/任务、社交、目标选择、状态、模型和效果资源。`combat.flying_projectile` 在这一阶段处理;除非它直接阻塞 P0 或引入回归,否则不提前插队。 - -### 阶段 3:P2/P3 平台和表现 - -处理音频、天气、平台发布、移动端细节和其他不阻塞核心游戏流程的差异。 - -## 防止重复修复规则 - -- 每一轮只设置一个主合同,并为本轮选定一个明确的问题/分支 ID;可以同时修复被它直接调用的跨合同根因,但必须在历史中列出关联 ID。 -- 同一个问题/分支 ID 不得重复修复或重复审计,除非出现新的回归、实现文件发生实质变化,或新增证据证明原结论不成立。 -- 同一合同可以在后续轮次继续处理,但必须选择不同的 `remaining` 分支;合同是功能分组,不是问题去重单位。 -- 一个合同仍为 `PARTIAL` 不代表此前修改无效;只处理其 `remaining` 中尚未闭合的分支,不能重新审计已通过的测试路径。 -- 每轮开始前必须检查 `audit/history.jsonl`,确认本轮问题/分支 ID 没有已完成记录;如果历史记录不够细,先补充问题 ID,再开始修改。 -- 每轮结束必须记录:参考调用链、当前调用链、修改的分支、修复前失败证据、修复后测试、剩余阻塞和下一轮候选。 -- 下一轮候选按 `P0 → P1 → P2 → P3` 排序;同优先级按用户可玩链路和阻塞关系排序,不按“最容易修改”排序。 - -## 完成门槛 - -单个分支只有同时满足以下条件才可关闭: - -1. 40250 完整调用链和当前实现调用链已对齐。 -2. 前置条件、分支、算法、状态顺序、单位、时序、资源、协议副作用和清理行为均有等价证据。 -3. 有修复前失败、修复后通过的自动测试;必要时覆盖拒绝、打断、重复包、死亡、重连和地图切换。 -4. 需要真实服务端、Windows 客户端或视觉证据时,已完成对应验证;否则保持 `PARTIAL` 并记录阻塞原因。 - -合同只有在全部剩余分支关闭后,才能从 `PARTIAL` 提升为 `STATIC_VERIFIED` 或 `TEST_VERIFIED`。 - -## 下一轮 - -下一轮主合同固定为 `network.login.phase_flow`,具体处理其尚未闭合的问题/分支。除非发现新的 P0 回归或登录链路被其他合同阻塞,否则不切回已处理过的问题。 - -上一轮已闭合分支:`network.login.phase_flow/auth-game-phase-matrix/peer-close-login-owner`。 -真实 `/logout` 已证明建立连接后的 peer close,AppFlow 现在按 40250 回到唯一 LoginPhase owner; -不得重复修改“保留 GameScene/自动重连遮罩”这一已闭合分支。 - -上一轮已闭合分支:`network.login.phase_flow/auth-game-phase-matrix/phase-close-server-fixture`。 -本地等价 fixture 已证明显式 `PHASE_CLOSE` 不再伪造 `disconnected`;真实服务端主动 -`PHASE_CLOSE` 包序仍是证据阻塞,后续只在获得新 live fixture 时验证,不重复修改 native owner。 - -上一轮已处理分支:`network.login.phase_flow/auth-game-phase-matrix/empty-role-live-fixture`。 -已补充 `net_classic_e2e` 的 `MT_CLASSIC_EXPECT_EMPTY=1` 只读探针;当前账号仅有部分空槽, -不能将其当作全空角色证据,也不要通过删号制造 fixture。全空角色的正向 live 证据仍保持在 -`remaining`,后续只在获得安全 fixture 时验证,不重复修改本地 SELECT owner。 - -上一轮已处理分支:`network.login.phase_flow/login-failure-retry-cleanup/login-form-retry-owner`。 -已按 40250 `LoginWindow.Connect -> AccountConnector.Connect` 增加 LoginPhase retry owner;登录失败后 -只替换认证 transport,不再由 AppFlow 重试触发全局 `disconnect_from_server/world_reset`。本地 UI/source -断言和 native loopback 包序已通过;真实 Windows 可视化重试和服务器“失败->重新输入->成功”仍是证据阻塞, -不得重复修改该 owner。 - -前一阶段首选分支:`network.login.phase_flow/auth-game-phase-matrix/cross-phase-retry-order`。 -下一步只处理跨阶段失败/重复进入的真实或等价包序:失败后旧 auth/game stream 的迟到包、重复 -`PHASE_LOGIN/PHASE_AUTH` 的替换顺序、以及完整 UI retry 的真实服务端证据;优先补测试和 fixture, -不要重新审计已经由 -`direct-enter-loading-select-packet`、`gc-warp-live-package-order`、`peer-close-login-owner` 或 -`phase-close-server-fixture` 闭合的成功进入、peer-close owner 和显式 PHASE_CLOSE transport-owner -分离链路。 - -本轮已处理该分支中的认证成功尾包子路径:`GC_AUTH_SUCCESS` 后的旧 `PHASE_AUTH` 会随旧 -AccountConnector 接收缓冲一起丢弃,不重复发送 `CG_LOGIN3`。后续必须使用新的稳定子分支 -`network.login.phase_flow/auth-game-phase-matrix/cross-phase-retry-order-game-login-stale-tail` -处理游戏登录失败后的迟到 phase;不得重复修改认证成功的清理边界。 - -本轮已处理 `cross-phase-retry-order-game-login-stale-tail`:游戏 LoginPhase/SelectPhase/ -Loading 自有包在当前应用帧完成后按 40250 phase-local return 停止,迟到 receive tail 延后到 -下一次处理周期;Loading 转交 GamePhase 的包仍保留游戏批处理语义。后续应选择新的稳定子分支 -处理真实失败后重新输入成功、重复进入包序或服务器主动 PHASE_CLOSE live 证据,不得重复修改 -本轮 `return_after_current_packet` 机制。 - -本轮已处理 `network.login.phase_flow/auth-game-phase-matrix/auth-connect-immediate-failure-owner`: -认证地址解析/socket 同步失败与异步 `OnConnectFailure` 统一回到 40250 的 Offline owner,并保留 -登录重试资格。不得重复修改本轮 `handle_auth_connect_failure()` 的认证清理边界。 - -本轮已处理 `network.login.phase_flow/auth-game-phase-matrix/game-connect-immediate-failure-owner`: -普通认证成功后的游戏连接失败现在走独立 `OnConnectFailure -> LoginWindow` owner,DirectEnter -和 GC_WARP 失败走 `ClosePhase -> SetLoginPhase`,不再触发凭据 `OnLoginFailure` 或伪造 -`disconnected`。修复前 DirectEnter/普通 game-connect fixture 均有失败断言,修复后 native、 -Godot bridge、CTest、ASAN 和阶段/包结构审计通过;不得重复修改本轮 -`handle_game_connect_failure()`、`connect_failed` signal 或 GC_WARP owner 分离。 - -本轮已处理 `network.login.phase_flow/auth-game-phase-matrix/auth-connect-bridge-owner`:按 -40250 `AccountConnector::OnConnectFailure -> Offline -> LoginWindow` 修复初始认证同步/异步 -连接失败的 M2Client bridge,新增独立 `connect_failed` 回调、保留认证 session 和 -`retry_login` owner;修复前专项桥接测试失败,修复后 native/CTest/ASAN/Godot 与阶段/包结构 -审计通过。不得重复修改本轮 `on_auth_connect_failure`、`classic_auth_connect_failure_pending` -或认证 session 保留逻辑。 - -本轮已处理 `network.login.phase_flow/auth-game-phase-matrix/auth-peer-close-bridge-owner`:按 -40250 `CAccountConnector::OnRemoteDisconnect -> __OfflineState_Set` 增加认证 peer-close -独立 callback;M2Client 不再把认证 socket 被动关闭伪造成 `login_failed` 或 `connect_failed`, -并保留非终态 LoginPhase owner。修复前后均由真实本地 TCP 接受/关闭/再次替换 transport 的 -Godot 回归固定;不得重复修改本轮 `on_auth_remote_disconnect` 或 -`classic_auth_remote_disconnect_pending`。 - -本轮已处理 `network.login.phase_flow/auth-game-phase-matrix/phase-close-disconnect-login-owner`: -重新拆分 40250 的 `PHASE_LOGIN -> CPythonNetworkStream::SetLoginPhase` 与 -`PHASE_CLOSE/OnRemoteDisconnect -> Python networkmodule.SetLoginPhase -> net.Disconnect` 两种 -语义;只有真正的 `PHASE_LOGIN` 会在当前 transport 上发送 `CG_LOGIN/CG_LOGIN2`,显式关闭、peer close -和 DirectEnter 失败会清理旧 transport 并回到 LoginWindow owner。`net_classic_session_test` 的修复前 -失败、修复后无旧连接登录包/旧 transport Offline、CTest/ASAN/Godot 回归均已固定;不得重复修改 -`enter_login_phase` 的发送分支或本地 PHASE_CLOSE owner。真实服务器主动 PHASE_CLOSE 包序仍需 live 证据。 - -本轮已处理 `network.login.phase_flow/login-failure-retry-cleanup/game-peer-close-retry-owner`: -按 40250 `OnRemoteDisconnect -> SetLoginPhase -> LoginWindow` 与后续 -`LoginWindow.Connect -> ConnectToAccountServer` 的 owner 边界,游戏态 peer close 现在保留同一 -`ClassicSession` 的显式重试资格;重试只替换认证 transport,新的 `PHASE_AUTH` 只发送一份使用新密码的 -`CG_LOGIN3`,不再回退到全局 session/world teardown。修复前失败和 loopback 替换 transport 回归已记录; -不得重复修改 `m_login_retry_pending` 的游戏 peer-close owner。真实 Windows LoginWindow 时序、真实服务端 -失败后重试成功包序和完整跨阶段 retry 仍需 live 证据。 - -下一主候选:`network.login.phase_flow/auth-game-phase-matrix/live-package-order/server-initiated-phase-close`。 -本地结构和 owner 分支已覆盖,下一轮只取真实 Windows/40250 服务端证据:失败后重新输入成功、 -服务器主动 `PHASE_CLOSE`、完整跨阶段 retry 和 DirectEnter/GC_WARP 失败回退包序;不要回到已关闭 -的认证或游戏连接失败 owner。 - -本轮已执行该候选的真实 `/logout` 探针并登记为 -`network.login.phase_flow/auth-game-phase-matrix/live-package-order/server-initiated-phase-close`: -登录到 PHASE_GAME、执行命令、peer close、world reset 和 LoginPhase owner 均通过,但 -`MT_NET_TRACE=1` 没有出现 `phase -> 0`。服务端 `DESC::SetPhase(PHASE_CLOSE)` 的源码状态链已 -确认,当前 build 因 close 后的 `DESC::Packet` guard 没有提供客户端可观察的 PHASE_CLOSE 包, -因此不能用客户端改动伪造这个证据。外层 wrapper 的失败仅来自既有 4 个 ParticlesShaderRD/RID -泄漏门禁。除非更换服务端 build/fixture,否则不要重复 `/logout` 探针;下一步改为获取真实 -PHASE_CLOSE fixture,并在同一 fixture 上完成失败重试和跨阶段包序审计。 - -本轮新增并闭合本地分支: -`network.login.phase_flow/auth-game-phase-matrix/auth-success-game-before-auth-clear`。 -对照 40250 `__AuthState_RecvAuthSuccess -> SetLoginKey -> MainStream.Connect -> AccountConnector.Disconnect` -发现当前 `ClassicSession::pump()` 曾反向执行 auth disconnect 再 game connect;修复后使用 loopback -双 transport 状态回调固定为 `game:connecting -> auth:offline`。该分支不得重复修改。下一主候选仍是 -新的真实失败后重试/跨阶段包序 fixture;如服务端 fixture 没有变化,不重复 `/logout`。 - -本轮新增并闭合本地分支: -`network.login.phase_flow/auth-game-phase-matrix/game-login-retry-process-boundary`。 -对照 40250 `CPythonApplication::Process -> CPythonNetworkStream::Process -> CAccountConnector::Process` -发现认证成功后当前端在同一 pump 再次处理新游戏流;修复后重试期间保留旧主流的处理机会, -认证成功建立替换流后延迟到下一 pump 才处理新流。该分支不得重复修改;真实服务端失败后重试 -成功包序和 Windows LoginWindow 仍是未验证证据,下一候选继续选择新的 live/跨阶段分支。 - -本轮新增并闭合本地分支: -`network.login.phase_flow/legacy-text-wire-encoding/ui-gb2312-input-capacity`。 -发现 native 已按 UTF-8 -> GB2312 转换后的 wire 字节校验,但 `CharSelectScreen` 仍按 UTF-8 -字节限制角色名输入,导致“龙驹简体中文免费版”在 UI 层被提前拒绝。现改为 UI 只做 Unicode -字符/控制字符校验,实际 24-byte GB2312 容量由 native 发包边界统一判断,并加入 UI/native -回归;不得重复把角色名 UI 改回 UTF-8 字节限制,也不得把地图名、任务名、系统提示套用该 -24-byte 字段限制。 - -本轮新增并处理分支: -`network.login.phase_flow/ticket-slot-warp-order/direct-enter-failure-login-owner-reset`。 -对照 40250 `OnConnectFailure -> ClosePhase -> net.Disconnect -> SetOffLinePhase`,修复 -DirectEnter/GC_WARP 失败回到 LoginPhase 时残留 `m_direct_enter`、导致替换 `PHASE_LOGIN` -跳过普通角色列表清理的问题;修复前失败测试和修复后通过证据已登记。真实 Windows/服务端 -失败回退包序仍是 `PARTIAL`,下一轮继续获取新的 live fixture,不重复修改本地 ClosePhase owner。 - -本轮新增并处理分支: -`network.login.phase_flow/auth-game-phase-matrix/phase-close-login-retry-owner`。 -对照 40250 `PHASE_CLOSE -> networkmodule.SetLoginPhase -> net.Disconnect -> LoginWindow.Connect` -补齐显式关闭后的 retry owner;同一 `ClassicSession` 现在可替换认证 transport,收到新的 -`PHASE_AUTH` 时只发送一份 `CG_LOGIN3`。真实服务端 `PHASE_CLOSE` 包序和完整跨阶段 retry -仍是 live 证据阻塞,不能据此关闭整个登录合同。 - -本轮新增并处理分支: -`network.login.phase_flow/auth-game-phase-matrix/phase-handshake-reentry-owner`。 -对照 40250 `RecvPhasePacket(PHASE_HANDSHAKE) -> SetHandShakePhase` 和 -`CAccountConnector::__AuthState_RecvPhase -> __HandshakeState_Set`,补齐认证/游戏 -HandShake owner 重入;从 Loading/Game 重入时先执行旧 phase-leave world 清理,后续只有新的 -`PHASE_LOGIN/PHASE_AUTH` 才能继续登录。真实 Windows/服务端重试包序仍未闭合;不得重复修改 -本轮的 `PHASE_HANDSHAKE` owner 和 world-leave 边界。 - -本轮新增并处理分支: -`network.login.phase_flow/auth-game-phase-matrix/auth-success-result-authority`。 -对照 40250 `__AuthState_RecvAuthSuccess` 的 `bResult` 分支,修复当前端把 `login_key == 0` -额外解释为凭据失败的问题;成功/失败现在只由 `result` 决定,票据值仍交给后续游戏连接。 -真实服务端不会主动构造成功零票据包,不能用该本地边界测试替代完整 live retry 或安全模块证据; -不得重复修改本轮的认证结果判定条件。 - -本轮新增并处理分支: -`network.login.phase_flow/auth-game-phase-matrix/auth-phase-close-silent-owner`。 -对照 40250 `CAccountConnector::__AuthState_RecvPhase` 只处理 `PHASE_HANDSHAKE/PHASE_AUTH` -的分支表,修复当前端把认证 `PHASE_CLOSE` 错误转成游戏侧 `phase_closed` 和 -`auth server closed phase`。认证 close 现在保持 AUTH owner,直到真实认证 socket -disconnect/failure;不得重复修改本轮的认证 close 静默语义。服务器主动 `PHASE_CLOSE` -的 live 包序仍需外部 fixture,不能用本地 phase 注入替代。 - -本轮新增并处理分支: -`network.login.phase_flow/auth-game-phase-matrix/phase-select-reentry-owner`。 -对照 40250 `RecvPhasePacket(PHASE_SELECT) -> SetSelectPhase -> m_phaseLeaveFunc.Run()`, -修复当前端从 Game 返回 Select 时未清理旧地图 world、未通知宿主 phase-leave 的差异;现在先执行 -Game owner 的 `reset_for_map_change()` 和 `on_phase_leave`,再安装 Select/DirectEnter owner。 -该分支不得重复修改;真实 Windows/服务端跨阶段 retry、服务器主动 PHASE_CLOSE 和 GC_WARP -失败回退包序仍需 live fixture。 - -本轮新增并处理分支: -`network.login.phase_flow/auth-game-phase-matrix/phase-dead-noop-time-sync`。 -对照 40250 `RecvPhasePacket(PHASE_DEAD)` 的显式空分支,修复当前端先打开 time-sync mode、 -导致后续重复握手错误发出 `CG_TIME_SYNC`,以及清除显式 PHASE_CLOSE owner 标记的差异;现在 -`PHASE_DEAD` 不改变阶段 owner 或 time-sync 状态。该分支不得重复修改;完整跨阶段 retry、 -服务器主动 PHASE_CLOSE、空角色 live 和 GC_WARP 失败回退包序仍需独立证据。 - -### 2026-09-22:本轮临时切换主合同到 `movement.keyboard.motion` - -`network.login.phase_flow` 的剩余分支目前全部卡在真实 Windows 客户端/服务端主动 -`PHASE_CLOSE` 包序证据或第三方闭源安全 SDK(Matrix/Passpod/Panama/HybridCrypt/HS/XTrap), -本地代码层面已无可继续推进的动作。按阶段 1 的可玩链路顺序(登录 → 移动 → 战斗 → 技能 → -网络),本轮改为处理 `movement.keyboard.motion`。 - -本轮新增并闭合分支:`movement.keyboard.motion/camera-auto-follow-rotation`。 -对照 40250 `CPythonPlayer::NEW_MoveToDirection` 的 `m_isCmrRot` 相机跟随分支(每帧对按住 -的方向键,把相对相机的方位角折算到 `fRotRat∈[-90,90]` 并按 `m_fCmrRotSpd=20.0` 的速率用 -`CCamera::Roll` 持续转动相机),当前端此前完全没有实现这个副作用。已在 -`player_controller.gd` 新增 `_camera_auto_rotate()`,用同样的折叠/缩放公式驱动 -`GameCamera.yaw`;D3D `Roll` 与 Godot `yaw` 转向手性相反,增量符号作为显式记录的平台适配 -取反,用『按住方向时相机朝该方向收敛、正前后不转、从不越界』的不变式在 -`keyboard_motion_timeline_test.gd` 中核验(因为没有真实 Windows 客户端可供逐位对照)。 -详见 `audit/contracts/movement.keyboard-motion.md` 的 `Implementation fix round -2026-09-22` 小节。不得重复修改本轮的 `_camera_auto_rotate()` 折叠公式或符号约定,除非 -出现新的 live 证据推翻当前的收敛不变式假设。 - -本轮过程中额外发现但未修复(已记录进合同 `Remaining` 与本文件供下一轮参考): -`CActorInstance::CanAct()` 的 `Paralysis/Faint/Sleep` 三个移动门当前端完全没有接入 -(`net_play.gd::_can_process_network_state()` 只挡 `dead/stunned/knock_down`)。这三个 -状态的触发源分别在萨满技能、Python 侧角色模块和效果可见性绑定,尚未定位到当前端对应的 -服务端 affect 管线,属于比相机分支更大的独立子项,不要在没有先做好触发源映射的情况下 -仓促接线。 - -`movement.keyboard.motion` 合同仍为 `PARTIAL`:根运动/`AccumulationMovement`、 -`OnMove/OnMoving/OnWaiting/OnStop` 资源事件源、同步/受击/死亡/传送门叠加顺序、上述 -Paralysis/Faint/Sleep 移动门和真实模型姿态测试仍未闭合,下一轮可以在这些分支中任选一个 -继续(`AccumulationMovement` 根运动差异或 Paralysis/Faint/Sleep 移动门是其中两个具体、 -无需真实服务端/客户端证据即可推进的候选)。 - -### 2026-09-22T01:30Z:`movement.keyboard.motion/canact-paralysis-faint-sleep-gate` 调查关闭(无需修复) - -对上一轮标记的两个候选之一(`Paralysis/Faint/Sleep` 移动门)做完整可达性追踪后确认它是 -**假阳性**,不是代码可执行的差异,本轮未改动 GDScript/C++ 实现: - -- Paralysis:唯一 setter `__Shaman_SetParalysis`(`InstanceBaseEffect.cpp:821`)在整个可达 - 40250 源码树中零调用点,是死代码,40250 客户端实际运行时 `IsParalysis()` 恒为 false。 -- Sleep:`SetAffect()` 的 `AFFECT_SLEEP` 分支已被注释掉,唯一存活路径是 - `AFFECT_STUN → SetSleep()`(`InstanceBaseEffect.cpp:932`),即 `IsSleep()` 实际上是 - AFFECT_STUN 的别名,已被当前端 `entity_store.h:119` 的 `stunned` 字段和 - `net_play.gd::_can_process_network_state()` 的 `dead/stunned/knock_down` 门覆盖。 -- Faint:唯一 setter `chrFaintTest()`(`PythonCharacterModule.cpp:1087`)是与 - `chrtestRestoreRenderMode` 并列的调试用 Python 绑定,作用于 `GetSelectedInstancePtr()`; - 现有 40250 检出没有 root/ Python 脚本可证明/证伪在线可达性,evidence-blocked,未修复。 - -调查过程中发现的衍生项(`IsStun()` 的 `RecvStunPacket()` 对主角 `GC_STUN` 走 `Die()` -而非 `Stun()`)核实后确认**不是** `movement.keyboard.motion` 差异——`CanAct()` 对 -`IsDead()`/`IsStun()` 的移动阻断效果相同,当前端 `_can_process_network_state()` 也已对 -`dead`/`stunned` 同等阻断,移动结果已正确等价;`Die()`/`Stun()` 分流只影响死亡系统本身 -(EXP 掉落/尸体/复活),已在 `combat.affect-status.md` Remaining #2、分支矩阵第 -63/67 行登记,不在本合同重复记录。详见 -`audit/contracts/movement.keyboard-motion.md` 的「调查轮次 2026-09-22T」小节。 -**该分支已关闭,不要在没有新 live 证据的情况下重新调查 Paralysis/Faint/Sleep 或 -GC_STUN 主角分流。** - -`movement.keyboard.motion` 下一轮候选收窄为:`AccumulationMovement` 固定 300cm -根运动/连续速度替代差异、`OnMove/OnMoving/OnWaiting/OnStop` 资源事件源、同步/受击/ -死亡/传送门叠加顺序、真实模型骨骼快切不翻转的模型级测试——四者均仍未闭合。 - -`network.login.phase_flow` 的下一主候选仍保持文件前面记录的 -`network.login.phase_flow/auth-game-phase-matrix/live-package-order/server-initiated-phase-close`: -本地结构和 owner 分支均已覆盖,只等新的真实服务端/Windows fixture;在没有新 fixture 前不要 -重复 `/logout` 探针或重复修改已闭合的 owner 分支。 - -### 2026-09-22T03:15Z:`movement.keyboard.motion/server-speed-scale-clamp` 修复 - -追查上一轮列出的四个候选之一(`AccumulationMovement` 根运动/连续速度替代差异)时,先确认 -远端 `entity_store.cpp::advance_walk_by_motion` 和本地 `_process()` 的位移积分在种类上同构 -(都是恒速 `speed*dt` 直线插值),排除了「需要整体迁移到逐帧根运动采样」这个更大方向;转而 -逐项核对两条路径各自的速度缩放公式,发现真正的差异:本地 `set_server_speed()` 把 -`moving_speed/100.0` 塞进 `clampf(..., 0.25, 3.0)`,而参考 `CInstanceBase::SetMoveSpeed` -(`InstanceBaseMovement.cpp`)和本项目已修复的远端 `EntityStore::motion_move_speed` 都只有 -`moving_speed>1100 → 0` 一个门,没有下限/上限夹钳。已修复为 -`0.0 if moving_speed > 1100 else moving_speed/100.0`,新增 `keyboard_motion_timeline_test.gd` -断言覆盖旧边界内外和 1100 冻结门,随同全部相关回归套件(`test_wasd_steering_parity`、 -`test_no_auto_move_regression`、`game_camera_test`、`movement_parity_test`、`player_move_test`、 -`mouse_controller_test`、`test_alignment_parity`、`netplay_test`)通过。详见 -`audit/contracts/movement.keyboard-motion.md` 的 `Implementation fix round 2026-09-22 — -server-speed-scale-clamp` 小节。**该分支已关闭,不要在没有新证据的情况下重新调整 -`set_server_speed` 的缩放边界。** - -副产品发现(未下定论,供下一轮参考):`CInstanceBase::SHORSE::SetMoveSpeed` -(`InstanceBase.cpp:40-135`)是坐骑上玩家的独立速度设定,只在 `IsMounting()` 时生效; -远端实体对 `mount_vnum!=0` 直接归零,而本地玩家改走 `MOTION_MODE_HORSE` 动作资源—— -两者是否构成差异本轮未核实,不要不经调查直接假设一致或假设有 bug。 - -`movement.keyboard.motion` 下一轮候选收窄为:`AccumulationMovement` 固定 300cm -根运动/连续速度替代差异(积分种类已确认同构,但逐帧时序/`.msa` motion event 源仍未对拍)、 -`OnMove/OnMoving/OnWaiting/OnStop` 资源事件源、同步/受击/死亡/传送门叠加顺序、真实模型骨骼 -快切不翻转的模型级测试、坐骑速度设定(`SHORSE::SetMoveSpeed`)——五者均仍未闭合。 +- Python UI 以 40250 的 `Client/Eternexus/root`(打包为 `pack/root.epk`,90 个文件)为准;仓库内 `assets/root` + 是另一版本(m2dev),与 40250 有实质差异(如 `uitarget.py` 按钮表),不能作为参考。 diff --git a/audit/reports/source-coverage.md b/audit/reports/source-coverage.md index 4174eb9e..b62a0800 100644 --- a/audit/reports/source-coverage.md +++ b/audit/reports/source-coverage.md @@ -3,8 +3,8 @@ This report is a coverage gate, not proof of parity. `manifest_exact` means the source is explicitly listed by a contract; `inferred_review_queue` means it is assigned by a broad area rule and still requires semantic review. - Active Visual Studio source/include files: **524** -- Explicitly listed by manifest: **142** -- Static-reviewed groups (still PARTIAL for parity): **364** +- Explicitly listed by manifest: **143** +- Static-reviewed groups (still PARTIAL for parity): **363** - Inferred review queue: **0** - Build support (`StdAfx`): **18** - Unowned behavior candidates: **0** @@ -26,7 +26,7 @@ This report is a coverage gate, not proof of parity. `manifest_exact` means the | `lifecycle.bootstrap_phase_host` | 11 | 34 | 0 | | `lifecycle.platform_release` | 6 | 105 | 0 | | `movement.click_collision` | 6 | 12 | 0 | -| `movement.keyboard.motion` | 6 | 6 | 0 | +| `movement.keyboard.motion` | 10 | 6 | 0 | | `movement.motion_resource_events` | 8 | 9 | 0 | | `movement.remote_sync_state` | 6 | 0 | 0 | | `network.character_select.create_delete` | 5 | 4 | 0 | @@ -47,7 +47,7 @@ This report is a coverage gate, not proof of parity. `manifest_exact` means the | `social.party_guild_messenger` | 9 | 13 | 0 | | `ui.equipment.tooltip` | 5 | 0 | 0 | | `ui.hud_target_minimap_status` | 12 | 30 | 0 | -| `ui.input_camera_mobile` | 5 | 8 | 0 | +| `ui.input_camera_mobile` | 5 | 7 | 0 | | `world.dungeon_activity` | 9 | 2 | 0 | | `world.entity_spawn_state_sync` | 4 | 6 | 0 | | `world.environment_weather` | 8 | 11 | 0 | @@ -55,7 +55,7 @@ This report is a coverage gate, not proof of parity. `manifest_exact` means the | `world.map_load_transition` | 6 | 34 | 0 | | `world.mount_pet_polymorph` | 9 | 0 | 0 | | `world.npc_drop_actor_spawn` | 10 | 10 | 0 | -| `world.reset_cleanup` | 7 | 0 | 0 | +| `world.reset_cleanup` | 9 | 0 | 0 | | `world.terrain_water_collision` | 6 | 35 | 0 | ## Unowned behavior candidates diff --git a/audit/source-coverage.json b/audit/source-coverage.json index 8c80c152..3bbe8bd4 100644 --- a/audit/source-coverage.json +++ b/audit/source-coverage.json @@ -1,9 +1,9 @@ { "reference_root": "../40250/Server Client TMP4/ClientVS22/source", "active_source_count": 524, - "manifest_exact_count": 142, + "manifest_exact_count": 143, "inferred_review_queue_count": 0, - "reviewed_static_group_count": 364, + "reviewed_static_group_count": 363, "build_support_count": 18, "unowned_behavior_candidates": [], "by_contract": { @@ -53,7 +53,7 @@ "manifest_exact": 6 }, "movement.keyboard.motion": { - "manifest_exact": 6, + "manifest_exact": 10, "reviewed_static_group": 6 }, "movement.motion_resource_events": { @@ -134,7 +134,7 @@ "manifest_exact": 12 }, "ui.input_camera_mobile": { - "reviewed_static_group": 8, + "reviewed_static_group": 7, "manifest_exact": 5 }, "world.dungeon_activity": { @@ -164,7 +164,7 @@ "manifest_exact": 10 }, "world.reset_cleanup": { - "manifest_exact": 7 + "manifest_exact": 9 }, "world.terrain_water_collision": { "reviewed_static_group": 35, @@ -233,6 +233,7 @@ "path": "EffectLib/EffectManager.cpp", "kind": "manifest_exact", "owners": [ + "world.reset_cleanup", "render.effect_particle_motion" ] }, @@ -649,9 +650,9 @@ }, { "path": "EterLib/Camera.cpp", - "kind": "reviewed_static_group", + "kind": "manifest_exact", "owners": [ - "ui.input_camera_mobile" + "movement.keyboard.motion" ] }, { @@ -1979,6 +1980,7 @@ "path": "GameLib/ActorInstance.cpp", "kind": "manifest_exact", "owners": [ + "movement.keyboard.motion", "combat.death.exp_drop", "movement.remote_sync_state", "world.mount_pet_polymorph" @@ -2004,6 +2006,7 @@ "path": "GameLib/ActorInstanceBattle.cpp", "kind": "manifest_exact", "owners": [ + "movement.keyboard.motion", "combat.attack_combo_damage", "combat.affect_status" ] @@ -2247,6 +2250,7 @@ "path": "GameLib/FlyingObjectManager.cpp", "kind": "manifest_exact", "owners": [ + "world.reset_cleanup", "combat.flying_projectile" ] }, @@ -2289,6 +2293,7 @@ "path": "GameLib/GameUtil.cpp", "kind": "manifest_exact", "owners": [ + "movement.keyboard.motion", "movement.click_collision" ] }, diff --git a/project/net_play.gd b/project/net_play.gd index 513d8e12..886f0faf 100644 --- a/project/net_play.gd +++ b/project/net_play.gd @@ -830,6 +830,8 @@ func _entity_name(e: Dictionary) -> String: # --- movement ---------------------------------------------------------------- # `pc.moved` 在平移的每一帧发。第一帧 = OnMove,之后各帧 = OnMoving。 +# 40250: CPythonPlayerEventHandler::OnMove +# 40250: CPythonPlayerEventHandler::OnMoving func _on_local_moved(pos: Vector3) -> void: if _observer_mode or client == null or not client.is_in_game(): return @@ -846,6 +848,7 @@ func _on_local_moved(pos: Vector3) -> void: _send_state(FUNC_MOVE, 0, pos) _last_moving_sent_t = now +# 40250: CPythonPlayerEventHandler::OnStop func _on_anim_state(state: String) -> void: if _observer_mode: return