python_3d_surface.gd turns RenderGame's recorded draws into meshes under a
camera read back from SetPerspective; textures come from the 40250 packs.
GetFarClip and the CMapManager ctor ported verbatim. Until the environment
is ported, lit draws with no light/ambient are shown unshaded (stand-in).
script/python_game_render_test.sh runs system.py login -> select -> game
against port_fake_login_server (or --live via MT_LIVE_*) and screenshots.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EterLib/StateManager.cpp and GrpBase.cpp ported verbatim. CGraphicDevice::Create builds a platform
IDirect3DDevice8 that keeps the device state and records every Draw* call as a Render3DDraw
(vertices decoded by FVF, strips/fans expanded, textures, world/view/proj, blend/depth/light state).
The 40250 camera (__UpdateCamera, SetCenterPosition, ...) and the Process order
(__UpdateCamera -> OnCameraUpdate -> OnUIUpdate -> Begin/SetInterfaceRenderState/OnUIRender/End)
are in place; CScreen Begin/End, CPythonGraphic render states and CCullingManager::Process matrix
updates are verbatim. port.login_flow asserts a textured, lit character draw under the RH
perspective with every vertex on screen; offline and live pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Implement the Granny SDK 2.11 API the client uses on top of libgr2
(platform/EterGrnLib/GrannyRuntime.cpp, shim granny.h declared from the
SDK header): file sections, type-definition vertex conversion, material
textures by Usage, mesh/bone bindings, deformer, control clock/loop/ease
and weighted SRT sampling. Port EterGrnLib (LODController, Material, Mesh,
Model, ModelInstance*, Motion, Thing, ThingInstance, Util) plus EterLib
GrpObjectInstance/GrpCollisionObject/CollisionData verbatim and drop their
platform skeletons and pending stand-ins; register the gr2 resource
factory; vertex/index buffers are CPU-side until the 2V2-e render adapter.
app.UpdateGame/RenderGame now run 40250's bodies, reaching the
CPythonApplication members through their singletons (the platform has no
app object); CPythonGraphic joins GameSingletons in member order, and
GetMousePosition reads the host cursor.
port.login_flow asserts the main instance's PART_MAIN model has meshes,
its diffuse image loaded and the WAIT motion moves the bones between
frames; offline and live pass, port_gate macos PASS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port the ActorInstance*/InstanceBase* siblings plus GameUtil, FlyTarget,
PhysicsObject, GameEventManager, WeaponTrace, MapUtil and lineintersect_utils
verbatim and drop their pending stand-ins; CGameEventManager and CPythonSystem
join PythonBoot in CPythonApplication member order.
Root cause of the intermittent Loading-phase header 7: TPlayerSkill::tNextRead
is time_t, 32-bit in 40250 (_USE_32BIT_TIME_T) but 64-bit on LP64, so
SKILL_LEVEL (76) was read as 2551 bytes instead of 1531. Packet.h now uses
LONG with a static_assert, and the fake server sends SKILL_LEVEL in Loading.
The fake server's CHARACTER_ADD now uses CHAR_TYPE_PC; port.login_flow
asserts the main instance exists with race 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PythonCharacterManagerModule/PythonCharacterModule ported verbatim; the
2V0 chr/chrmgr stubs are gone and initchr/initchrmgr run in 40250 order.
The registration chain they need (RaceData/RaceDataFile/RaceManager/
RaceMotionData, GameType, AttributeData/AttributeInstance, TextFileLoader,
Util, Camera/CameraProcedure) is ported; CRaceManager is created in
PythonBoot. port.login_flow asserts race 0 has its wait motions and no
unhandled packet header; port.login_live 8/8 against 192.168.21.203.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PythonNetworkStreamPhaseGame/PhaseGameActor/PhaseGameItem/Command,
NetworkActorManager and PythonChat/PythonChatModule are ported verbatim and
dispatched at runtime; the 2V0 chat stub module is removed and initChat()
runs before initnet() as in 40250. InstanceBase, PythonCharacterManager and
GameLib ActorInstance/RaceData/RaceManager compile but stay TODO until
2V2-c/d wire them.
Units not ported yet get their singletons from PythonBoot's GameSingletons
(CPythonApplication member order) and platform/pending stand-ins generated
by platform_stub.py pending; const char* stand-ins return "".
port.login_flow's fake server now sends the CInputLogin::Entergame order
(actor insert, NPC positions, PHASE_GAME, land list, time, channel, greet
notice, ping) and a TraceError observer asserts no unknown packet header.
port.login_live passes against the real server for 10 s of game phase.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
port.login_live ran twice against the live server: auth handshake, key
agreement and LOGIN3, game LOGIN2, the character list, the DirectEnter
reconnect, CHARACTER_SELECT and GC_MAIN_CHARACTER -> LoadData +
CG_CLIENT_VERSION, with no unknown packets on the stream. The loading
burst after the main character, ENTERGAME and failure popups stay
NEEDS_LIVE for 2V2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
port_login_flow_test gains a `live` mode that runs the same script path
against a real 40250 server and checks the client's own state: the
character list, then LoadingWindow.LoadData from GC_MAIN_CHARACTER (which
CPythonNetworkStream answers with CG_CLIENT_VERSION), exiting before the
game phase. Host, ports, account and slot come only from MT_LIVE_* env
vars, are handed to Python as objects and never printed; without them
the test skips. Not yet run live: the server was unreachable today.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
port.login_flow drives the real 40250 root scripts through the ported
CPythonNetworkStream and CAccountConnector against a loopback auth + game
server built from net/classic (wire layouts, sequence table, cipher) that
answers like the 40250 desc.cpp/input*.cpp: handshake, key agreement,
LOGIN3/AUTH_SUCCESS, LOGIN2, the character list, DirectEnter reconnect,
CHARACTER_SELECT, the loading packets and CLIENT_VERSION, with every
sequence byte checked.
- NetStream::ActivateCipher decrypts ciphertext already buffered behind
KEY_AGREEMENT_COMPLETED (PORT)
- CPythonApplication is final (PORT): no application object exists on the
platform yet, so its virtual overrides must be called directly
- app.GetTime (GetGlobalTime) and camera/center-position stubs
- SendClientVersionPacket verbatim in the PhaseGame stand-in
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Port PythonNetworkStream with its HandShake/Login/Select/Loading/Offline
phases, AccountConnector and PythonNetworkStreamModule verbatim; the net
stub goes. PythonBoot creates the network stream, account connector,
player, flying manager and effect manager like CPythonApplication, and
app.Create/Process drive CNetworkDevice and both connectors.
Packet.h long fields become 32-bit LONG; port.packet checks 38
login/select/loading packet sizes against wire_classic.h. Unported units
in the link closure live under platform/pending/ as stand-ins. The macOS
gate now builds all and finds iconv.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NetStream/NetAddress/NetDevice/NetPacketHeaderMap and EterBase/cipher.cpp
are mechanical copies. The winsock shim maps Winsock onto BSD sockets and
keeps the Winsock semantics CNetworkStream relies on: select ignores nfds,
a socket whose connect failed is not writable (40250 times out instead),
EINPROGRESS reads as WSAEWOULDBLOCK, and SIGPIPE is ignored.
port.net covers loopback connect, send/recv, peer close, the refused
connect timeout and the client/server key agreement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CGraphicText, CGraphicFontTexture and CGraphicDib are the 40250 bodies
verbatim; CGraphicTextInstance is verbatim except that its D3D draws become
UI render commands (one image quad per glyph, Bar for cursor/underline).
- platform/Win32Gdi.cpp: the GDI subset those files call, over vendored
FreeType 2.13.3, following GDI's LOGFONT cell height, win metrics and gasp
rules; system font lookup with substitute and CJK fallback faces
- Win32Crt: MultiByteToWideChar / WideCharToMultiByte (UTF-8 and 1252 by
hand, other code pages through iconv)
- CGraphicImageTexture memory textures: glyph pages reach Godot as
"mem:<id>@<revision>"; Metin2PythonHost.memory_texture + Canvas cache
- Util.cpp code page / font face table, SetDefaultCodePage at boot,
DefaultFont_* block, fnt resource type, GetMaxTextureWidth/Height
- TextTag, StringCodec, Arabic, Vietnamese copied from 40250
- tests: port.text (Tahoma 12 metrics, alignment, CP949), port.app_loop
and python_host_test.gd now check glyph quads instead of text commands
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- system.py runs app.Loop() on the script fiber; each UIUpdate is one
CPythonApplication::Process(). Logo -> Login, popup ESC/click and
app.Exit teardown (launcher cleared first, as in 40250 Main) work.
- Image commands are base-texture quads with UVs (a .sub shares its parent
texture); ImageInstance/ExpandedImageInstance OnRender and SetRenderingRect
ported verbatim, so scale/rotation/rendering rect are drawn.
- CGraphicBase::GetColor / grp.GenerateColor ported (ThinBoard background).
- port.app_loop test, python_host_test app-loop checks, MT_TEST_MODE=python_ui.
- TODO: .fnt/CGraphicText font pipeline, ExpandedImage blend modes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CPython opens the stdlib zip with its own stdio, so it needs a real path.
On the desktop res:// is a directory and globalize_path() is enough; in an
exported build res:// is inside the PCK, which nothing outside Godot reads.
make_stdlib_zip.py now also writes <out>.sha256
mtpython_stdlib_project copies python27.zip + .sha256 into project/
(gitignored), and both export presets' include_filter lists them, so
they enter the PCK — checked with --export-pack
extension/src/python_stdlib.{h,cpp} (Metin2Python.stdlib_path in
GDScript): reads the bytes out of the PCK, writes
user://python27.zip.part, hashes the file as written against the
shipped digest, then renames. A sandbox copy is reused only when it
matches, so a first start killed mid-copy and a zip replaced by a new
build both re-stage instead of feeding zipimport a torn file.
PythonHost::SetDefaultStdLibPath / DefaultStdLibPath() answer with that
path ($MT_PYTHON_STDLIB still wins). python_stdlib.cpp is the only unit
that knows res:// / user://; port_platform stays godot-free.
Also fixes the Windows gate, which step 3b broke: without mtpython the
MinGW build compiled UserInterface/StdAfx.h (it includes ScriptLib/StdAfx.h,
as the original PCH does). port/CMakeLists.txt excludes that header and
PythonPackModule.cpp with ScriptLib, and platform/CMakeLists.txt excludes
platform/ScriptLib/ the same way.
Not done: on-device staging. The macOS test drives the mobile path with
force_stage=true, and nothing in the app boot calls it yet — the
interpreter only starts in the process with 2V0.
gates: python_stdlib_test.gd PASS (in-place path, staging, sha256, bytes
equal res://, no .part, ZIPReader finds encodings/__init__.py, no re-copy
on a second call, corrupted copy re-staged) · macOS ctest 27/27 ·
mingw + android + ios port_platform compile clean · port_map.py check 0
errors · key leak check 8/8 none.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
step 3a — 标准库:
tools/py_embed/make_stdlib_zip.py packs the vendored Lib/ into
<build>/python27.zip (397 modules, 6876547 bytes, ZIP_STORED — the zlib
module is trimmed out of our CPython, so zipimport cannot inflate).
The mtpython_stdlib target builds it; py_embed_test and the launcher test
both run against it.
platform/ScriptLib/PythonHost sets the embedding flags before
Py_Initialize and, after it, puts the zip on sys.path and re-registers
encodings.search_function with codecs — 2.7 builds the codec registry
once, at startup, so replacing sys.path afterwards needs it back.
step 3b — pack 模块:
UserInterface/PythonPackModule.cpp copied verbatim; packExist/packGet/
initpack read root/ and uiscript/ through CEterPackManager, i.e. through
asset_io. The CHINA_CRYPT_KEY block in that unit is CAccountConnector's
and is kept #if 0 until the login slice (2V1). UserInterface/StdAfx.h is
a documented partial mirror — the original is the whole client's PCH.
The launcher test now runs initpack() as RunMainScript does (its first
line), reads system.py / uiscript / locale text back out of the packs, and
confirms cp1252 decoding works off the zip. system.py still stops at
"No module named app" — the C++ modules are the 2V0 slice.
gates: port_map.py check 0 errors · key leak check 8/8 none · macOS ctest
27/27 · android + ios port_platform compile clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Link mtpython into port_logic and copy 40250's ScriptLib launcher across:
PythonLauncher, PythonUtils and PythonMarshal, plus the 12 forwarding headers
that make its `#include <Python-2.7/*>` lines compile unchanged. symtable.h
forward-declares mod_ty rather than including Python-ast.h, whose one-word
macros (Delete, Module, Set, Print) collide with EterLib/Pool.h — the same
clash 40250 sidestepped by commenting out PySymtable_Build.
Two `// PORT:` edits: `unsigned char(val)` is an MSVC extension (PythonUtils.cpp,
twice), and PyObject_AsCharBuffer wants a Py_ssize_t* (TraceFunc).
port_python_launcher_test drives the launcher in the order UserInterface.cpp
does (:241-434): Create(), __DEBUG__/__COMMAND_LINE__, TRUE/FALSE, a CRLF script
through RunMemoryTextFile, a .pyc through RunCompiledFile, then RunFile reading
system.py out of the real pack. It stops at `ImportError: No module named app`,
i.e. the first C++ module RunMainScript registers — that is the 2V0 slice, and
the stdlib behind it is step 3. The host flags (Py_NoSiteFlag and friends) sit
in the test for now; step 3 moves them into the platform layer.
port-map statuses are set only for what the test actually executes: 10 of 12
launcher functions and 4 of 11 marshal ones (the small-.pyc path delegates to
CPython's own reader, so the ported r_object stays unreached). PythonUtils
compiles but nothing calls it until the binding modules arrive, so it stays TODO.
Windows has no mtpython yet, so port_logic drops ScriptLib and the shims there.
Tests: macOS ctest 27/27 incl. port.python_launcher; port_logic (with ScriptLib)
also compiles for android arm64 (NDK API 24) and ios arm64.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The embedded 40250 script layer needs its own interpreter on five platforms.
2.7 is end-of-life, so nothing can be fetched from the target SDKs and the
source is vendored (official 2.7.18 tarball, trimmed to 25MB; Lib/ stays for
step 3's python27.zip).
cpython-2.7.18/CMakeLists.txt builds one `mtpython` static library from exactly
the 133 objects the reference libpython2.7.a contains, off by default behind
-DMTGODOT_EMBED_PYTHON=ON. The source list and the built-in module table
(config/config.c, 39 entries) are shared; pyconfig.h is a probe result and is
not, so each platform keeps its own under config/<platform>/, regenerated by
tools/py_embed/gen_pyconfig.sh and checked against the shared table.
Three vendor patches, documented in docs/THIRD-PARTY.md: configure/configure.ac
learn arm64 on macOS, and posixmodule.c undefines the process-control calls it
hard-defines past pyconfig.h when building for iOS.
macOS (ctest python.embed: every builtin imports, codecs and pickle work off the
vendored Lib/), Android arm64 and iOS arm64 build. Linux needs a Linux host and
Windows needs a MinGW-vs-MSVC decision; both are noted for step 4.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tools/asset_manifest.py records path/size/sha256 of pack/Index, every .eix/.epk
and Eternexus/root/*.msm (audit/assets/40250-client.json). The gate script sets
MT_ASSETS_STRICT=1, verifies the manifest, runs the native pack/proto tests and
four Godot tests, and fails on a missing client, a mismatch or a skip.
MT_40250_CLIENT now comes from the environment for the native tests too.
remote_player_test accepts both weighted WAIT variants (wait/wait_1).
PORT-PLAN §5 records the 2D status.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
proto.cpp reads the 40250 MIPX/MMPT tables with the ported CLZO/TEA (156-byte
TItemTable, 255-byte SMobTable), replacing the m2dev XChaCha20 236/335 reader.
Names follow CItemData::GetName / GetMonsterName (szLocaleName, cp1252).
GDScript callers load AssetRoot.locale_file() = pack://locale/en/<name>, which
makes the EterPack chain runtime-reachable; port-map entries for the functions
a coverage run hit move to PORTED/ADAPTED.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read-only scanner for Client/pack following PackInitialize -> CEterPackManager
-> CEterPack::__BuildIndex/Get2. Keys are read from the reference EterPack.cpp
at run time; none are stored. Pure-Python TEA + LZO1X.
- 136 registered packs, 54,891 entries: NONE/COMPRESS/SECURITY only, no
PANAMA/HybridCrypt, all entries decode
- first-registered pack wins; 2,282 shadowed entries (2,194 differ)
- metin2_patch_{sd,mineral,mundi} use a second packer layout; 4 SECURITY
files fail 40250's CRC check and never load in the original
- no case collisions; paths are already lowercase
- PORT-PLAN: read EPK on device via ported EterPack, base install minus
extra locales, rebuild dict in Index order on any pack change
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix the per-function inventory against the clang-derived platform skeleton:
strip comments, accept indented qualified definitions in namespace blocks,
long parameter/initializer lists and all-caps qualified ctors, and skip
constructor initializer entries. Split EterBase by unit: tea/lzo/cipher/
Random/Stl/Timer/Poly become logic, the file/OS units stay platform; drop
the 8 obsolete platform stubs and regenerate audit/slices.
Baseline: logic 3343, python 5175, platform 2091 (10609), 0 done.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
These files were loaded only by their own test_*_parity.gd and never by
the runtime (transitively checked, incl. client_phase_integration_system).
Removes 76 manifest implementation/evidence references across 18
contracts; contract docs get a note that conclusions based on them are
void. Live systems kept: chest, consumable, item_attr, metin_socket,
whisper_chat.
Full headless suite: no new failures (7 failing + 5 headless-hanging
render tests are identical before the deletion).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
40250 only calls OnWaiting inside CInstanceBase::Transform() while
IsWalking() (InstanceBase.cpp:1922-1930); it never resends position
while standing. Remove _tick_on_waiting and its throttle state.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CInstanceBase::SetMoveSpeed only guards moving_speed > 1100 -> 0; the local
player's set_server_speed() was additionally floor/ceil-clamping the scale to
an arbitrary [0.25, 3.0] band with no basis in the reference or in this
project's already-fixed remote-entity equivalent (EntityStore::motion_move_speed).
A heavy haste stack or slow debuff on the local player would silently diverge
from what everyone else sees. Removed the extra clamp; kept the moving_speed<=0
early-return guard against pre-spawn/malformed values.
audit: movement.keyboard.motion/server-speed-scale-clamp closed with reference
citation, regression test, and full related-suite pass; contract stays PARTIAL.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Investigated the prior round's flagged gap ("CanAct()'s Paralysis/Faint/Sleep
movement doors not wired into net_play.gd") via exhaustive reachability
tracing of the 40250 reference source. All three resolve to non-gaps:
- Paralysis: its only setter (__Shaman_SetParalysis, InstanceBaseEffect.cpp:821)
has zero callers anywhere in the reachable source -- dead code, never true
in live gameplay.
- Sleep: the AFFECT_SLEEP case in SetAffect() is commented out; the only live
setter is the AFFECT_STUN case (SetSleep(), InstanceBaseEffect.cpp:932-933),
i.e. IsSleep() is just an alias for AFFECT_STUN -- already covered by the
existing `stunned` field and _can_process_network_state()'s dead/stunned/
knock_down gate.
- Faint: only setter is chrFaintTest(), a debug-only Python binding operating
on GetSelectedInstancePtr(); unverifiable from the available 40250 checkout
(no root/ UI scripts) -- evidence-blocked, left unresolved per the skill's
reachability rule.
Byproduct finding while tracing IsStun(): main-character GC_STUN routes to
Die() in the reference (RecvStunPacket), not Stun(); verified this causes no
movement.keyboard.motion discrepancy since CanAct() blocks movement equally
on IsDead()/IsStun() and the current client already blocks movement equally
on dead/stunned. The Die()-vs-Stun() distinction only affects death-system
semantics, already tracked under combat.affect-status.md (Remaining #2, rows
63/67) -- not duplicated here.
No implementation code changed. Removed the resolved item from manifest.json
and movement.keyboard-motion.md's Remaining list; documented full evidence
in the contract doc and remediation roadmap. Contract stays PARTIAL
(AccumulationMovement root-motion, OnMove/OnMoving event-source parity,
sync/hit/death/warp gate stacking, and real-model bone-flip test remain open
for future rounds).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
40250 parity: CPythonPlayer::NEW_MoveToDirection's m_isCmrRot branch
continuously rolls the camera toward the held movement direction every
frame while a direction key is down, independent of whether a new
translation target was just set. Add _camera_auto_rotate() to replicate
this (forward/back = no rotation, strafe = max rate, diagonal = half
rate), called unconditionally after the frozen/locked/shop/emotion gate
in _process(). Godot's yaw-increase handedness is opposite the
reference's D3D Roll(), so the sign is flipped as a platform adaptation;
verified by convergence invariants in keyboard_motion_timeline_test.gd
rather than bit-for-bit angle matching.
Closes movement.keyboard.motion/camera-auto-follow-rotation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>