Files
mtgodot-poc/.agents/skills/metin2-40250-parity-audit/SKILL.md
T
shenleiandClaude Opus 5.5 d16fabf61c 2V2-a: game.GameWindow opens after Loading and sends CG_ENTERGAME
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>
2026-09-23 16:56:23 +09:00

13 KiB

name, description
name description
metin2-40250-parity-audit 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.