docs(port-plan): close migration prerequisite gaps

This commit is contained in:
shen
2026-09-22 03:01:57 -07:00
parent dcefc83211
commit ae68ec5944
9 changed files with 111 additions and 65 deletions
@@ -27,13 +27,13 @@ The 40250 source is the specification. Do not design behavior; transcribe it. Co
## Code layout: mirror 40250 ## Code layout: mirror 40250
The logic layer has the 40250 structure, not a new architecture. Route, batch order and 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: one of three layers:
| Layer | 40250 units | How to port | Where | | 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/<Lib>/<File>.{h,cpp}` | | `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/<Lib>/<File>.{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/<Lib>/` (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/<Lib>/` (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`, ...) | | `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: 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 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, 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`. 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 - Keep 40250 **widths and overflow semantics**, not its C type names: the 40250 executable targets
`unsigned long` are 32-bit. Use the fixed-width types from `port/common/Win32Types.h`; every 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 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). - Keep 40250 units (`TPixelPosition` in cm, `DWORD` ms from `ELTimer_GetMSec`, degrees).
Convert to Godot space only in the adapter. 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 - 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. - `TODO` — not yet compared.
- `PORTED` — current counterpart is statement-equivalent; `impl` names it. - `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. - `ADAPTED` — platform adapter; `note` states both sides and the invariant, with a test.
- `N_A` — platform plumbing with no gameplay semantics; `note` states why. - `N_A` — platform plumbing with no gameplay semantics; `note` states why.
- `DIVERGENT` — known different and not yet fixed; `note` says how. - `DIVERGENT` — known different and not yet fixed; `note` says how.
@@ -124,8 +124,13 @@ One JSON file per 40250 source unit at `audit/port-map/<Lib>/<File>.json`:
``` ```
- Function keys are the reference's qualified names (`Class::Method`, or the free-function name). - 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`). - `status` is one of `TODO`, `PORTED`, `RUN_AS_IS`, `ADAPTED`, `N_A`, `DIVERGENT`, `NEEDS_LIVE`
- `impl` is required for `PORTED`/`ADAPTED`/`NEEDS_LIVE`; `note` is required for `ADAPTED`, `N_A`, (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. `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 - When `reference_sha256` no longer matches the file, every non-`TODO` function in the unit must be
rechecked before the hash is updated. rechecked before the hash is updated.
@@ -31,8 +31,8 @@ from pathlib import Path
import refroot # script directory is on sys.path when run directly import refroot # script directory is on sys.path when run directly
from audit_source_coverage import REFERENCE_DIRS, active_project_sources from audit_source_coverage import REFERENCE_DIRS, active_project_sources
STATUSES = ("TODO", "PORTED", "ADAPTED", "N_A", "DIVERGENT", "NEEDS_LIVE") STATUSES = ("TODO", "PORTED", "RUN_AS_IS", "ADAPTED", "N_A", "DIVERGENT", "NEEDS_LIVE")
DONE = {"PORTED", "ADAPTED", "N_A"} DONE = {"PORTED", "RUN_AS_IS", "ADAPTED", "N_A"}
PY_ROOT = "../../Client/Eternexus/root" PY_ROOT = "../../Client/Eternexus/root"
UNIT_DIRS = REFERENCE_DIRS + ("EterBase", "EterPythonLib", "ScriptLib", "SpeedTreeLib", "SphereLib", "EterImageLib") UNIT_DIRS = REFERENCE_DIRS + ("EterBase", "EterPythonLib", "ScriptLib", "SpeedTreeLib", "SphereLib", "EterImageLib")
PORT_DIR = "extension/src/port" 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: if status not in STATUSES:
print(f"ERROR {label}: {fn} has invalid status {status!r}") print(f"ERROR {label}: {fn} has invalid status {status!r}")
problems += 1 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") print(f"ERROR {label}: {fn} is {status} without impl")
problems += 1 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"): if status in {"ADAPTED", "N_A", "DIVERGENT", "NEEDS_LIVE"} and not info.get("note"):
print(f"ERROR {label}: {fn} is {status} without note") print(f"ERROR {label}: {fn} is {status} without note")
problems += 1 problems += 1
+1
View File
@@ -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-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-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-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"]}
@@ -7,12 +7,10 @@
], ],
"functions": { "functions": {
"CPythonPlayerEventHandler::GetSingleton": { "CPythonPlayerEventHandler::GetSingleton": {
"status": "N_A", "status": "TODO"
"note": "singleton/ctor/dtor plumbing; NetPlay is a scene node"
}, },
"CPythonPlayerEventHandler::~CPythonPlayerEventHandler": { "CPythonPlayerEventHandler::~CPythonPlayerEventHandler": {
"status": "N_A", "status": "TODO"
"note": "singleton/ctor/dtor plumbing; NetPlay is a scene node"
}, },
"CPythonPlayerEventHandler::OnClearAffects": { "CPythonPlayerEventHandler::OnClearAffects": {
"status": "TODO" "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." "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": { "CPythonPlayerEventHandler::OnMoving": {
"status": "PORTED", "status": "TODO"
"impl": [
"project/net_play.gd:_on_local_moved"
]
}, },
"CPythonPlayerEventHandler::OnMove": { "CPythonPlayerEventHandler::OnMove": {
"status": "PORTED", "status": "TODO"
"impl": [
"project/net_play.gd:_on_local_moved"
],
"note": "first moved frame after _was_moving=false; resets the 300ms moving window"
}, },
"CPythonPlayerEventHandler::OnStop": { "CPythonPlayerEventHandler::OnStop": {
"status": "PORTED", "status": "TODO"
"impl": [
"project/net_play.gd:_on_anim_state"
]
}, },
"CPythonPlayerEventHandler::OnWarp": { "CPythonPlayerEventHandler::OnWarp": {
"status": "TODO" "status": "TODO"
@@ -71,8 +59,7 @@
"status": "TODO" "status": "TODO"
}, },
"CPythonPlayerEventHandler::CPythonPlayerEventHandler": { "CPythonPlayerEventHandler::CPythonPlayerEventHandler": {
"status": "N_A", "status": "TODO"
"note": "singleton/ctor/dtor plumbing; NetPlay is a scene node"
}, },
"CPythonPlayerEventHandler::CNormalBowAttack_FlyEventHandler_AutoClear::OnSetFlyTarget": { "CPythonPlayerEventHandler::CNormalBowAttack_FlyEventHandler_AutoClear::OnSetFlyTarget": {
"status": "TODO" "status": "TODO"
+6 -3
View File
@@ -20,9 +20,12 @@ python3 .agents/skills/metin2-40250-parity-audit/scripts/port_map.py queue --lim
| 2R | 资源包盘点:EPK 压缩/加密类型、密钥来源、覆盖顺序、路径大小写、移动端交付 | 下一步(可与 2A 并行) | | 2R | 资源包盘点:EPK 压缩/加密类型、密钥来源、覆盖顺序、路径大小写、移动端交付 | 下一步(可与 2A 并行) |
| 2P | CPython 2.7.18 编进 libmtgodot,五个平台各自 `pyconfig.h` 并分别验证 | 2A 之后(Android 独立程序已验证 b02c49bb) | | 2P | CPython 2.7.18 编进 libmtgodot,五个平台各自 `pyconfig.h` 并分别验证 | 2A 之后(Android 独立程序已验证 b02c49bb) |
| 2D | 数据源切到 40250:msm 路径、proto(TEA + `TItemTable`)、严格资源测试、资源根 | proto 在 2A 之后,资源根在 2R 之后 | | 2D | 数据源切到 40250:msm 路径、proto(TEA + `TItemTable`)、严格资源测试、资源根 | proto 在 2A 之后,资源根在 2R 之后 |
| 2V | 最小纵向切片:PythonLauncher → system.py → wndMgr → 登录/选角 → GamePhase → 本地移动 | 需要 2A、2P、2D | | 2V0 | UI 壳:真实 PythonLauncher/wndMgr/grp/app + platform UI,桩玩法模块,显示并操作 Logo/Popup | 需要 2A、2P、root/uiscript 资源 |
| 2 | 其余 P0 角色/移动单元,按 `#include` 依赖拓扑移植 | 2V 之后 | | 2V1 | 登录/选角:真实 net phase、登录/选角脚本和所需模块,进入 Loading | 2V0 之后 |
| 3 | P1 战斗/技能 → P2 游戏阶段封包 → P3 物品;Python 层按 2P 的结论进行 | 未开始 | | 2V2 | GamePhase:真实角色管理/主角创建/最小渲染,进入游戏并显示角色 | 2V1 和 2D 之后 |
| 2V3 | 本地移动:真实 player/input/event/movement 链,输入、位置和移动封包由新路径负责 | 2V2 之后 |
| 2 | 2V3 未覆盖的其余 P0 角色/移动单元,按 `#include` 依赖拓扑移植 | 2V3 之后 |
| 3 | 剩余 Python 模块和窗口 → P1 战斗/技能 → P2 游戏阶段封包 → P3 物品 | 未开始 |
| 4 | `NEEDS_LIVE` 真服验证批 | 未开始 | | 4 | `NEEDS_LIVE` 真服验证批 | 未开始 |
## 批次 2 单元(顺序由 2A 的依赖图重新生成;下表仅为候选) ## 批次 2 单元(顺序由 2A 的依赖图重新生成;下表仅为候选)
+59 -22
View File
@@ -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 | 数据源统一改为 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 | 现有 `project/ui/*.gd`(60 个文件,依据的是错误版本的脚本)不再维护,由 Python 层取代 | — |
| 2026-09-22 | 照抄时保持 40250 的**宽度和溢出语义**(Win32 下 `long` 为 32 位),序列化结构用定宽类型并 `static_assert` 大小 | 64 位平台 `long` 是 8 字节,机械照抄会破坏 proto/封包/EPK 布局和 TEA | | 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. 三个层和代码位置 ## 3. 三个层和代码位置
@@ -51,13 +51,15 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次
- 40250 调用平台类的地方,调用 `extension/src/platform/` 下同名的适配接口,ported 代码里不直接写 Godot 调用。 - 40250 调用平台类的地方,调用 `extension/src/platform/` 下同名的适配接口,ported 代码里不直接写 Godot 调用。
- 40250 的单例(`CPythonPlayer`、`CPythonCharacterManager`、`CPythonNetworkStream`)归扩展所有,GDScript 不持有玩法状态。 - 40250 的单例(`CPythonPlayer`、`CPythonCharacterManager`、`CPythonNetworkStream`)归扩展所有,GDScript 不持有玩法状态。
- **保持 40250 的宽度和溢出语义,不是机械保留 C++ 类型名。** 40250 是 Win32(ILP32/LLP64):`long`/`unsigned long` - **保持 40250 的宽度和溢出语义,不是机械保留 C++ 类型名。** 40250 可执行文件目标是 32 位 Win32(ILP32):
是 32 位,指针是 32 位。64 位的 macOS/Linux/Android/iOS 上 `long` 是 64 位,照抄会改变结构大小、TEA 的读取宽度和 `long`/`unsigned long` 和指针都是 32 位;移植目标则可能是 LP64 或 LLP64。`port/common/Win32Types.h`(批次 2A)
溢出行为。统一由 `port/common/Win32Types.h`(批次 2A)把 `LONG`/`DWORD`/`long` 等映射到 `int32_t`/`uint32_t`: 只为 `BYTE`/`WORD`/`DWORD`/`LONG`/`BOOL`/`UINT` 等 Win32 **标量别名**提供定宽定义,不能重定义 C++ 关键字 `long`:
- 凡是参与序列化的结构(proto 记录、封包、EPK 索引、msa/msm 二进制)一律用定宽类型,`#pragma pack` 与 40250 相同, - 凡是参与序列化的结构(proto 记录、封包、EPK 索引、msa/msm 二进制)一律用定宽类型,`#pragma pack` 与 40250 相同,
并对 40250 已知大小加 `static_assert(sizeof(...) == N)`(如 `TItemTable` == 156); 并对 40250 已知大小加 `static_assert(sizeof(...) == N)`(如 `TItemTable` == 156);
- 纯计算里的 `long` 如依赖 32 位回绕,也换成 `int32_t`; - 纯计算里的 `long` 按语义改成 `int32_t`/`uint32_t`;依赖 32 位回绕时使用无符号运算或显式 wrapping helper,不能依赖
- 指针存进 `DWORD` 的写法(句柄、`SetUserData` 等)改成 `uintptr_t`,并在 port-map 的 `note` 里记下。 C++ 有符号溢出的未定义行为;
- `HANDLE`、`HWND`、`WPARAM`、`LPARAM` 等句柄/指针类型保持指针宽度并隔离在 platform adapter;指针存进 `DWORD`
的原写法改成 `uintptr_t`,在 port-map 的 `note` 里记录适配不变式。
- 单位保持 40250 的(`TPixelPosition` 用 cm,时间是 `ELTimer_GetMSec` 的毫秒,角度用度),只在适配层换算。 - 单位保持 40250 的(`TPixelPosition` 用 cm,时间是 `ELTimer_GetMSec` 的毫秒,角度用度),只在适配层换算。
- 迁移来源:`net_play.gd`、`net_world.gd`、`game_scene.gd`、`player_controller.gd`、`entity_store.cpp`。**旧逻辑只在新路径 - 迁移来源:`net_play.gd`、`net_world.gd`、`game_scene.gd`、`player_controller.gd`、`entity_store.cpp`。**旧逻辑只在新路径
已接通运行时之后才删除**(见第 4 节"迁移方式"),删除与接通在同一次提交完成。 已接通运行时之后才删除**(见第 4 节"迁移方式"),删除与接通在同一次提交完成。
@@ -74,9 +76,12 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次
| **2R** | 资源包能力盘点:EPK 类型/密钥/覆盖顺序/路径大小写/移动端交付 | **下一步**,可与 2A 并行(只读分析 + 独立工具) | | **2R** | 资源包能力盘点:EPK 类型/密钥/覆盖顺序/路径大小写/移动端交付 | **下一步**,可与 2A 并行(只读分析 + 独立工具) |
| 2P | CPython 2.7.18 编进 libmtgodot,五个平台分别配置和验证 | 2A 之后 | | 2P | CPython 2.7.18 编进 libmtgodot,五个平台分别配置和验证 | 2A 之后 |
| 2D | 数据源切到 40250(msm 路径、proto、资源根、严格资源测试) | proto 部分在 2A 之后;资源根在 2R 之后 | | 2D | 数据源切到 40250(msm 路径、proto、资源根、严格资源测试) | proto 部分在 2A 之后;资源根在 2R 之后 |
| **2V** | 最小纵向切片:`CPythonLauncher` → `system.py` → `wndMgr` → 登录/选角 → GamePhase → 本地角色移动 | 需要 2A、2P、2D;**这是第一个可运行的新架构里程碑** | | **2V0** | UI 壳:真实 `PythonLauncher/wndMgr/grp/app` + platform UI,其他玩法模块用桩,显示并操作 Logo/Popup | 需要 2A、2P 和 root/uiscript 子集 |
| 2 | 其余 P0 角色/移动单元,按依赖拓扑移植 | 2V 之后 | | **2V1** | 登录/选角:真实 `net` phase、登录/选角脚本及所需模块,走到 Loading | 2V0 之后 |
| 3 | P1 战斗/技能 → P2 游戏阶段封包 → P3 物品;Python 层(窗口系统 → `*Module.cpp`) | 未开始 | | **2V2** | GamePhase:真实角色管理、主角创建和最小渲染,进入游戏并显示角色 | 2V1、2D 之后 |
| **2V3** | 本地移动:真实 player/input/event/movement 链,输入、位置和移动封包切到新路径 | 2V2 之后;**第一个可玩的新架构里程碑** |
| 2 | 2V3 未覆盖的其余 P0 角色/移动单元,按依赖拓扑移植 | 2V3 之后 |
| 3 | 剩余 Python 模块和窗口 → P1 战斗/技能 → P2 游戏阶段封包 → P3 物品 | 未开始 |
| 4 | `NEEDS_LIVE` 真服验证 | 未开始 | | 4 | `NEEDS_LIVE` 真服验证 | 未开始 |
### 迁移方式:始终保持可玩 ### 迁移方式:始终保持可玩
@@ -88,16 +93,19 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次
没有接通前,port-map 状态保持 `TODO`,`note` 写"已照抄,未接通"。 没有接通前,port-map 状态保持 `TODO`,`note` 写"已照抄,未接通"。
- 每个接通步骤的提交同时:切换调用方到新路径、删除被取代的旧逻辑、跑一遍能覆盖该路径的运行测试(离线 FakeClient - 每个接通步骤的提交同时:切换调用方到新路径、删除被取代的旧逻辑、跑一遍能覆盖该路径的运行测试(离线 FakeClient
或真服 e2e)。 或真服 e2e)。
- `main` 在任意提交上都能进游戏走动;`legacy` 路径在 2V 完成、默认值切到 `port` 并稳定后整体删除。 - 每次接通都同时跑 `legacy` 和 `port` 两套路由的相关回归;共享 extension 的改动不能破坏默认的 legacy 路径。
- `main` 在任意提交上都能进游戏走动;`legacy` 路径在 2V3 完成、默认值切到 `port` 并稳定后整体删除。
### 批次 2A:基础 ### 批次 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` 等)的最小实现、 40250 用到的 Win32/CRT 宏和函数(`ZeroMemory`、`_snprintf`、`stricmp`、`timeGetTime` 等)的最小实现、
`StdAfx.h` 等价物。 `StdAfx.h` 等价物。
2. 参考公共头的最小闭包:从第一批要移植的单元出发(`PythonPlayerEventHandler.h` 依赖 `ActorInstance.h`、`FlyHandler.h`、 2. 参考公共头的最小闭包:从第一批要移植的单元出发(`PythonPlayerEventHandler.h` 依赖 `ActorInstance.h`、`FlyHandler.h`、
`PythonNetworkStream.h`、`InstanceBase.h`),用脚本列出 `#include` 闭包,把闭包里的头文件先照抄为可编译的声明。 `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` 等)只声明 4. `extension/src/platform/` 接口骨架:闭包里出现的平台类(`CGraphicThingInstance`、`CSoundManager` 等)只声明
40250 调用方用到的方法,先给空实现。 40250 调用方用到的方法,先给空实现。
5. 门禁:`port/**` 下每个头文件单独编译通过(header self-containment),序列化结构的 `static_assert` 全部通过。 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` 的文件数和字节数。 1. 写一个只读的扫描工具(Python,放在 `tools/`),解析所有 `.eix`,统计每个包、每种 `compressed_type` 的文件数和字节数。
2. 按统计结果决定: 2. 按统计结果决定:
- 只有 `NONE/COMPRESS/SECURITY` 的包可以离线解出; - 只有 `NONE/COMPRESS/SECURITY` 的包可以离线解出;
- `PANAMA`/`HYBRIDCRYPT*` 如存在,确认密钥来源:本地的 `Index`/配置,还是服务器 `GC_HYBRIDCRYPT_KEYS`/`SDB` 下发。需要服务器密钥的包,要么移植密钥链,要么在开发期用一次真服登录抓取密钥后离线解包(密钥不进仓库)。 - `PANAMA`/`HYBRIDCRYPT*` 如存在,确认密钥来源:本地的 `Index`/配置,还是服务器 `GC_HYBRIDCRYPT_KEYS`/`SDB` 下发。
一次真服登录抓取仅可用于确认格式和密钥来源,不能作为构建或发布依赖;正式方案必须是可重复的运行时密钥链,或经过
授权、可重复并有来源清单的重新打包流程,密钥本身不进仓库。
3. 验证 `Index` 文件列出的包顺序和同名文件的覆盖优先级,与 `CEterPackManager` 一致。 3. 验证 `Index` 文件列出的包顺序和同名文件的覆盖优先级,与 `CEterPackManager` 一致。
4. 路径归一化:40250 在 Windows 上大小写不敏感,并把 `d:/ymir work/` 等前缀映射到包内路径。确定统一的小写化规则,扫描大小写冲突。 4. 路径归一化:40250 在 Windows 上大小写不敏感,并把 `d:/ymir work/` 等前缀映射到包内路径。确定统一的小写化规则,扫描大小写冲突。
5. 移动端交付:决定最终形式(例如解包后重新打成我们自己的 `mtpack`,或直接在设备上读 EPK),给出 Android/iOS 的包体积、首包与按需下载的划分,以及更新方式。`MT_ASSETS` 只是开发期覆盖。 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`。 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`。 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++ 函数)。 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 | 未做 | | Linux x86_64 | 未做 |
| Windows x64 | 未做(用 `PCbuild` 的源码清单和 `PC/pyconfig.h`,不走 configure) | | 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 ## 5. 数据源切换:m2dev assets → 40250
现在的渲染和逻辑读的是 `assets/`,这是从 m2dev 的包解出来的。2026-09-22 与 40250 `Client/Eternexus` 逐项对比的结果: 现在的渲染和逻辑读的是 `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 格式。 2. proto 读取按 40250 移植:`EterBase/tea.cpp`、`GameLib/ItemData.h` 的 `TItemTable`、`CPythonNonPlayer` 的 mob 表,放进镜像文件(使用 2A 的定宽类型和 `static_assert`),替换 `extension/src/proto/proto.cpp` 里的 m2dev 格式。
3. 资源测试严格模式: 3. 资源测试严格模式:
- 统一资源环境变量:C++ 测试(`extension/CMakeLists.txt` 目前只透传 `M2_ASSETS`)和 GDScript(`MT_ASSETS`)统一读 `MT_ASSETS`,过渡期两者都透传; - 统一资源环境变量: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/`,测试开始时核对。 - 为 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 基线 ## 6. 进度与 port-map 基线
实时数字用 `port_map.py status` 查看。 实时数字用 `port_map.py status` 查看。
2A 时做一次重新基线,之后的数字才代表新架构的进度: 本次方案修订已经重置旧的 6 个完成状态;2A 完成工具和基础目标后再次确认基线:
- 之前"完成"的 6 个函数(`PythonPlayerEventHandler.cpp`)作废:OnMove/OnMoving/OnStop 的实现在 `net_play.gd`,属于迁移来源; - 之前"完成"的 6 个函数(`PythonPlayerEventHandler.cpp`)已作废并回退为 `TODO`:OnMove/OnMoving/OnStop 的实现在
3 个 `N_A`(单例、构造、析构,理由是"由 NetPlay 场景节点持有")与"单例归扩展所有"冲突。全部回退为 `TODO`。 `net_play.gd`,属于迁移来源;3 个 `N_A`(单例、构造、析构)与"单例归扩展所有"冲突。
- 原样运行的 Python 脚本函数**不标 `N_A`**(`N_A` 只用于没有玩法语义的平台胶水)。新增状态 `RUN_AS_IS`: - 原样运行的 Python 脚本函数**不标 `N_A`**(`N_A` 只用于没有玩法语义的平台胶水)。状态 `RUN_AS_IS`:
`impl` 指向随包运行的原脚本,`evidence` 要求该函数所在模块在目标平台的运行证据(导入成功且被运行路径调用到)。 `impl` 指向随包运行的原脚本,`evidence` 要求目标平台的导入/运行证据;单元的 reference hash 与资源来源清单共同证明
需要同步修改 `port_map.py` 和 `references/audit-schema.md`。 运行的是完全相同的字节。`port_map.py`、SKILL 和 schema 已同步支持并把它计入 done。
2026-09-22 基线前的数字(仅供参考):logic 3285 个函数、python 4890、platform 2229,完成数视为 0。 2026-09-22 基线前的数字(仅供参考):logic 3285 个函数、python 4890、platform 2229,完成数视为 0。
+11 -8
View File
@@ -51,7 +51,7 @@
### 方案 A 的平台问题 ### 方案 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 曾长期支持),但需要补丁: - Android(arm64,NDK 27):CPython 2.7 能用 NDK 编译(python-for-android、Kivy 曾长期支持),但需要补丁:
关闭 `dlopen` 扩展、把用到的标准库 C 模块静态编进去、`pyconfig.h` 按 NDK 调整。 关闭 `dlopen` 扩展、把用到的标准库 C 模块静态编进去、`pyconfig.h` 按 NDK 调整。
- iOS:同样静态链接(Kivy-ios 曾支持 2.7)。App Store 允许包内自带的解释型代码(不下载代码即可)。 - iOS:同样静态链接(Kivy-ios 曾支持 2.7)。App Store 允许包内自带的解释型代码(不下载代码即可)。
@@ -72,8 +72,9 @@ extension/third_party/cpython-2.7.18/ # 静态库
``` ```
Godot 这边只剩一个宿主 Control:把输入转发给 `CPythonWindowManager`,每帧调用它的 Update/Render。 Godot 这边只剩一个宿主 Control:把输入转发给 `CPythonWindowManager`,每帧调用它的 Update/Render。
`root/*.py` 和 `uiscript/` 直接从 pack 读取,和 40250 相同。port-map 中 `Client/root` 的 3350 个函数 `root/*.py` 和 `uiscript/` 直接从 pack 读取,和 40250 相同。port-map 中 `Client/root` 的函数在其
全部记为 `N_A`(原样运行,无需移植),python 层剩下约 1540 个 C++ 函数要移植。 参考字节、资源清单和目标平台运行证据都成立后记为 `RUN_AS_IS`,不能记为 `N_A`;python 层剩下约
1540 个 C++ 函数要移植。
## Android 验证(2026-09-22,已通过) ## Android 验证(2026-09-22,已通过)
@@ -95,8 +96,10 @@ Godot 这边只剩一个宿主 Control:把输入转发给 `CPythonWindowManage
## 下一步 ## 下一步
1. 把 CPython 2.7.18 放进 `extension/third_party/`,用我们自己的 CMake 编译(macOS / Android / iOS 共用一份源码清单和 1. 把 CPython 2.7.18 放进 `extension/third_party/`,共用源码/静态模块清单,但为 macOS、Linux、Windows、
`pyconfig.h`),静态链接进 `libmtgodot`; Android、iOS 分别生成或维护 `pyconfig.h`,静态链接进 `libmtgodot`;
2. `pack` 模块走 `asset_io`,标准库的纯 Python 文件放进资源包,用 `system.py` 自带的导入钩子加载; 2. `pack` 模块走 `asset_io`,继续负责 40250 的 root/uiscript。标准库沿用已验证的 `python27.zip + sys.path`
3. 在 APK 里跑同样的引导流程(C++ 桩模块),然后按同样方法验证 iOS; 方案:桌面端放在解释器可读的真实文件系统路径,移动端首次启动从应用资源复制到应用沙盒并校验 sha256;静态启用
4. 之后 python 层才开始移植:`ScriptLib/PythonLauncher.cpp` → `EterPythonLib` 窗口系统 → `*Module.cpp`。 `zipimport`。不要依赖 `system.py` 的简单 `name + '.py'` 钩子加载带 package/dotted import 的标准库;
3. 在 APK app 进程里跑同样的引导流程(C++ 桩模块),然后分别验证 iOS、Linux 和 Windows;
4. 按 `docs/PORT-PLAN.md` 的 2V0-2V3 切片依次替换桩模块和旧运行路径。
-3
View File
@@ -830,8 +830,6 @@ func _entity_name(e: Dictionary) -> String:
# --- movement ---------------------------------------------------------------- # --- movement ----------------------------------------------------------------
# `pc.moved` 在平移的每一帧发。第一帧 = OnMove,之后各帧 = OnMoving。 # `pc.moved` 在平移的每一帧发。第一帧 = OnMove,之后各帧 = OnMoving。
# 40250: CPythonPlayerEventHandler::OnMove
# 40250: CPythonPlayerEventHandler::OnMoving
func _on_local_moved(pos: Vector3) -> void: func _on_local_moved(pos: Vector3) -> void:
if _observer_mode or client == null or not client.is_in_game(): if _observer_mode or client == null or not client.is_in_game():
return return
@@ -848,7 +846,6 @@ func _on_local_moved(pos: Vector3) -> void:
_send_state(FUNC_MOVE, 0, pos) _send_state(FUNC_MOVE, 0, pos)
_last_moving_sent_t = now _last_moving_sent_t = now
# 40250: CPythonPlayerEventHandler::OnStop
func _on_anim_state(state: String) -> void: func _on_anim_state(state: String) -> void:
if _observer_mode: if _observer_mode:
return return