diff --git a/.agents/skills/metin2-40250-parity-audit/SKILL.md b/.agents/skills/metin2-40250-parity-audit/SKILL.md index 76d92cd4..2732da9d 100644 --- a/.agents/skills/metin2-40250-parity-audit/SKILL.md +++ b/.agents/skills/metin2-40250-parity-audit/SKILL.md @@ -27,13 +27,13 @@ The 40250 source is the specification. Do not design behavior; transcribe it. Co ## Code layout: mirror 40250 The logic layer has the 40250 structure, not a new architecture. Route, batch order and -prerequisites (2A base, 2R pack inventory, 2V vertical slice) are in `docs/PORT-PLAN.md`. `port_map.py` assigns every unit +prerequisites (2A base, 2R pack inventory, 2V0-2V3 vertical slices) are in `docs/PORT-PLAN.md`. `port_map.py` assigns every unit one of three layers: | Layer | 40250 units | How to port | Where | | --- | --- | --- | --- | | `logic` | `UserInterface/`, `GameLib/`, `EterLib` net/timer/text parsing, `EterPack`, `EterLocale` (everything not listed below) | Copy C++ -> C++. Same file name, class name, method names, member names and statement order; one reference file = one implementation file | `extension/src/port//.{h,cpp}` | -| `python` | `Client/Eternexus/root/*.py`, `uiscript/`, `UserInterface/*Module.cpp`, `EterPythonLib/`, `ScriptLib/` | Decided: embedded CPython 2.7.18 runs the scripts unchanged (`docs/PYTHON-EMBED-EVAL.md`, batch 2P); port only the C++ side. Never translate the scripts. Script functions get `RUN_AS_IS` (after the 2A re-baseline), never `N_A`. Reference is 40250 `Eternexus/root`, **not** `assets/root` | `extension/src/port//` (bindings), scripts from pack | +| `python` | `Client/Eternexus/root/*.py`, `uiscript/`, `UserInterface/*Module.cpp`, `EterPythonLib/`, `ScriptLib/` | Decided: embedded CPython 2.7.18 runs the scripts unchanged (`docs/PYTHON-EMBED-EVAL.md`, batch 2P); port only the C++ side. Never translate the scripts. Script functions get `RUN_AS_IS` once its evidence gate passes, never `N_A`. Reference is 40250 `Eternexus/root`, **not** `assets/root` | `extension/src/port//` (bindings), scripts from pack | | `platform` | Direct3D/`Grp*`, Granny (`EterGrnLib`), Miles, SpeedTree, `EffectLib`/terrain rendering, Win32 window/input/IME, threads, anti-cheat | Adapter behind the interface the 40250 caller uses; equivalence by observable output | `extension/src/platform/` + existing render code (`metin2_model`, `metin2_anim`, `gr2_bridge`, ...) | Rules for the `logic` layer: @@ -48,10 +48,14 @@ Rules for the `logic` layer: switch until the new path is wired into the runtime; delete the old logic in the same commit that switches its callers to the ported code, leaving only glue (node creation, forwarding Godot input, reading ported state to place nodes). A ported function nothing calls at runtime stays `TODO`. -- Keep 40250 **widths and overflow semantics**, not its C type names: 40250 is Win32, where `long` and - `unsigned long` are 32-bit. Use the fixed-width types from `port/common/Win32Types.h`; every +- Keep 40250 **widths and overflow semantics**, not its C type names: the 40250 executable targets + 32-bit Win32 (ILP32), where `long`, `unsigned long`, and pointers are 32-bit. `Win32Types.h` + supplies fixed-width Win32 scalar aliases, but cannot redefine C++ keywords such as `long`; + serialized declarations use explicit fixed-width types. Every serialized struct (proto records, packets, EPK index, msa/msm) gets 40250's `#pragma pack` and a - `static_assert(sizeof(T) == N)`; pointers stored in `DWORD` become `uintptr_t` with a port-map note. + `static_assert(sizeof(T) == N)`. Pointer/handle types stay pointer-sized behind platform adapters; + pointers stored in `DWORD` become `uintptr_t` with a port-map note. Arithmetic that relies on + 32-bit wrap uses unsigned operations or explicit wrapping helpers, never signed-overflow UB. - Keep 40250 units (`TPixelPosition` in cm, `DWORD` ms from `ELTimer_GetMSec`, degrees). Convert to Godot space only in the adapter. - Port in `#include`-dependency order: a shared header belongs to the first unit that needs it, and @@ -97,6 +101,9 @@ conflicts between parallel rounds). Schema in `references/audit-schema.md`. Func - `TODO` — not yet compared. - `PORTED` — current counterpart is statement-equivalent; `impl` names it. +- `RUN_AS_IS` — Python source is byte-identical to the reference, packaged and loaded by embedded + CPython on a reachable runtime path; `impl` names the packaged script and `evidence` names the + target-platform import/runtime proof. It is invalid for C++ units. - `ADAPTED` — platform adapter; `note` states both sides and the invariant, with a test. - `N_A` — platform plumbing with no gameplay semantics; `note` states why. - `DIVERGENT` — known different and not yet fixed; `note` says how. 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 4421000f..b211de99 100644 --- a/.agents/skills/metin2-40250-parity-audit/references/audit-schema.md +++ b/.agents/skills/metin2-40250-parity-audit/references/audit-schema.md @@ -124,8 +124,13 @@ One JSON file per 40250 source unit at `audit/port-map//.json`: ``` - Function keys are the reference's qualified names (`Class::Method`, or the free-function name). -- `status` is one of `TODO`, `PORTED`, `ADAPTED`, `N_A`, `DIVERGENT`, `NEEDS_LIVE` (meanings in `SKILL.md`). -- `impl` is required for `PORTED`/`ADAPTED`/`NEEDS_LIVE`; `note` is required for `ADAPTED`, `N_A`, +- `status` is one of `TODO`, `PORTED`, `RUN_AS_IS`, `ADAPTED`, `N_A`, `DIVERGENT`, `NEEDS_LIVE` + (meanings in `SKILL.md`). +- `RUN_AS_IS` is valid only for a Python script unit whose exact reference bytes are shipped and + loaded unchanged by embedded CPython. It requires `impl` (the packaged script path) and a + non-empty `evidence` list naming target-platform import/runtime evidence. The unit's + `reference_sha256` plus the committed resource manifest bind the evidence to exact source bytes. +- `impl` is required for `PORTED`/`RUN_AS_IS`/`ADAPTED`/`NEEDS_LIVE`; `note` is required for `ADAPTED`, `N_A`, `DIVERGENT` and `NEEDS_LIVE`. `ADAPTED` also needs a `test` path. - When `reference_sha256` no longer matches the file, every non-`TODO` function in the unit must be rechecked before the hash is updated. diff --git a/.agents/skills/metin2-40250-parity-audit/scripts/port_map.py b/.agents/skills/metin2-40250-parity-audit/scripts/port_map.py index fddc7b55..812db949 100644 --- a/.agents/skills/metin2-40250-parity-audit/scripts/port_map.py +++ b/.agents/skills/metin2-40250-parity-audit/scripts/port_map.py @@ -31,8 +31,8 @@ from pathlib import Path import refroot # script directory is on sys.path when run directly from audit_source_coverage import REFERENCE_DIRS, active_project_sources -STATUSES = ("TODO", "PORTED", "ADAPTED", "N_A", "DIVERGENT", "NEEDS_LIVE") -DONE = {"PORTED", "ADAPTED", "N_A"} +STATUSES = ("TODO", "PORTED", "RUN_AS_IS", "ADAPTED", "N_A", "DIVERGENT", "NEEDS_LIVE") +DONE = {"PORTED", "RUN_AS_IS", "ADAPTED", "N_A"} PY_ROOT = "../../Client/Eternexus/root" UNIT_DIRS = REFERENCE_DIRS + ("EterBase", "EterPythonLib", "ScriptLib", "SpeedTreeLib", "SphereLib", "EterImageLib") PORT_DIR = "extension/src/port" @@ -310,9 +310,15 @@ def cmd_check(repo: Path, units: dict[str, Path], args) -> int: 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"): + if status in {"PORTED", "RUN_AS_IS", "ADAPTED", "NEEDS_LIVE"} and not info.get("impl"): print(f"ERROR {label}: {fn} is {status} without impl") problems += 1 + if status == "RUN_AS_IS" and (layer(unit) != "python" or not unit.endswith(".py")): + print(f"ERROR {label}: {fn} is RUN_AS_IS outside a Python script unit") + problems += 1 + if status == "RUN_AS_IS" and not info.get("evidence"): + print(f"ERROR {label}: {fn} is RUN_AS_IS without runtime evidence") + 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 diff --git a/audit/history.jsonl b/audit/history.jsonl index 094eb107..d7ed09e9 100644 --- a/audit/history.jsonl +++ b/audit/history.jsonl @@ -485,3 +485,4 @@ {"time": "2026-09-22T03:15:00Z", "event": "implementation_fix_round", "result": "PARTIAL", "ids": ["movement.keyboard.motion/server-speed-scale-clamp"], "files": ["project/player_controller.gd", "project/keyboard_motion_timeline_test.gd", "audit/contracts/movement.keyboard-motion.md", "audit/manifest.json", "audit/history.jsonl", "audit/remediation-roadmap.md", "audit/reports/coverage.md"], "tests": ["40250 reference source inspection (PASS: InstanceBaseMovement.cpp CInstanceBase::SetMoveSpeed(UINT uMovSpd) only guards uMovSpd>1100 -> 0; otherwise applies uMovSpd/100.0f unclamped)", "extension/src/net/entity_store.cpp EntityStore::motion_move_speed re-read (PASS: already-fixed remote-entity path mirrors the same >1100 -> 0 / mount_vnum!=0 -> 0 guards, no [0.25,3.0]-style band)", "pre-fix player_controller.gd::set_server_speed (FAIL by construction: clampf(moving_speed/100.0, 0.25, 3.0) silently floors/ceils haste or slow stacks outside the reference's 25..300 movSpd band)", "post-fix godot --headless --path project --script keyboard_motion_timeline_test.gd (PASS, incl. new set_server_speed(10/500/1200/100) assertions)", "godot --headless --path project --script test_wasd_steering_parity.gd (PASS)", "godot --headless --path project --script test_no_auto_move_regression.gd (PASS)", "godot --headless --path project --script game_camera_test.gd (PASS)", "godot --headless --path project --script movement_parity_test.gd (PASS)", "godot --headless --path project --script player_move_test.gd (PASS)", "godot --headless --path project --script mouse_controller_test.gd (PASS)", "godot --headless --path project --script test_alignment_parity.gd (PASS)", "godot --headless --path project --script netplay_test.gd (PASS; unrelated mock set_server_speed in netplay_test.gd has no clamp, unaffected)", "git diff --check (PASS)", "audit_ledger.py refresh --write/report --write/validate (PASS)"], "reason": "在同一合同下追查『根运动 vs 连续速度』假设时发现这是假线索——entity_store.cpp::advance_walk_by_motion 与本地 _process() 的位移积分在种类上同构(都是恒速直线插值);真正的差异是 set_server_speed() 把 moving_speed/100.0 塞进无参考依据的 clampf(0.25, 3.0),而参考实现 CInstanceBase::SetMoveSpeed 和本项目已修复的 EntityStore::motion_move_speed 都只有 moving_speed>1100 -> 0 这一个门。移除该私有夹钳,改为 0.0 if moving_speed > 1100 else moving_speed/100.0,两处调用方(net_world.gd、net_play.gd)均不额外夹钳,改动范围限于 player_controller.gd。新增回归断言覆盖旧边界内外和 1100 冻结门。副产品发现:CInstanceBase::SHORSE::SetMoveSpeed 是坐骑上玩家的独立速度设定,只在 IsMounting() 时生效,与远端 mount_vnum!=0 -> 0 的处理方式不同——本轮未下定论,留作独立候选。未修改根运动/AccumulationMovement 适配层、资源与网络事件完整顺序、同步/受击门叠加或真实模型测试,合同保持 PARTIAL。"} {"time": "2026-09-22T06:00:00Z", "event": "port_round", "unit": "UserInterface/PythonPlayerEventHandler.cpp", "ported": ["CPythonPlayerEventHandler::OnMove", "CPythonPlayerEventHandler::OnMoving", "CPythonPlayerEventHandler::OnStop"], "deleted": ["net_play.gd _tick_on_waiting idle FUNC_WAIT resend"], "divergent": ["CPythonPlayerEventHandler::OnWaiting (walking-but-stalled path not ported)"], "needs_live": [], "tests": ["project/netplay_test.gd PASS", "project/keyboard_motion_timeline_test.gd PASS", "project/movement_parity_test.gd PASS", "project/player_move_test.gd PASS"]} {"time":"2026-09-22T12:00:00Z","event":"dead_code_removal","deleted_system_files":106,"deleted_tests":106,"lines_removed":36221,"manifest_refs_removed":76,"contracts_touched":18,"reason":"project/*_system.gd reachable only from their own test_*_parity.gd; not implementations of 40250 behavior (SKILL.md: unreachable code is not evidence)","tests":["full headless Godot suite: 192 PASS, 7 FAIL and 5 render tests hanging in headless, all identical on the pre-deletion tree"]} +{"time":"2026-09-22T13:00:00Z","event":"port_map_rebaseline","unit":"UserInterface/PythonPlayerEventHandler.cpp","reset_to_todo":["CPythonPlayerEventHandler::GetSingleton","CPythonPlayerEventHandler::~CPythonPlayerEventHandler","CPythonPlayerEventHandler::OnMoving","CPythonPlayerEventHandler::OnMove","CPythonPlayerEventHandler::OnStop","CPythonPlayerEventHandler::CPythonPlayerEventHandler"],"reason":"new mirror architecture owns the CPythonPlayerEventHandler singleton in native code; prior GDScript counterparts are migration sources, not final mirror implementations","tests":["port_map.py check"]} diff --git a/audit/port-map/UserInterface/PythonPlayerEventHandler.cpp.json b/audit/port-map/UserInterface/PythonPlayerEventHandler.cpp.json index 3e0e9faf..41feea45 100644 --- a/audit/port-map/UserInterface/PythonPlayerEventHandler.cpp.json +++ b/audit/port-map/UserInterface/PythonPlayerEventHandler.cpp.json @@ -7,12 +7,10 @@ ], "functions": { "CPythonPlayerEventHandler::GetSingleton": { - "status": "N_A", - "note": "singleton/ctor/dtor plumbing; NetPlay is a scene node" + "status": "TODO" }, "CPythonPlayerEventHandler::~CPythonPlayerEventHandler": { - "status": "N_A", - "note": "singleton/ctor/dtor plumbing; NetPlay is a scene node" + "status": "TODO" }, "CPythonPlayerEventHandler::OnClearAffects": { "status": "TODO" @@ -31,23 +29,13 @@ "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" - ] + "status": "TODO" }, "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" + "status": "TODO" }, "CPythonPlayerEventHandler::OnStop": { - "status": "PORTED", - "impl": [ - "project/net_play.gd:_on_anim_state" - ] + "status": "TODO" }, "CPythonPlayerEventHandler::OnWarp": { "status": "TODO" @@ -71,8 +59,7 @@ "status": "TODO" }, "CPythonPlayerEventHandler::CPythonPlayerEventHandler": { - "status": "N_A", - "note": "singleton/ctor/dtor plumbing; NetPlay is a scene node" + "status": "TODO" }, "CPythonPlayerEventHandler::CNormalBowAttack_FlyEventHandler_AutoClear::OnSetFlyTarget": { "status": "TODO" diff --git a/audit/remediation-roadmap.md b/audit/remediation-roadmap.md index 5d054cbc..90cbbd8a 100644 --- a/audit/remediation-roadmap.md +++ b/audit/remediation-roadmap.md @@ -20,9 +20,12 @@ python3 .agents/skills/metin2-40250-parity-audit/scripts/port_map.py queue --lim | 2R | 资源包盘点:EPK 压缩/加密类型、密钥来源、覆盖顺序、路径大小写、移动端交付 | 下一步(可与 2A 并行) | | 2P | CPython 2.7.18 编进 libmtgodot,五个平台各自 `pyconfig.h` 并分别验证 | 2A 之后(Android 独立程序已验证 b02c49bb) | | 2D | 数据源切到 40250:msm 路径、proto(TEA + `TItemTable`)、严格资源测试、资源根 | proto 在 2A 之后,资源根在 2R 之后 | -| 2V | 最小纵向切片:PythonLauncher → system.py → wndMgr → 登录/选角 → GamePhase → 本地移动 | 需要 2A、2P、2D | -| 2 | 其余 P0 角色/移动单元,按 `#include` 依赖拓扑移植 | 2V 之后 | -| 3 | P1 战斗/技能 → P2 游戏阶段封包 → P3 物品;Python 层按 2P 的结论进行 | 未开始 | +| 2V0 | UI 壳:真实 PythonLauncher/wndMgr/grp/app + platform UI,桩玩法模块,显示并操作 Logo/Popup | 需要 2A、2P、root/uiscript 资源 | +| 2V1 | 登录/选角:真实 net phase、登录/选角脚本和所需模块,进入 Loading | 2V0 之后 | +| 2V2 | GamePhase:真实角色管理/主角创建/最小渲染,进入游戏并显示角色 | 2V1 和 2D 之后 | +| 2V3 | 本地移动:真实 player/input/event/movement 链,输入、位置和移动封包由新路径负责 | 2V2 之后 | +| 2 | 2V3 未覆盖的其余 P0 角色/移动单元,按 `#include` 依赖拓扑移植 | 2V3 之后 | +| 3 | 剩余 Python 模块和窗口 → P1 战斗/技能 → P2 游戏阶段封包 → P3 物品 | 未开始 | | 4 | `NEEDS_LIVE` 真服验证批 | 未开始 | ## 批次 2 单元(顺序由 2A 的依赖图重新生成;下表仅为候选) diff --git a/docs/PORT-PLAN.md b/docs/PORT-PLAN.md index f35f0ab7..f64a8ad5 100644 --- a/docs/PORT-PLAN.md +++ b/docs/PORT-PLAN.md @@ -27,7 +27,7 @@ metin2-client 是 40250 Windows 客户端的跨平台版本(macOS / Windows / | 2026-09-22 | 数据源统一改为 40250(`Client/Eternexus` 的 root/uiscript/locale,以及 `Client/pack` 的资源),**不再用**仓库里 m2dev 版本的 `assets/root`、`assets/uiscript`、`assets/locale` | 两者有实质差异,见第 5 节 | | 2026-09-22 | 现有 `project/ui/*.gd`(60 个文件,依据的是错误版本的脚本)不再维护,由 Python 层取代 | — | | 2026-09-22 | 照抄时保持 40250 的**宽度和溢出语义**(Win32 下 `long` 为 32 位),序列化结构用定宽类型并 `static_assert` 大小 | 64 位平台 `long` 是 8 字节,机械照抄会破坏 proto/封包/EPK 布局和 TEA | -| 2026-09-22 | 迁移期新旧路径共存,新路径接通运行后才在同一提交删除旧逻辑;先做最小纵向切片 2V | `main` 始终可玩;运行时不可达的新代码不算实现 | +| 2026-09-22 | 迁移期新旧路径共存,新路径接通运行后才在同一提交删除旧逻辑;纵向切片拆成 2V0–2V3 | `main` 始终可玩;运行时不可达的新代码不算实现 | ## 3. 三个层和代码位置 @@ -51,13 +51,15 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次 - 40250 调用平台类的地方,调用 `extension/src/platform/` 下同名的适配接口,ported 代码里不直接写 Godot 调用。 - 40250 的单例(`CPythonPlayer`、`CPythonCharacterManager`、`CPythonNetworkStream`)归扩展所有,GDScript 不持有玩法状态。 -- **保持 40250 的宽度和溢出语义,不是机械保留 C++ 类型名。** 40250 是 Win32(ILP32/LLP64):`long`/`unsigned long` - 是 32 位,指针是 32 位。64 位的 macOS/Linux/Android/iOS 上 `long` 是 64 位,照抄会改变结构大小、TEA 的读取宽度和 - 溢出行为。统一由 `port/common/Win32Types.h`(批次 2A)把 `LONG`/`DWORD`/`long` 等映射到 `int32_t`/`uint32_t`: +- **保持 40250 的宽度和溢出语义,不是机械保留 C++ 类型名。** 40250 可执行文件目标是 32 位 Win32(ILP32): + `long`/`unsigned long` 和指针都是 32 位;移植目标则可能是 LP64 或 LLP64。`port/common/Win32Types.h`(批次 2A) + 只为 `BYTE`/`WORD`/`DWORD`/`LONG`/`BOOL`/`UINT` 等 Win32 **标量别名**提供定宽定义,不能重定义 C++ 关键字 `long`: - 凡是参与序列化的结构(proto 记录、封包、EPK 索引、msa/msm 二进制)一律用定宽类型,`#pragma pack` 与 40250 相同, 并对 40250 已知大小加 `static_assert(sizeof(...) == N)`(如 `TItemTable` == 156); - - 纯计算里的 `long` 如依赖 32 位回绕,也换成 `int32_t`; - - 指针存进 `DWORD` 的写法(句柄、`SetUserData` 等)改成 `uintptr_t`,并在 port-map 的 `note` 里记下。 + - 纯计算里的 `long` 按语义改成 `int32_t`/`uint32_t`;依赖 32 位回绕时使用无符号运算或显式 wrapping helper,不能依赖 + C++ 有符号溢出的未定义行为; + - `HANDLE`、`HWND`、`WPARAM`、`LPARAM` 等句柄/指针类型保持指针宽度并隔离在 platform adapter;指针存进 `DWORD` + 的原写法改成 `uintptr_t`,在 port-map 的 `note` 里记录适配不变式。 - 单位保持 40250 的(`TPixelPosition` 用 cm,时间是 `ELTimer_GetMSec` 的毫秒,角度用度),只在适配层换算。 - 迁移来源:`net_play.gd`、`net_world.gd`、`game_scene.gd`、`player_controller.gd`、`entity_store.cpp`。**旧逻辑只在新路径 已接通运行时之后才删除**(见第 4 节"迁移方式"),删除与接通在同一次提交完成。 @@ -74,9 +76,12 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次 | **2R** | 资源包能力盘点:EPK 类型/密钥/覆盖顺序/路径大小写/移动端交付 | **下一步**,可与 2A 并行(只读分析 + 独立工具) | | 2P | CPython 2.7.18 编进 libmtgodot,五个平台分别配置和验证 | 2A 之后 | | 2D | 数据源切到 40250(msm 路径、proto、资源根、严格资源测试) | proto 部分在 2A 之后;资源根在 2R 之后 | -| **2V** | 最小纵向切片:`CPythonLauncher` → `system.py` → `wndMgr` → 登录/选角 → GamePhase → 本地角色移动 | 需要 2A、2P、2D;**这是第一个可运行的新架构里程碑** | -| 2 | 其余 P0 角色/移动单元,按依赖拓扑移植 | 2V 之后 | -| 3 | P1 战斗/技能 → P2 游戏阶段封包 → P3 物品;Python 层(窗口系统 → `*Module.cpp`) | 未开始 | +| **2V0** | UI 壳:真实 `PythonLauncher/wndMgr/grp/app` + platform UI,其他玩法模块用桩,显示并操作 Logo/Popup | 需要 2A、2P 和 root/uiscript 子集 | +| **2V1** | 登录/选角:真实 `net` phase、登录/选角脚本及所需模块,走到 Loading | 2V0 之后 | +| **2V2** | GamePhase:真实角色管理、主角创建和最小渲染,进入游戏并显示角色 | 2V1、2D 之后 | +| **2V3** | 本地移动:真实 player/input/event/movement 链,输入、位置和移动封包切到新路径 | 2V2 之后;**第一个可玩的新架构里程碑** | +| 2 | 2V3 未覆盖的其余 P0 角色/移动单元,按依赖拓扑移植 | 2V3 之后 | +| 3 | 剩余 Python 模块和窗口 → P1 战斗/技能 → P2 游戏阶段封包 → P3 物品 | 未开始 | | 4 | `NEEDS_LIVE` 真服验证 | 未开始 | ### 迁移方式:始终保持可玩 @@ -88,16 +93,19 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次 没有接通前,port-map 状态保持 `TODO`,`note` 写"已照抄,未接通"。 - 每个接通步骤的提交同时:切换调用方到新路径、删除被取代的旧逻辑、跑一遍能覆盖该路径的运行测试(离线 FakeClient 或真服 e2e)。 -- `main` 在任意提交上都能进游戏走动;`legacy` 路径在 2V 完成、默认值切到 `port` 并稳定后整体删除。 +- 每次接通都同时跑 `legacy` 和 `port` 两套路由的相关回归;共享 extension 的改动不能破坏默认的 legacy 路径。 +- `main` 在任意提交上都能进游戏走动;`legacy` 路径在 2V3 完成、默认值切到 `port` 并稳定后整体删除。 ### 批次 2A:基础 -1. `extension/src/port/common/`:`Win32Types.h`(`BYTE`/`WORD`/`DWORD`/`LONG`/`BOOL`/`UINT`/`HANDLE` 等的定宽映射)、 +1. `extension/src/port/common/`:`Win32Types.h`(`BYTE`/`WORD`/`DWORD`/`LONG`/`BOOL`/`UINT` 等标量的定宽映射; + `HANDLE`/窗口句柄/消息参数另用指针宽度的平台类型)、 40250 用到的 Win32/CRT 宏和函数(`ZeroMemory`、`_snprintf`、`stricmp`、`timeGetTime` 等)的最小实现、 `StdAfx.h` 等价物。 2. 参考公共头的最小闭包:从第一批要移植的单元出发(`PythonPlayerEventHandler.h` 依赖 `ActorInstance.h`、`FlyHandler.h`、 `PythonNetworkStream.h`、`InstanceBase.h`),用脚本列出 `#include` 闭包,把闭包里的头文件先照抄为可编译的声明。 -3. `port_logic` 静态库 CMake 目标,链接进 `libmtgodot`;在 macOS 和 Android 两个工具链上编译。 +3. `port_logic` 静态库 CMake 目标,链接进 `libmtgodot`;macOS、Android、iOS、Linux、Windows 五个平台分别编译。 + 某个平台的工具链暂不可用时,2A 对该平台保持 `BLOCKED`,不能用其他平台的通过结果替代。 4. `extension/src/platform/` 接口骨架:闭包里出现的平台类(`CGraphicThingInstance`、`CSoundManager` 等)只声明 40250 调用方用到的方法,先给空实现。 5. 门禁:`port/**` 下每个头文件单独编译通过(header self-containment),序列化结构的 `static_assert` 全部通过。 @@ -115,7 +123,9 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次 1. 写一个只读的扫描工具(Python,放在 `tools/`),解析所有 `.eix`,统计每个包、每种 `compressed_type` 的文件数和字节数。 2. 按统计结果决定: - 只有 `NONE/COMPRESS/SECURITY` 的包可以离线解出; - - `PANAMA`/`HYBRIDCRYPT*` 如存在,确认密钥来源:本地的 `Index`/配置,还是服务器 `GC_HYBRIDCRYPT_KEYS`/`SDB` 下发。需要服务器密钥的包,要么移植密钥链,要么在开发期用一次真服登录抓取密钥后离线解包(密钥不进仓库)。 + - `PANAMA`/`HYBRIDCRYPT*` 如存在,确认密钥来源:本地的 `Index`/配置,还是服务器 `GC_HYBRIDCRYPT_KEYS`/`SDB` 下发。 + 一次真服登录抓取仅可用于确认格式和密钥来源,不能作为构建或发布依赖;正式方案必须是可重复的运行时密钥链,或经过 + 授权、可重复并有来源清单的重新打包流程,密钥本身不进仓库。 3. 验证 `Index` 文件列出的包顺序和同名文件的覆盖优先级,与 `CEterPackManager` 一致。 4. 路径归一化:40250 在 Windows 上大小写不敏感,并把 `d:/ymir work/` 等前缀映射到包内路径。确定统一的小写化规则,扫描大小写冲突。 5. 移动端交付:决定最终形式(例如解包后重新打成我们自己的 `mtpack`,或直接在设备上读 EPK),给出 Android/iOS 的包体积、首包与按需下载的划分,以及更新方式。`MT_ASSETS` 只是开发期覆盖。 @@ -125,7 +135,11 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次 1. CPython 2.7.18 源码放进 `extension/third_party/cpython-2.7.18/`。**共用源码清单和静态模块清单**(`Modules/Setup` 中启用的 C 模块,见 `tools/py_embed_android/build-and-run.sh`);**每个平台各自一份 `pyconfig.h`**(由该平台的 configure 生成后提交,或 Windows 用 `PC/pyconfig.h`),因为它是对目标平台类型大小和系统 API 的探测结果,不能共用。已知平台差异:Android API 24 需关掉 `HAVE_LANGINFO_H`。 2. 静态链接进 `libmtgodot`,照抄 `ScriptLib/PythonLauncher.cpp`。 -3. `pack` 模块通过 `asset_io` 读取,标准库的纯 Python 文件放进资源包,由 `system.py` 自带的导入钩子加载。 +3. `pack` 模块通过 `asset_io` 读取 40250 的 root/uiscript。标准库沿用已验证的 `python27.zip + sys.path`: + - 桌面端把 zip 放在 CPython 可读的真实文件系统路径; + - Android/iOS 首次启动从应用资源复制到应用沙盒,校验提交在资源清单中的 sha256,再加入 `sys.path`; + - `zipimport` 静态编进解释器。`system.py` 的导入钩子只负责 pack 内的简单 root 模块,不能用来加载标准库 package、 + dotted import 或 `encodings`。 4. 五个平台分别验证:编译、链接无未定义符号、`Py_Initialize`、静态 C 模块逐个 `import`、在 app 进程里跑 `system.py` → `prototype.RunApp()`(对照结果:74 个模块中 66 个加载成功,引导期调用 33 个 C++ 函数)。 | 平台 | 状态 | @@ -136,6 +150,27 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次 | Linux x86_64 | 未做 | | Windows x64 | 未做(用 `PCbuild` 的源码清单和 `PC/pyconfig.h`,不走 configure) | +### 批次 2V0–2V3:纵向切片 + +2A 先根据真实 `#include` 和 Python import 生成每个切片的机器可读单元清单;清单提交到 `audit/`,进入切片前不得仍有 +“后续批次才实现”的隐式依赖。未列出的 native 模块可以使用行为可观察、会记录调用的桩,但桩函数不能计为移植完成。 + +1. **2V0 — UI 壳**:真实 `ScriptLib/PythonLauncher.cpp`、`PythonPackModule.cpp`、 + `EterPythonLib/PythonWindow*.cpp`、`PythonWindowManager*.cpp`、`PythonGraphic*Module.cpp`、 + `PythonApplicationModule.cpp` 和所需 platform UI adapter;玩法模块先用桩。验收为 app 进程内运行 `system.py`,显示并可操作 + Logo/Popup,输入、焦点、裁剪、文本和图片至少各有一个运行测试。 +2. **2V1 — 登录/选角**:加入 `PythonNetworkStream{,PhaseHandShake,PhaseLogin,PhaseSelect,PhaseLoading}.cpp`、 + `PythonNetworkStreamModule.cpp`、AccountConnector 及登录/选角脚本真实调用到的模块。验收为 FakeServer 离线完成 phase 顺序, + 再以真服 smoke 证明登录、角色列表和选择进入 Loading;未验证的服务器分支记 `NEEDS_LIVE`。 +3. **2V2 — GamePhase/角色显示**:加入 GamePhase 的最小分派闭包、`PythonCharacterManager`、`InstanceBase`、`ActorInstance` + 及角色模型/动画/资源 adapter。验收为进入游戏、创建主角并显示可辨认的静止角色;这个切片要求 2D 的 proto 和所需资源通过 + strict gate。 +4. **2V3 — 本地移动**:加入 `PythonPlayer`、`PythonPlayerInput*`、`PythonPlayerEventHandler`、 + `InstanceBaseMovement`、`ActorInstance{Motion,Event,Position,Rotation,CollisionDetection}` 的实际依赖闭包。验收为输入经过新路径 + 改变位置/朝向、发送与 40250 相同的移动封包,并在离线边界测试和真服 smoke 中通过。接通时删除对应 legacy 玩法逻辑。 + +每个切片都必须同时跑 legacy/port 双路回归;只有 2V3 稳定并把默认路由切到 `port` 后,才能删除总开关和剩余 legacy 宿主。 + ## 5. 数据源切换:m2dev assets → 40250 现在的渲染和逻辑读的是 `assets/`,这是从 m2dev 的包解出来的。2026-09-22 与 40250 `Client/Eternexus` 逐项对比的结果: @@ -160,21 +195,23 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次 2. proto 读取按 40250 移植:`EterBase/tea.cpp`、`GameLib/ItemData.h` 的 `TItemTable`、`CPythonNonPlayer` 的 mob 表,放进镜像文件(使用 2A 的定宽类型和 `static_assert`),替换 `extension/src/proto/proto.cpp` 里的 m2dev 格式。 3. 资源测试严格模式: - 统一资源环境变量:C++ 测试(`extension/CMakeLists.txt` 目前只透传 `M2_ASSETS`)和 GDScript(`MT_ASSETS`)统一读 `MT_ASSETS`,过渡期两者都透传; - - 新增 `MT_ASSETS_STRICT=1`:指定了 40250 资源时,缺文件必须失败,不能跳过; + - 新增 `MT_ASSETS_STRICT=1`:该模式下 `MT_ASSETS` 未设置、来源清单缺失、sha256 不匹配或必需文件缺失都必须失败,不能跳过; - 为 40250 的 root、locale、proto 和关键包生成来源清单(路径 + sha256),提交到 `audit/`,测试开始时核对。 -4. 资源根切换方式按 2R 的结论执行,跑渲染和解析测试,更新本节状态。 +4. 新增并在 CI/发布前强制运行 `script/run_40250_asset_gate.sh`;它设置/校验 `MT_ASSETS_STRICT=1`,运行 native proto/pack/formats + 测试和 Godot root/locale/render 测试。脚本在没有 40250 资源时必须失败,不能把跳过报告成 PASS。 +5. 资源根切换方式按 2R 的结论执行,跑严格门禁,更新本节状态。 ## 6. 进度与 port-map 基线 实时数字用 `port_map.py status` 查看。 -2A 时做一次重新基线,之后的数字才代表新架构的进度: +本次方案修订已经重置旧的 6 个完成状态;2A 完成工具和基础目标后再次确认基线: -- 之前"完成"的 6 个函数(`PythonPlayerEventHandler.cpp`)作废:OnMove/OnMoving/OnStop 的实现在 `net_play.gd`,属于迁移来源; - 3 个 `N_A`(单例、构造、析构,理由是"由 NetPlay 场景节点持有")与"单例归扩展所有"冲突。全部回退为 `TODO`。 -- 原样运行的 Python 脚本函数**不标 `N_A`**(`N_A` 只用于没有玩法语义的平台胶水)。新增状态 `RUN_AS_IS`: - `impl` 指向随包运行的原脚本,`evidence` 要求该函数所在模块在目标平台的运行证据(导入成功且被运行路径调用到)。 - 需要同步修改 `port_map.py` 和 `references/audit-schema.md`。 +- 之前"完成"的 6 个函数(`PythonPlayerEventHandler.cpp`)已作废并回退为 `TODO`:OnMove/OnMoving/OnStop 的实现在 + `net_play.gd`,属于迁移来源;3 个 `N_A`(单例、构造、析构)与"单例归扩展所有"冲突。 +- 原样运行的 Python 脚本函数**不标 `N_A`**(`N_A` 只用于没有玩法语义的平台胶水)。状态 `RUN_AS_IS`: + `impl` 指向随包运行的原脚本,`evidence` 要求目标平台的导入/运行证据;单元的 reference hash 与资源来源清单共同证明 + 运行的是完全相同的字节。`port_map.py`、SKILL 和 schema 已同步支持并把它计入 done。 2026-09-22 基线前的数字(仅供参考):logic 3285 个函数、python 4890、platform 2229,完成数视为 0。 diff --git a/docs/PYTHON-EMBED-EVAL.md b/docs/PYTHON-EMBED-EVAL.md index 01d69f76..1b0e0537 100644 --- a/docs/PYTHON-EMBED-EVAL.md +++ b/docs/PYTHON-EMBED-EVAL.md @@ -51,7 +51,7 @@ ### 方案 A 的平台问题 -- macOS / Linux / Windows:直接编译,无风险。 +- macOS / Linux / Windows:都需要各自编译和验证;Windows 使用 `PCbuild`/`PC/pyconfig.h`,不走 Unix configure。 - Android(arm64,NDK 27):CPython 2.7 能用 NDK 编译(python-for-android、Kivy 曾长期支持),但需要补丁: 关闭 `dlopen` 扩展、把用到的标准库 C 模块静态编进去、`pyconfig.h` 按 NDK 调整。 - iOS:同样静态链接(Kivy-ios 曾支持 2.7)。App Store 允许包内自带的解释型代码(不下载代码即可)。 @@ -72,8 +72,9 @@ extension/third_party/cpython-2.7.18/ # 静态库 ``` Godot 这边只剩一个宿主 Control:把输入转发给 `CPythonWindowManager`,每帧调用它的 Update/Render。 -`root/*.py` 和 `uiscript/` 直接从 pack 读取,和 40250 相同。port-map 中 `Client/root` 的 3350 个函数 -全部记为 `N_A`(原样运行,无需移植),python 层剩下约 1540 个 C++ 函数要移植。 +`root/*.py` 和 `uiscript/` 直接从 pack 读取,和 40250 相同。port-map 中 `Client/root` 的函数在其 +参考字节、资源清单和目标平台运行证据都成立后记为 `RUN_AS_IS`,不能记为 `N_A`;python 层剩下约 +1540 个 C++ 函数要移植。 ## Android 验证(2026-09-22,已通过) @@ -95,8 +96,10 @@ Godot 这边只剩一个宿主 Control:把输入转发给 `CPythonWindowManage ## 下一步 -1. 把 CPython 2.7.18 放进 `extension/third_party/`,用我们自己的 CMake 编译(macOS / Android / iOS 共用一份源码清单和 - `pyconfig.h`),静态链接进 `libmtgodot`; -2. `pack` 模块走 `asset_io`,标准库的纯 Python 文件放进资源包,用 `system.py` 自带的导入钩子加载; -3. 在 APK 里跑同样的引导流程(C++ 桩模块),然后按同样方法验证 iOS; -4. 之后 python 层才开始移植:`ScriptLib/PythonLauncher.cpp` → `EterPythonLib` 窗口系统 → `*Module.cpp`。 +1. 把 CPython 2.7.18 放进 `extension/third_party/`,共用源码/静态模块清单,但为 macOS、Linux、Windows、 + Android、iOS 分别生成或维护 `pyconfig.h`,静态链接进 `libmtgodot`; +2. `pack` 模块走 `asset_io`,继续负责 40250 的 root/uiscript。标准库沿用已验证的 `python27.zip + sys.path` + 方案:桌面端放在解释器可读的真实文件系统路径,移动端首次启动从应用资源复制到应用沙盒并校验 sha256;静态启用 + `zipimport`。不要依赖 `system.py` 的简单 `name + '.py'` 钩子加载带 package/dotted import 的标准库; +3. 在 APK app 进程里跑同样的引导流程(C++ 桩模块),然后分别验证 iOS、Linux 和 Windows; +4. 按 `docs/PORT-PLAN.md` 的 2V0-2V3 切片依次替换桩模块和旧运行路径。 diff --git a/project/net_play.gd b/project/net_play.gd index 886f0faf..513d8e12 100644 --- a/project/net_play.gd +++ b/project/net_play.gd @@ -830,8 +830,6 @@ 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 @@ -848,7 +846,6 @@ 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