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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 <unit>` and `port_map.py show <unit>` 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 <unit> | show <unit> | 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 <test>.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`.
|
||||
|
||||
@@ -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"<missing>")
|
||||
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")),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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/<unit path>.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 <unit> [<unit>...] create or extend entries (new functions as TODO)
|
||||
port_map.py show <unit> 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: `<ret> [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())
|
||||
@@ -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
|
||||
+113
-107
File diff suppressed because one or more lines are too long
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
-326
@@ -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 使用稳定格式 `<contract-id>/<branch-slug>`;历史中只有合同 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` 按钮表),不能作为参考。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user