The 2V0 stub modules now carry the 40250 module constants (copied from each module's init function by stub_constants.py) and return zero values shaped like each reference function's Py_BuildValue, so interfaceModule.MakeInterface runs through (uiSafebox no longer divides by a zero SAFEBOX_SLOT_Y_COUNT). UserInterface/StdAfx.h includes Locale.h before GameType.h as the 40250 PCH does; the reversed order dropped GameType.h's ENABLE_NEW_EQUIPMENT_SYSTEM branches. port.login_flow's fake server checks CG_ENTERGAME; port.login_live reaches the GameWindow on 192.168.21.203. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
13 KiB
Markdown
181 lines
13 KiB
Markdown
---
|
|
name: metin2-40250-parity-audit
|
|
description: Port, fix, or verify 1:1 parity between the Metin2 40250 Windows C++ client and this Godot client. Use for any 40250 comparison, 1:1 parity, missing original-client behavior, parity regression, or continuation of the client port/audit. Work is done by transliterating 40250 source units, not by hunting behavior differences.
|
|
---
|
|
|
|
# Metin2 40250 1:1 port
|
|
|
|
## Principle
|
|
|
|
metin2-client is the cross-platform build of the 40250 Windows client. Apart from rendering and
|
|
platform APIs, every gameplay algorithm, branch, constant, state order, timing source, data
|
|
source, protocol side effect and cleanup path must be the 40250 one.
|
|
|
|
The 40250 source is the specification. Do not design behavior; transcribe it. Consequences:
|
|
|
|
- **The unit of work is a 40250 source unit** (one `.cpp` file, or one cohesive function group in a
|
|
very large file such as `PythonNetworkStreamPhaseGame.cpp`), not a behavior branch. A round reads
|
|
the whole unit and fixes every divergence in it.
|
|
- **Current-client logic with no 40250 counterpart is a defect**, not an adaptation: delete it
|
|
(e.g. an invented `clampf(0.25, 3.0)` on move speed, a periodic idle `FUNC_WAIT` resend, a
|
|
client-side boss AI). "The reference has no mechanism for X, so we added one" is never a
|
|
platform adaptation.
|
|
- **Unreachable code is not an implementation, and tests of it are not evidence.** Code that the
|
|
runtime never loads (e.g. a `*_system.gd` referenced only by its own `test_*_parity.gd`) must be
|
|
deleted or wired in by porting the real 40250 caller, never cited as parity.
|
|
|
|
## 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, 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`, `EterBase` pure units (`tea`, `lzo`, `cipher`, `Random`, `Stl`, `Timer`, `Poly/`) (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` 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, `EterBase` file/OS units (`_ETERBASE_PLATFORM` in `port_map.py`) | 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:
|
|
|
|
- Port a whole unit into its mirror file. Where 40250 calls a platform class (`CGraphicThingInstance`,
|
|
`CStateManager`, `CSoundManager`, ...), call a same-named adapter interface declared under
|
|
`extension/src/platform/`; never inline Godot calls into ported logic.
|
|
- 40250 singletons (`CPythonPlayer::Instance()`, `CPythonCharacterManager`, `CPythonNetworkStream`)
|
|
stay singletons owned by the extension. GDScript does not hold gameplay state.
|
|
- GDScript (`net_play.gd`, `net_world.gd`, `game_scene.gd`, `player_controller.gd`) and
|
|
`extension/src/net/entity_store.cpp` are migration sources. Old and new paths coexist behind a
|
|
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: 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)`. 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
|
|
units depending on it start only after it lands. Separate `.cpp` files do not make units independent.
|
|
- `port_map.py check` prints `LEGACY` for a `logic` function whose `impl` is outside its mirror file.
|
|
|
|
Mark each ported function with a one-line tag at its definition:
|
|
`// 40250: CInstanceBase::SetMoveSpeed`. Tag real counterparts only. In mirror files the tag is
|
|
optional, because the file and method name already map one to one.
|
|
|
|
## Round workflow
|
|
|
|
1. **Pick a unit** from the current batch in `audit/remediation-roadmap.md`, or from
|
|
`port_map.py queue` (logic layer by default, 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`.
|
|
3. **Copy the unit into its mirror file** and, for each reference function, compare it with the
|
|
old counterpart statement by statement: preconditions and early returns, branches, formulas,
|
|
constants and units, state writes and their order, timing/event source, data source
|
|
(proto/msa/msm/txt, never hardcoded), packets sent, cleanup. Then:
|
|
- keep the 40250 version; record each old behavior that differed as a divergence found;
|
|
- when the ported path is wired into the runtime, delete the old logic it replaces and rewire
|
|
callers in the same commit (see `docs/PORT-PLAN.md`, "迁移方式");
|
|
- mark `N_A` only for pure platform plumbing (D3D state, Python binding glue, Win32), with a reason.
|
|
4. **Test what changed**: a focused test using the reference's own boundary values for each changed
|
|
formula/branch (fails before, passes after). Do not write tests for unchanged or trivial code.
|
|
Run only the tests touching the changed files; the full suite runs once per batch.
|
|
5. **Record**: update the unit's port-map entry (per-function status) and append one line to
|
|
`audit/history.jsonl`. Touch `audit/manifest.json` and `audit/contracts/*.md` only when a
|
|
contract's status changes. Do not write narrative into `remediation-roadmap.md`; only reorder or
|
|
tick the queue.
|
|
6. **Commit** the unit (code + test + port-map) as one commit.
|
|
|
|
A round that finds no divergence still records the per-function result so the unit is never re-read
|
|
without a hash change.
|
|
|
|
## Port-map (dedup and coverage)
|
|
|
|
`audit/port-map/<Lib>/<File>.json`, one file per reference source unit (per-file to avoid merge
|
|
conflicts between parallel rounds). Schema in `references/audit-schema.md`. Function statuses:
|
|
|
|
- `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.
|
|
- `NEEDS_LIVE` — code ported, but equivalence depends on real-server/Windows evidence.
|
|
|
|
A function is revisited only if its reference or implementation hash changed or new evidence
|
|
contradicts its status. This replaces the old per-branch "do not repeat" prose.
|
|
|
|
## Priority
|
|
|
|
Order the queue by what a player sees, P0 first:
|
|
|
|
1. Actor/movement: `InstanceBase*.cpp`, `GameLib/ActorInstance*.cpp`, `PythonPlayer*.cpp`,
|
|
`PythonPlayerEventHandler.cpp`, `PythonCharacterManager*.cpp`.
|
|
2. Combat/skill: `ActorInstanceBattle.cpp`, `ActorInstanceAttach*`, `FlyingObject*`,
|
|
`PythonPlayerSkill.cpp`, `PythonSkill.cpp`.
|
|
3. Game-phase packet handlers: `PythonNetworkStreamPhaseGame*.cpp`, `PythonNetworkStreamEvent.cpp`.
|
|
4. Items/inventory/UI data: `PythonItem.cpp`, `PythonExchange.cpp`, `PythonSafeBox.cpp`, `PythonShop.cpp`,
|
|
then the Python UI scripts.
|
|
5. World/map/terrain/effects, then audio, weather, platform.
|
|
|
|
Login/phase edge cases whose only open item is real-server evidence are `NEEDS_LIVE` and go to a
|
|
separate live-verification batch; do not spend porting rounds on them.
|
|
|
|
## Parallel rounds
|
|
|
|
Independent units may run in parallel, one git worktree per unit. Two parallel rounds must not
|
|
edit the same implementation file; if a unit's counterpart overlaps a running round, queue it.
|
|
Port-map files are per unit and `history.jsonl` is append-only, so merges are mechanical.
|
|
|
|
## Contracts and verification states
|
|
|
|
`audit/manifest.json` contracts remain the behavior-level roll-up (status meanings and gates in
|
|
`references/audit-schema.md`). A contract may reach `STATIC_VERIFIED` only when every reference
|
|
unit it lists has no `TODO`/`DIVERGENT` functions, and `TEST_VERIFIED` when its tests also pass.
|
|
Never cite a test of unreachable code, and never mark visual, timing-feel, driver or Windows-only
|
|
behavior verified from code inspection alone.
|
|
|
|
## Tools
|
|
|
|
- `scripts/port_map.py status | queue [--layer logic|python|platform] | init <unit> | show <unit> | check`
|
|
— function inventory (active `.vcxproj` sources + the 40250 Python root), progress per library and
|
|
per layer, and tag/port-map/layout consistency. Run `check` before every commit that touches
|
|
`audit/port-map/`.
|
|
- `scripts/port_copy.py copy <Lib/File>... | diff [<Lib/File>...]` — copy 40250 files into the
|
|
`extension/src/port/` mirror with mechanical edits only (CP949→UTF-8, LF, include-path case); `diff`
|
|
lists the manual `// PORT:` edits.
|
|
- `scripts/platform_stub.py list | gen [<Lib/File.h>...] [--force]` — generate the empty platform
|
|
skeleton `extension/src/platform/<Lib>/<File>.cpp` for platform-layer mirror headers from clang's AST
|
|
(needs `script/port_gate.sh macos` for compile commands); never overwrites without `--force`.
|
|
`grep -r MT_PLATFORM_STUB extension/src/platform` lists the platform functions still unimplemented.
|
|
- `scripts/stub_constants.py [--check]` — regenerates the 2V0 stub modules' constants
|
|
(`platform/ScriptLib/GameplayModuleConstants.cpp`) and stub return shapes (`GameplayModules.cpp`)
|
|
from the 40250 module sources; rerun when a stub module is ported for real and removed.
|
|
- `scripts/port_deps.py closure <file> | slices [--write] | order [--write]` — `#include` graph with the
|
|
implicit edges each library's `StdAfx.h` supplies; writes the 2V0-2V3 unit lists and the batch-2
|
|
topological order to `audit/slices/`. Regenerate after changing a slice definition.
|
|
- `scripts/py_embed_spike.py` (Python 2.7) — runs the 40250 `system.py` bootstrap with stub native
|
|
modules; used by the embedded-Python evaluation.
|
|
- `scripts/audit_ledger.py refresh --write | report --write | validate` — run once per batch, not
|
|
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
|
|
contracts.
|
|
- Reference root: `MT_40250_SOURCE` env var, else `reference_root` in `audit/manifest.json`
|
|
(relative to the repo root).
|
|
|
|
## Completion report
|
|
|
|
Per round: unit, functions ported/fixed/deleted/marked `N_A`, divergences found (one line each:
|
|
reference vs old current behavior), tests run, commit hash, remaining `TODO`/`DIVERGENT`/`NEEDS_LIVE`
|
|
in the unit. Keep it short.
|