feat(python): 在 app 进程里起 40250 脚本层(2P step 4,macOS)
移植 UserInterface/RunMainScript 到 platform/ScriptLib/PythonBoot:40250 把
CPythonLauncher 放在 Main() 的栈上,这里进程归 Godot,launcher 必须活过创建它的
那次调用,所以拆成 Start / RunMainScript / RunLine / Stop。37 个模块初始化函数里
只调已移植的 initpack(),system.py 因此仍停在第一个 import。
脚本报错能传回 GDScript,靠的是 platform/EterBase/Debug.cpp 实现 LogBox/LogBoxf:
ScriptLib 的 Traceback() 在 PyErr_Fetch 之后只剩这一条路,LastLogBoxMessage()
(platform/EterBase/LogBox.h)是 traceback 唯一幸存的地方。
GDScript 侧新增 Metin2PythonHost(python_host_node.{h,cpp})——唯一同时认识
godot-cpp 和 port 树的单元,PythonBoot 保持 godot-free。检查脚本
python_host_check.gd 分两段:解释器 + 标准库那段不需要 40250 的 pack,哪个平台都
能跑;system.py 那段需要 pack,没有就跳过。桌面入口 python_host_test.gd,真机入口
MT_TEST_MODE=python(导出模板不接受 --script)。
验证(macOS):标准库可 import、codec 注册表可用、sys.path 非空,system.py 停在和
ctest port.python_launcher 完全相同的 ImportError: No module named app;
ctest 27/27。Android/iOS 进程内未做,正式启动路径仍不起解释器(2V0)。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -499,3 +499,4 @@
|
||||
{"time": "2026-09-23T22:20:00Z", "event": "scope_decision", "unit": "目标平台收窄为 macOS arm64 + Android arm64(Linux / Windows 不做)", "ported": [], "adapted": [], "deleted": [], "divergent": [], "needs_live": [], "decision": {"by": "user", "statement": "Linux 和 Windows 不考虑,目前主要支持 macOS arm64 和 Android arm64", "ios": "iOS arm64 保留为交叉编译干净的次要目标,不作为验收目标(我的判断,待用户确认)", "mingw": "仓库里的 mingw-w64 交叉编译只留作可移植性门禁——它这次抓到了 UserInterface/StdAfx.h 的真问题——不代表支持 Windows,不为它投入额外工作,也不阻塞任何步骤"}, "docs": ["docs/PORT-PLAN.md: 第 1 节新增范围段;批次 2A step 3 平台列表;批次 2P step 1/2 措辞;step 4 由「五个平台」改为「两个目标平台」,Linux/Windows 两行划掉并保留原因备查;总表 2P 行", "docs/PLATFORMS.md: 新增 Scope 段", "docs/THIRD-PARTY.md: CPython 的 Windows 段由「not built yet」改为 out of scope + 原因备查", "docs/PYTHON-EMBED-EVAL.md: 下一步 1/3 两条"], "tests": "仅文档改动,无代码改动;port_map.py check 0 error / 1 tag;参考 EterPack.cpp 的 8 个 pack key 值逐个 git grep --cached -w,均未入库", "not_done": "2P step 4 剩余项仍在:Android 真机 APK 进程内的 3c 沙盒落地,以及 app 进程里跑 system.py → prototype.RunApp()"}
|
||||
{"time": "2026-09-23T22:35:00Z", "event": "known_issue", "unit": "Android GBK 编解码挂起;优先保证 macOS 可用", "ported": [], "adapted": [], "deleted": [], "divergent": [], "needs_live": [], "decision": {"by": "user", "statement": "先不管这个问题了,先保证 macOS 能正常使用,Android 后期再想办法"}, "finding": {"symptom": "build-android.sh 默认 API 24 下 extension/src/net/text_codec.cpp 编译失败:use of undeclared identifier 'iconv_open' / 'iconv' / 'iconv_close'", "root_cause": "text_codec.cpp:10 用 __has_include(<iconv.h>) 探测。NDK 里该头在任何 API 等级都存在,但声明被 __BIONIC_AVAILABILITY_GUARD(28) 挡住,探测方式与 bionic 的可用性模型不匹配", "measured": "NDK 28.2.13676358 clang++ -fsyntax-only:--target=aarch64-linux-android24 报 3 处 undeclared;android28 通过", "why_api_bump_is_wrong": "NDK iconv.h 注释写明 bionic 只支持 utf8/ascii/usascii/utf16be/utf16le/utf32be/utf32le/wchart,没有 GB2312/GBK。升到 28 后 iconv_open(\"GB2312\",\"UTF-8\") 运行期返回 -1,encode_one()/decode_wire() 对所有输入都返回 false,连纯 ASCII 也被拒,比现有 #else 的 ASCII 回退分支(text_codec.cpp:104-127)更糟。附带成本:APK min_sdk 需 >=28;config/android/pyconfig.h 是 API 24 下 configure 的(HAVE_LANGINFO_H 因 nl_langinfo 需 API 26 被关掉),要重跑 gen_pyconfig.sh android 并重验内建模块表", "future_fix": "1) 探测改为 #if __has_include(<iconv.h>) && !defined(__ANDROID__),不要写 __ANDROID_API__ >= 28;2) Android 自带 GBK<->Unicode 表(约 22k 码位)。40250 原客户端走 Windows MultiByteToWideChar(CP949/CP936),跨平台无对应物;macOS 能用只是系统 libiconv 恰好带 GBK"}, "docs": ["docs/PORT-PLAN.md 新增「已知问题:Android 的 GBK 编解码(挂起,2026-09-23)」小节"], "tests": "仅文档改动;诊断由 NDK clang++ -fsyntax-only 双 API 等级实测得出", "not_done": "text_codec.cpp 未改动;Android 扩展构建目前仍是红的"}
|
||||
{"time": "2026-09-23T23:30:00Z", "event": "test_fix", "unit": "net 测试去 flaky:net_classic_session_test + net_loopback_test", "ported": [], "adapted": [], "deleted": [], "divergent": [], "needs_live": [], "trigger": "macOS 全量 ctest 连续两次各挂 1 个 net 测试,且每次挂的不是同一个(第一次 net.loopback_flow,第二次 net.classic_session)", "root_causes": ["accept_loopback(): poll() 的 `<= 0` 把 EINTR 当成对端从未连接;且单片 500ms 超时在有负载时不够。50 轮压测里 4 次失败全部源于此", "net_classic_session_test.cpp:570 的 pump 循环条件抄自上面的死端口用例,但此处 connect 会成功,stage 先经过 LoggingIn,循环提前一步退出", "同文件 5 处手写 pump 循环只给 10-20ms 等真实 loopback socket(非阻塞 connect 完成 / connect 被拒 / 对端 FIN)", "net_loopback_test.cpp 10 处 run(..., N, []{return false;}) 是固定空转而非等待条件,等不够就断言失败"], "fixes": ["accept_loopback() 改为 EINTR 重试 + 5s 截止时间(listener 本就是非阻塞,超时后 accept 不会挂住)", "新增 pump_until(session, want, max_ms=2000),替换全部 5 处手写循环;等的是断言真正关心的那个状态", "替换 replacement PHASE_LOGIN 处的单次 pump(前一条 CHECK 已经断言过「不在同一帧」,这里只需要后续某次 pump 收到)", "net_loopback_test 的 10 处固定空转改为条件等待;服务端批量收包用该批最后一个包的标志作条件(TCP 有序,最后一个到了说明前面都到了),客户端事件用 decltype 声明的本地累加器在谓词里 drain"], "speedup": {"net_classic_session_test": "61.2s -> 3.5s:两处 this-*-host-must-not-resolve.invalid 换成带空标签的名字。本机解析器不按 RFC 6761 本地拒绝 .invalid,每次发往上游等满 30s 超时;空标签让 getaddrinfo 在语法层返回同一个 EAI_NONAME,实测 <10ms", "net_loopback_test": "固定空转改条件退出后单跑 0.94s", "ctest_total": "78.7s -> 8.7s"}, "tests": "net_classic_session_test 100/100(修复前 50 轮挂 4 轮);net_loopback_test 100/100(修复前单独跑第 7 轮即挂);全量 ctest 27/27;port_map.py check 0 error;参考 EterPack.cpp 的 8 个 pack key 值逐个 git grep --cached -w,均未入库", "not_done": "只改测试,未改产品代码。这些都是测试自身的计时/错误处理缺陷,不是 mtnet 的行为问题"}
|
||||
{"time": "2026-09-23T11:00:00Z", "event": "port_round", "unit": "PORT-PLAN 批次 2P step 4 (macOS): the 40250 script layer starts inside the Godot process", "ported": [], "adapted": ["UserInterface/UserInterface.cpp:RunMainScript -> extension/src/platform/ScriptLib/PythonBoot.cpp (Start/RunMainScript/RunLine/Stop; 40250 keeps CPythonLauncher on Main()'s stack, Godot owns this process so the launcher has to outlive the call that starts it)", "EterBase/Debug.cpp:LogBox, EterBase/Debug.cpp:LogBoxf -> extension/src/platform/EterBase/Debug.cpp (stderr plus LastLogBoxMessage(); ScriptLib Traceback() reports through LogBoxf after PyErr_Fetch has cleared the exception, so this is the only place a script traceback survives)"], "deleted": [], "divergent": ["RunMainScript RegisterCommandLine passes the command line through unchanged: the -cs/-ncs auto-login decoding needs EterBase/Utils.cpp SplitLine and EterLib/Util.cpp base64_decode, both declared but not ported (批次 2V1). Nothing in this host passes -cs today.", "RunMainScript drops '--pause-before-create-window' (system(\"pause\")) and the XTrap init check: no console and no XTrap on macOS/Android.", "Of the 37 module initializers RunMainScript calls, only initpack() is called — the other 36 are 2V slices."], "needs_live": [], "staging": {"native": "extension/src/platform/ScriptLib/PythonBoot.{h,cpp} (godot-free, placed under ScriptLib because that is the directory port_logic/port_platform drop when `mtpython` is absent; compiled with the UserInterface library's definitions so its `#ifdef _DISTRIBUTE` gives __DEBUG__ = 0). extension/src/platform/EterBase/LogBox.h. extension/src/python_host_node.{h,cpp} -> GDScript class Metin2PythonHost (start/is_running/run_main_script/run_line/stop; each returns \"\" or the reason, and a build without the interpreter answers with a distinguishable message instead of failing to register).", "gdscript": "project/python_host_check.gd in two halves — the interpreter + stdlib half needs no 40250 pack and runs on any platform, the system.py half needs the pack and is skipped without it. project/python_host_test.gd (--script) is the desktop entry; MT_TEST_MODE=python in project/client_main.gd is the on-device entry, since export templates do not accept --script.", "boot": "Metin2PythonHost.start(\"\") stages the stdlib through Metin2Python.stdlib_path(), which until now had no caller outside python_stdlib_test.gd."}, "status_note": "macOS only. Android/iOS in-process is untouched: the APK cross-build is blocked on the GBK iconv issue (see the PORT-PLAN known-issue section, deferred by the user 2026-09-23), and a device has no 40250 Client/pack, so on a phone only the interpreter + stdlib half of the check can run. prototype.RunApp() is 2V0: system.py still stops at its first import. The real boot path (AppFlow) does not start the interpreter.", "tests": ["godot --headless --path project --script python_host_test.gd -> PASS: interpreter starts in-process, is_running(), a second start() is a no-op, `import os, string, types, codecs`, u'\\xe4'.encode('cp1252'), sys.path non-empty, system.py stops at ImportError: No module named app (identical to ctest port.python_launcher), stop() tears it down", "MT_TEST_MODE=python godot --headless --path project -> same result through client_main.gd", "ctest --test-dir build -> 27/27 passed (14.51s)"], "not_done": "Android/iOS 进程内验证;正式启动路径起解释器(2V0);RunMainScript 的 -cs 命令行解码(2V1)", "commit": "2P-step4-macos"}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"reference": "EterBase/Debug.cpp",
|
||||
"reference_sha256": "af7a6cba9e6d83511a40f596629f85f1b770b510fc0db1d9cbefcec88ecb15ec",
|
||||
"priority": "P4",
|
||||
"contracts": [],
|
||||
"functions": {
|
||||
"SetLogLevel": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"Log": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"Logn": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"Logf": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"Lognf": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"Trace": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"Tracen": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"Tracenf": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"Tracef": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"TraceError": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"TraceErrorWithoutEnter": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"LogBoxf": {
|
||||
"impl": [
|
||||
"extension/src/platform/EterBase/Debug.cpp:LogBoxf"
|
||||
],
|
||||
"status": "ADAPTED",
|
||||
"note": "verbatim apart from two PORT fixes the 40250 build did not need: va_end (40250 leaks the va_list) and terminating szBuf, which _vsnprintf does not do on truncation.",
|
||||
"test": "project/python_host_check.gd — system.py's ImportError reaches GDScript through it"
|
||||
},
|
||||
"LogBox": {
|
||||
"impl": [
|
||||
"extension/src/platform/EterBase/Debug.cpp:LogBox"
|
||||
],
|
||||
"status": "ADAPTED",
|
||||
"note": "40250 pops a MessageBox on g_PopupHwnd and Tracen()s the text. No window here: the text goes to stderr and is kept in LastLogBoxMessage() (extension/src/platform/EterBase/LogBox.h), because ScriptLib/PythonLauncher.cpp Traceback() reports every failed script line through LogBoxf after PyErr_Fetch has already cleared the exception — this is the only place a traceback survives.",
|
||||
"test": "project/python_host_check.gd — system.py's ImportError reaches GDScript through it"
|
||||
},
|
||||
"LogFile": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"LogFilef": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"OpenLogFile": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"OpenConsoleWindow": {
|
||||
"status": "TODO"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +1,78 @@
|
||||
{
|
||||
"reference": "UserInterface/UserInterface.cpp",
|
||||
"reference_sha256": "beed987e4b1de627acafe7ea6f5fde989b8f4b33c8643fe2abb0f7152ea4a9eb",
|
||||
"priority": "P4",
|
||||
"contracts": [],
|
||||
"functions": {
|
||||
"ApplicationSetErrorString": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"CheckPythonLibraryFilenames": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"ApplicationStringTable_Initialize": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"ApplicationStringTable_GetString": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"ApplicationStringTable_GetStringz": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"PackInitialize": {
|
||||
"impl": [
|
||||
"extension/src/platform/UserInterface/UserInterface.cpp:PackInitialize"
|
||||
],
|
||||
"status": "ADAPTED",
|
||||
"note": "platform PackInitialize: Index pack names resolved to the on-disk .eix spelling case-insensitively; no LogBoxf; CTextFileLoader::SetCacheMode / CSoundData::SetPackMode not called (units not ported, neither affects pack registration)",
|
||||
"test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects); extension/tests/proto_test.cpp (40250 locale/en item_proto/mob_proto through pack://)"
|
||||
},
|
||||
"RunMainScript": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"Main": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"CreateMetin2GameMutex": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"DestroyMetin2GameMutex": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"__ErrorPythonLibraryIsNotExist": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"__IsTimeStampOption": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"__PrintTimeStamp": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"__IsLocaleOption": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"__IsLocaleVersion": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"__IsOpenIDAuthKeyOption": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"__IsOpenIDTestOption": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"WinMain": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"GrannyError": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"Setup": {
|
||||
"status": "TODO"
|
||||
}
|
||||
"reference": "UserInterface/UserInterface.cpp",
|
||||
"reference_sha256": "beed987e4b1de627acafe7ea6f5fde989b8f4b33c8643fe2abb0f7152ea4a9eb",
|
||||
"priority": "P4",
|
||||
"contracts": [],
|
||||
"functions": {
|
||||
"ApplicationSetErrorString": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"CheckPythonLibraryFilenames": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"ApplicationStringTable_Initialize": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"ApplicationStringTable_GetString": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"ApplicationStringTable_GetStringz": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"PackInitialize": {
|
||||
"impl": [
|
||||
"extension/src/platform/UserInterface/UserInterface.cpp:PackInitialize"
|
||||
],
|
||||
"status": "ADAPTED",
|
||||
"note": "platform PackInitialize: Index pack names resolved to the on-disk .eix spelling case-insensitively; no LogBoxf; CTextFileLoader::SetCacheMode / CSoundData::SetPackMode not called (units not ported, neither affects pack registration)",
|
||||
"test": "extension/tests/port_eterpack_test.cpp (103 packs, 54891 entries, first-registered wins, 4 SECURITY rejects); extension/tests/proto_test.cpp (40250 locale/en item_proto/mob_proto through pack://)"
|
||||
},
|
||||
"RunMainScript": {
|
||||
"impl": [
|
||||
"extension/src/platform/ScriptLib/PythonBoot.cpp:PythonBoot::RunMainScript"
|
||||
],
|
||||
"status": "ADAPTED",
|
||||
"note": "40250 runs it on Main()'s stack and takes the CPythonLauncher by reference; Godot owns this process, so PythonBoot::Start holds the launcher and RunMainScript takes only the command line plus a std::string* for the text 40250 shows in LogBox's message box. Of the 37 module initializers only initpack() is called — the rest are 2V slices, which is why system.py still stops at its first import. RegisterCommandLine passes the line through unchanged: the -cs/-ncs auto-login decoding needs EterBase/Utils.cpp SplitLine and EterLib/Util.cpp base64_decode, both still TODO (批次 2V1). '--pause-before-create-window' (system(\"pause\")) and the XTrap init check are dropped: no console and no XTrap on this platform.",
|
||||
"test": "project/python_host_test.gd (godot --headless --path project --script python_host_test.gd) and the same check through MT_TEST_MODE=python in project/client_main.gd -> system.py stops at ImportError: No module named app, the same point as ctest port.python_launcher"
|
||||
},
|
||||
"Main": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"CreateMetin2GameMutex": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"DestroyMetin2GameMutex": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"__ErrorPythonLibraryIsNotExist": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"__IsTimeStampOption": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"__PrintTimeStamp": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"__IsLocaleOption": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"__IsLocaleVersion": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"__IsOpenIDAuthKeyOption": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"__IsOpenIDTestOption": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"WinMain": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"GrannyError": {
|
||||
"status": "TODO"
|
||||
},
|
||||
"Setup": {
|
||||
"status": "TODO"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-2
@@ -328,14 +328,28 @@ extension/third_party/cpython-2.7.18/ # 静态库(2P 批次
|
||||
macOS 上用 `force_stage=true` 跑的就是移动端那条路径:落盘、sha 一致、字节与 `res://` 相同、`.part` 不残留、
|
||||
`ZIPReader` 能打开且含 `encodings/__init__.py`,重复调用不重复复制,人为弄坏副本后重新落盘。
|
||||
**未完成**:真机上跑(Android/iOS 的 `user://` 沙盒路径本身没验过),随 step 4 一起;
|
||||
app 的启动流程还没有调用它——解释器要到 2V0 才在进程里起来,现在只有测试调。
|
||||
进程内的调用者见 step 4(`Metin2PythonHost.start()` 空路径时就调 `Metin2Python.stdlib_path()`);
|
||||
正式启动路径要到 2V0 才起解释器,现在只有 `MT_TEST_MODE=python` 和测试调。
|
||||
`port_platform` 现在在没有 `mtpython` 时也会排除 `platform/ScriptLib/`(与 `port_logic` 同一条规则),
|
||||
否则 Windows 门禁会编到 `PythonHost.cpp`。
|
||||
4. 两个目标平台分别验证:编译、链接无未定义符号、`Py_Initialize`、静态 C 模块逐个 `import`、在 app 进程里跑 `system.py` → `prototype.RunApp()`(对照结果:74 个模块中 66 个加载成功,引导期调用 33 个 C++ 函数)。
|
||||
|
||||
**进程内入口(2026-09-23)**:`extension/src/platform/ScriptLib/PythonBoot.{h,cpp}` 是
|
||||
40250 `RunMainScript` 的移植(`audit/port-map/UserInterface/UserInterface.cpp.json`),
|
||||
拆成 `Start` / `RunMainScript` / `RunLine` / `Stop`——40250 把 `CPythonLauncher` 放在 `Main()` 的栈上,
|
||||
这里进程归 Godot,launcher 必须活过创建它的那次调用。37 个模块初始化函数里只调了已移植的 `initpack()`。
|
||||
GDScript 侧是 `Metin2PythonHost`(`extension/src/python_host_node.{h,cpp}`,唯一同时认识 godot-cpp 和
|
||||
port 树的单元;`PythonBoot` 保持 godot-free)。脚本报错能传回 GDScript,是因为
|
||||
`platform/EterBase/Debug.cpp` 实现了 `LogBox`/`LogBoxf`:`Traceback()` 在 `PyErr_Fetch` 之后只剩这一条路
|
||||
(`platform/EterBase/LogBox.h` 的 `LastLogBoxMessage()`)。
|
||||
检查脚本 `project/python_host_check.gd` 分两段:解释器 + 标准库那段不需要 40250 的 pack,哪个平台都能跑;
|
||||
`system.py` 那段需要 pack,没有就跳过。桌面入口 `project/python_host_test.gd`
|
||||
(`godot --headless --path project --script python_host_test.gd`),真机入口是
|
||||
`MT_TEST_MODE=python`(导出模板不接受 `--script`)。
|
||||
|
||||
| 平台 | 状态 |
|
||||
| --- | --- |
|
||||
| macOS arm64 | step 1、2 完成:`mtpython` 静态库 + `python.embed`(39 个内建模块全部 import,标准库 codec/pickle 通过)+ `port.python_launcher`(`CPythonLauncher` 跑到 `system.py` 的第一个 import);`system.py` 在 app 进程内未做 |
|
||||
| macOS arm64 | step 1、2、3 完成,step 4 完成:`mtpython` 静态库 + `python.embed`(39 个内建模块全部 import,标准库 codec/pickle 通过)+ `port.python_launcher`(`CPythonLauncher` 跑到 `system.py` 的第一个 import)+ **app 进程内**(`python_host_test.gd` / `MT_TEST_MODE=python`):标准库可 import、codec 注册表可用,`system.py` 停在和 ctest 完全相同的 `ImportError: No module named app`。`prototype.RunApp()` 要等 2V0 |
|
||||
| Android arm64 | step 1 完成:NDK API 24 交叉编译出 `libmtpython.a`;独立可执行文件在真机跑通(b02c49bb);APK 进程内未做。step 2 的 ScriptLib 只在 macOS 编译过 |
|
||||
| iOS arm64 | step 1 完成:iphoneos arm64 编出 133 个目标文件,无 `_system`/`_fork`/`_popen` 未定义符号;真机未跑。step 2 的 ScriptLib 只在 macOS 编译过 |
|
||||
| ~~Linux x86_64~~ | 不做(2026-09-23 定,见第 1 节) |
|
||||
|
||||
@@ -199,6 +199,7 @@ add_library(mtgodot ${MT_LIB_KIND}
|
||||
src/proto/proto_node.cpp
|
||||
src/pack40250_node.cpp
|
||||
src/python_stdlib.cpp
|
||||
src/python_host_node.cpp
|
||||
)
|
||||
target_compile_features(mtgodot PRIVATE cxx_std_20)
|
||||
target_link_libraries(mtgodot PRIVATE godot::cpp xrender::libgr2 xrender::formats mtnet mtproto port_platform)
|
||||
|
||||
@@ -14,3 +14,10 @@ target_link_libraries(port_platform PUBLIC port_logic)
|
||||
foreach(src IN LISTS MT_PLATFORM_SOURCES)
|
||||
mt_port_apply_defines(${src})
|
||||
endforeach()
|
||||
# ScriptLib/PythonBoot.cpp is UserInterface's RunMainScript; it sits under ScriptLib only because that is
|
||||
# the directory dropped when there is no embedded interpreter. Its `#ifdef _DISTRIBUTE` (__DEBUG__ = 0)
|
||||
# has to see the UserInterface library's definitions, not ScriptLib's.
|
||||
if(TARGET mtpython)
|
||||
set_property(SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/ScriptLib/PythonBoot.cpp
|
||||
PROPERTY COMPILE_DEFINITIONS ${MT_PORT_DEFINES_UserInterface})
|
||||
endif()
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
|
||||
#include "../PlatformStub.h"
|
||||
#include "EterBase/Timer.h"
|
||||
#include "LogBox.h"
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
const DWORD DEBUG_STRING_MAX_LEN = 1024;
|
||||
|
||||
@@ -97,14 +99,52 @@ auto TraceErrorWithoutEnter(const char *, ...) -> void
|
||||
MT_PLATFORM_STUB();
|
||||
}
|
||||
|
||||
auto LogBox(const char *, const char *, HWND) -> void
|
||||
namespace
|
||||
{
|
||||
MT_PLATFORM_STUB();
|
||||
std::string& log_box_message()
|
||||
{
|
||||
static std::string message;
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
auto LogBoxf(const char *, ...) -> void
|
||||
const std::string& LastLogBoxMessage()
|
||||
{
|
||||
MT_PLATFORM_STUB();
|
||||
return log_box_message();
|
||||
}
|
||||
|
||||
void ClearLogBoxMessage()
|
||||
{
|
||||
log_box_message().clear();
|
||||
}
|
||||
|
||||
void LogBox(const char * c_szMsg, const char * c_szCaption, HWND)
|
||||
{
|
||||
// 40250 pops a MessageBox on g_PopupHwnd and Tracen()s the text. There is no window here and no
|
||||
// log file yet, so stderr is the whole of it; the text is also kept for the caller (LogBox.h),
|
||||
// because it is the only place a script traceback survives.
|
||||
std::string& message = log_box_message();
|
||||
message = c_szCaption ? c_szCaption : "LOG";
|
||||
message += ": ";
|
||||
message += c_szMsg ? c_szMsg : "";
|
||||
|
||||
fprintf(stderr, "%s\n", message.c_str());
|
||||
fflush(stderr);
|
||||
Tracen(c_szMsg);
|
||||
}
|
||||
|
||||
void LogBoxf(const char * c_szFormat, ...)
|
||||
{
|
||||
va_list args;
|
||||
va_start(args, c_szFormat);
|
||||
|
||||
char szBuf[2048];
|
||||
_vsnprintf(szBuf, sizeof(szBuf), c_szFormat, args);
|
||||
va_end(args); // PORT: 40250 leaks the va_list; on this ABI va_end is not optional
|
||||
|
||||
// PORT: _vsnprintf does not terminate on truncation.
|
||||
szBuf[sizeof(szBuf) - 1] = '\0';
|
||||
LogBox(szBuf);
|
||||
}
|
||||
|
||||
auto LogFile(const char *) -> void
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
// The text 40250 would have put in a message box (EterBase/Debug.cpp LogBox/LogBoxf).
|
||||
//
|
||||
// LogBox is the client's fatal-error channel: Traceback() in ScriptLib/PythonLauncher.cpp reports every
|
||||
// failed script line through it, and RunMainScript's callers show it and quit. There is no message box
|
||||
// on this platform, so the implementation writes the text to stderr and keeps it here for whoever asked
|
||||
// for the work — that is how a caller learns *why* a script failed, since Traceback() fetches the
|
||||
// exception (and so clears it) before returning.
|
||||
|
||||
#include <string>
|
||||
|
||||
// The last LogBox/LogBoxf text, "" when none since the last ClearLogBoxMessage().
|
||||
const std::string& LastLogBoxMessage();
|
||||
void ClearLogBoxMessage();
|
||||
@@ -0,0 +1,150 @@
|
||||
// See PythonBoot.h. The sequence is 40250 UserInterface.cpp:241-352 (RunMainScript) and 419-434 (Main),
|
||||
// split so the launcher can outlive the call that creates it.
|
||||
#include "ScriptLib/StdAfx.h"
|
||||
#include "ScriptLib/PythonLauncher.h"
|
||||
#include "UserInterface/StdAfx.h" // initpack and the rest of the module initializer list
|
||||
#include "PythonBoot.h"
|
||||
#include "PythonHost.h"
|
||||
#include "../EterBase/LogBox.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace
|
||||
{
|
||||
// 40250 has this on Main()'s stack. CPythonLauncher is a CSingleton, so there is still only ever one.
|
||||
std::unique_ptr<CPythonLauncher> g_launcher;
|
||||
|
||||
bool fail(std::string* error, const std::string& text)
|
||||
{
|
||||
if (error)
|
||||
*error = text;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
namespace PythonBoot
|
||||
{
|
||||
|
||||
bool IsRunning()
|
||||
{
|
||||
return g_launcher != nullptr;
|
||||
}
|
||||
|
||||
bool Start(const char* stdlib_path, std::string* error)
|
||||
{
|
||||
if (g_launcher)
|
||||
return true;
|
||||
|
||||
const std::string stdlib = (stdlib_path && *stdlib_path) ? stdlib_path : PythonHost::DefaultStdLibPath();
|
||||
if (stdlib.empty())
|
||||
return fail(error, "no Python standard library path (Metin2Python.stdlib_path / $MT_PYTHON_STDLIB)");
|
||||
|
||||
// The flags have to be set before the interpreter starts, i.e. before the constructor below.
|
||||
PythonHost::Configure();
|
||||
|
||||
g_launcher = std::make_unique<CPythonLauncher>(); // Py_Initialize
|
||||
if (!g_launcher->Create())
|
||||
{
|
||||
Stop();
|
||||
return fail(error, "CPythonLauncher::Create failed");
|
||||
}
|
||||
|
||||
std::string stdlib_error;
|
||||
if (!PythonHost::InstallStdLib(stdlib.c_str(), &stdlib_error))
|
||||
{
|
||||
Stop();
|
||||
return fail(error, "InstallStdLib(" + stdlib + "): " + stdlib_error);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 40250: RunMainScript
|
||||
bool RunMainScript(const char* lpCmdLine, std::string* error)
|
||||
{
|
||||
if (!g_launcher)
|
||||
return fail(error, "PythonBoot::Start has not run");
|
||||
|
||||
CPythonLauncher& pyLauncher = *g_launcher;
|
||||
|
||||
// The module initializers, in 40250's order. Everything after initpack (initdbg, initime, initgrp,
|
||||
// initgrpImage, initgrpText, initwndMgr, initudp, initapp, ... initServerStateChecker) is a 2V slice:
|
||||
// system.py stops at the first import of one of them, today `app`.
|
||||
initpack();
|
||||
|
||||
ClearLogBoxMessage();
|
||||
|
||||
NANOBEGIN
|
||||
|
||||
// RegisterDebugFlag
|
||||
{
|
||||
std::string stRegisterDebugFlag;
|
||||
|
||||
#ifdef _DISTRIBUTE
|
||||
stRegisterDebugFlag ="__DEBUG__ = 0";
|
||||
#else
|
||||
stRegisterDebugFlag ="__DEBUG__ = 1";
|
||||
#endif
|
||||
|
||||
if (!pyLauncher.RunLine(stRegisterDebugFlag.c_str()))
|
||||
{
|
||||
TraceError("RegisterDebugFlag Error");
|
||||
return fail(error, "RegisterDebugFlag Error: " + LastLogBoxMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterCommandLine.
|
||||
// PORT: the "-cs"/"-ncs" auto-login argument is passed through unchanged. Decoding it needs
|
||||
// EterBase/Utils.cpp SplitLine and EterLib/Util.cpp base64_decode, which are declared but not ported
|
||||
// yet; they belong to the login track (批次 2V1) and nothing in this host passes -cs today.
|
||||
{
|
||||
std::string stRegisterCmdLine;
|
||||
|
||||
std::string stCmdLine = lpCmdLine ? lpCmdLine : "";
|
||||
|
||||
stRegisterCmdLine ="__COMMAND_LINE__ = ";
|
||||
stRegisterCmdLine+='"';
|
||||
stRegisterCmdLine+=stCmdLine;
|
||||
stRegisterCmdLine+='"';
|
||||
|
||||
const CHAR* c_szRegisterCmdLine=stRegisterCmdLine.c_str();
|
||||
if (!pyLauncher.RunLine(c_szRegisterCmdLine))
|
||||
{
|
||||
TraceError("RegisterCommandLine Error");
|
||||
return fail(error, "RegisterCommandLine Error: " + LastLogBoxMessage());
|
||||
}
|
||||
}
|
||||
// PORT: 40250 also honours "--pause-before-create-window" here with system("pause") and the XTrap
|
||||
// init check. Neither exists on this platform (no console to pause, no XTrap), so only the RunFile
|
||||
// is kept.
|
||||
{
|
||||
if (!pyLauncher.RunFile("system.py"))
|
||||
{
|
||||
TraceError("RunMain Error");
|
||||
return fail(error, "RunMain Error: " + LastLogBoxMessage());
|
||||
}
|
||||
}
|
||||
|
||||
NANOEND
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RunLine(const char* source, std::string* error)
|
||||
{
|
||||
if (!g_launcher)
|
||||
return fail(error, "PythonBoot::Start has not run");
|
||||
|
||||
ClearLogBoxMessage();
|
||||
if (!g_launcher->RunLine(source ? source : ""))
|
||||
return fail(error, LastLogBoxMessage());
|
||||
return true;
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
if (!g_launcher)
|
||||
return;
|
||||
g_launcher->Clear(); // Main() calls Clear() explicitly before the launcher leaves scope
|
||||
g_launcher.reset();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
#pragma once
|
||||
// Starting the script layer inside a host process (docs/PORT-PLAN.md 批次 2P step 4).
|
||||
//
|
||||
// 40250 runs the interpreter on Main()'s stack (UserInterface/UserInterface.cpp:419-434): construct
|
||||
// CPythonLauncher, Create(), RunMainScript(), Clear(), all inside one WinMain. Godot owns the process
|
||||
// here and calls in from GDScript, so the launcher has to outlive the call that starts it — that split
|
||||
// (Start / RunMainScript / Stop) is the only difference from the original control flow.
|
||||
//
|
||||
// It lives under platform/ScriptLib rather than platform/UserInterface because it is only buildable
|
||||
// with the embedded interpreter, and that is the directory both port_logic and port_platform drop when
|
||||
// `mtpython` is absent (extension/src/port/CMakeLists.txt).
|
||||
//
|
||||
// godot-free, like everything else under platform/: extension/src/python_host_node.cpp is what GDScript
|
||||
// talks to.
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace PythonBoot
|
||||
{
|
||||
// PythonHost::Configure() -> CPythonLauncher -> Create() -> PythonHost::InstallStdLib(stdlib_path).
|
||||
// An empty stdlib_path means PythonHost::DefaultStdLibPath(). Starting twice is a no-op that succeeds.
|
||||
// On failure *error is set and the interpreter is shut down again, so the next attempt starts clean.
|
||||
// The packs must already be registered (platform/PackBackend.h), as 40250 registers them in Main()
|
||||
// before the launcher exists.
|
||||
bool Start(const char* stdlib_path, std::string* error);
|
||||
bool IsRunning();
|
||||
|
||||
// 40250: RunMainScript(CPythonLauncher&, const char*) — the module initializers, __DEBUG__,
|
||||
// __COMMAND_LINE__ and RunFile("system.py"). Only the initializers that are ported are called; the rest
|
||||
// arrive with the 2V slices, which is why system.py still stops at its first import.
|
||||
// False leaves *error set to what the launcher reported through LogBox (platform/EterBase/LogBox.h).
|
||||
bool RunMainScript(const char* lpCmdLine, std::string* error);
|
||||
|
||||
// CPythonLauncher::RunLine, with the reported text handed back rather than only shown. The host's own
|
||||
// check uses it to prove the interpreter and the standard library work in-process on a device that has
|
||||
// no 40250 packs, which is where RunMainScript cannot run at all.
|
||||
bool RunLine(const char* source, std::string* error);
|
||||
|
||||
// 40250 Main() calls Clear() and lets the launcher leave scope.
|
||||
void Stop();
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#include "python_host_node.h"
|
||||
|
||||
#include <godot_cpp/core/class_db.hpp>
|
||||
|
||||
#include "python_stdlib.h"
|
||||
|
||||
#ifdef MTGODOT_HAVE_PYTHON
|
||||
// Standard types only — PythonBoot.h keeps <Python-2.7/*> and the Win32 shims out of this translation
|
||||
// unit, which has to stay compilable next to godot-cpp.
|
||||
#include "platform/ScriptLib/PythonBoot.h"
|
||||
#include <string>
|
||||
#endif
|
||||
|
||||
using namespace godot;
|
||||
|
||||
namespace mtgodot {
|
||||
|
||||
#ifdef MTGODOT_HAVE_PYTHON
|
||||
|
||||
String Metin2PythonHost::start(const String &stdlib_path) {
|
||||
String path = stdlib_path;
|
||||
if (path.is_empty()) {
|
||||
path = Metin2Python::stdlib_path(false);
|
||||
if (path.is_empty()) {
|
||||
return String("python27.zip unavailable: ") + Metin2Python::last_error();
|
||||
}
|
||||
}
|
||||
std::string error;
|
||||
if (!PythonBoot::Start(path.utf8().get_data(), &error)) {
|
||||
return String::utf8(error.c_str());
|
||||
}
|
||||
return String();
|
||||
}
|
||||
|
||||
bool Metin2PythonHost::is_running() {
|
||||
return PythonBoot::IsRunning();
|
||||
}
|
||||
|
||||
String Metin2PythonHost::run_main_script(const String &command_line) {
|
||||
std::string error;
|
||||
if (!PythonBoot::RunMainScript(command_line.utf8().get_data(), &error)) {
|
||||
return String::utf8(error.c_str());
|
||||
}
|
||||
return String();
|
||||
}
|
||||
|
||||
String Metin2PythonHost::run_line(const String &source) {
|
||||
std::string error;
|
||||
if (!PythonBoot::RunLine(source.utf8().get_data(), &error)) {
|
||||
return String::utf8(error.c_str());
|
||||
}
|
||||
return String();
|
||||
}
|
||||
|
||||
void Metin2PythonHost::stop() {
|
||||
PythonBoot::Stop();
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
// Built without -DMTGODOT_EMBED_PYTHON=ON: the class is still registered so GDScript can tell "no
|
||||
// interpreter in this build" apart from "the interpreter failed to start".
|
||||
String Metin2PythonHost::start(const String &) {
|
||||
return String("this build has no embedded Python (-DMTGODOT_EMBED_PYTHON=ON)");
|
||||
}
|
||||
|
||||
bool Metin2PythonHost::is_running() {
|
||||
return false;
|
||||
}
|
||||
|
||||
String Metin2PythonHost::run_main_script(const String &) {
|
||||
return String("this build has no embedded Python (-DMTGODOT_EMBED_PYTHON=ON)");
|
||||
}
|
||||
|
||||
String Metin2PythonHost::run_line(const String &) {
|
||||
return String("this build has no embedded Python (-DMTGODOT_EMBED_PYTHON=ON)");
|
||||
}
|
||||
|
||||
void Metin2PythonHost::stop() {
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void Metin2PythonHost::_bind_methods() {
|
||||
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("start", "stdlib_path"),
|
||||
&Metin2PythonHost::start, DEFVAL(String()));
|
||||
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("is_running"), &Metin2PythonHost::is_running);
|
||||
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("run_main_script", "command_line"),
|
||||
&Metin2PythonHost::run_main_script, DEFVAL(String()));
|
||||
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("run_line", "source"), &Metin2PythonHost::run_line);
|
||||
ClassDB::bind_static_method("Metin2PythonHost", D_METHOD("stop"), &Metin2PythonHost::stop);
|
||||
}
|
||||
|
||||
} // namespace mtgodot
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
// Metin2PythonHost — the 40250 script layer inside the Godot process (docs/PORT-PLAN.md 批次 2P step 4).
|
||||
//
|
||||
// Metin2Pack.initialize(AssetRoot.client_path()) # the packs system.py is read from
|
||||
// if Metin2PythonHost.start() == "":
|
||||
// var err := Metin2PythonHost.run_main_script("")
|
||||
//
|
||||
// Both calls answer with "" on success and with the reason otherwise, so GDScript never has to guess
|
||||
// from a bool. Until 批次 2V0 registers the C++ modules, run_main_script() is expected to come back with
|
||||
// system.py's first import error; that is what step 4 verifies on each target platform.
|
||||
//
|
||||
// This is the only unit that knows both godot-cpp and the port tree: the sequence itself lives in
|
||||
// platform/ScriptLib/PythonBoot.h, which stays godot-free.
|
||||
|
||||
#include <godot_cpp/classes/object.hpp>
|
||||
#include <godot_cpp/variant/string.hpp>
|
||||
|
||||
namespace mtgodot {
|
||||
|
||||
class Metin2PythonHost : public godot::Object {
|
||||
GDCLASS(Metin2PythonHost, godot::Object)
|
||||
|
||||
public:
|
||||
// Stages the standard library (Metin2Python::stdlib_path) when `stdlib_path` is empty, then starts
|
||||
// the interpreter. "" on success, including when it was already running.
|
||||
static godot::String start(const godot::String &stdlib_path);
|
||||
static bool is_running();
|
||||
// __DEBUG__, __COMMAND_LINE__ and system.py out of the packs. "" when system.py ran to the end.
|
||||
static godot::String run_main_script(const godot::String &command_line);
|
||||
// One line of Python in the same __main__ dictionary. "" when it ran.
|
||||
static godot::String run_line(const godot::String &source);
|
||||
static void stop();
|
||||
|
||||
protected:
|
||||
static void _bind_methods();
|
||||
};
|
||||
|
||||
} // namespace mtgodot
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "net/m2_client.h"
|
||||
#include "pack40250_node.h"
|
||||
#include "proto/proto_node.h"
|
||||
#include "python_host_node.h"
|
||||
#include "python_stdlib.h"
|
||||
#include "static_object.h"
|
||||
#include "terrain_splat.h"
|
||||
@@ -47,6 +48,7 @@ void initialize_mtgodot_module(ModuleInitializationLevel p_level) {
|
||||
GDREGISTER_CLASS(mtgodot::Metin2Proto);
|
||||
GDREGISTER_CLASS(mtgodot::Metin2Pack);
|
||||
GDREGISTER_CLASS(mtgodot::Metin2Python);
|
||||
GDREGISTER_CLASS(mtgodot::Metin2PythonHost);
|
||||
}
|
||||
|
||||
void uninitialize_mtgodot_module(ModuleInitializationLevel p_level) {
|
||||
|
||||
@@ -24,6 +24,17 @@ func _ready() -> void:
|
||||
t.name = "PackageRenderTest"
|
||||
add_child(t)
|
||||
return
|
||||
# MT_TEST_MODE=python:在 app 进程里起内嵌解释器跑 system.py(PORT-PLAN 批次 2P step 4)。
|
||||
# 导出模板不接受 --script,这是在真机 APK/IPA 里跑这条检查的唯一入口。
|
||||
if OS.get_environment("MT_TEST_MODE") == "python":
|
||||
AssetPack.ensure()
|
||||
var fail: int = preload("res://python_host_check.gd").new().run()
|
||||
if fail == 0:
|
||||
print("PASS: python_host_check (app 进程内 system.py)")
|
||||
else:
|
||||
printerr("%d check(s) failed" % fail)
|
||||
get_tree().quit(1 if fail else 0)
|
||||
return
|
||||
if OS.get_environment("MT_TEST_MODE") == "forest_render":
|
||||
var forest := ForestMapRenderTest.new()
|
||||
forest.name = "ForestMapRenderTest"
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# python_host_check —— 在 app 进程里把内嵌解释器起来并跑 system.py(docs/PORT-PLAN.md 批次 2P step 4)。
|
||||
#
|
||||
# 和 ctest 的 `port.python_launcher` 跑的是同一条序列,区别只有一个:那边是独立可执行文件,这边在
|
||||
# Godot 进程里,由 GDScript 发起 —— step 4 要验的就是这个差别。2V0 把 C++ 模块注册上之前,
|
||||
# system.py 停在第一个 import(`No module named app`),这就是当前的验收结果。
|
||||
#
|
||||
# 真机上通常没有 40250 的 Client/pack(APK 里只有 python27.zip),所以分两段:解释器 + 标准库这一段
|
||||
# 不需要 pack,任何平台都跑;system.py 这一段需要 pack,没有就跳过并说清楚。
|
||||
#
|
||||
# 桌面:python_host_test.gd(--script);真机:MT_TEST_MODE=python 从 client_main.gd 进来。
|
||||
extends RefCounted
|
||||
|
||||
# 2V0 之前 system.py 必然停在这里;2V0 之后这一行要跟着改。
|
||||
const EXPECTED_STOP := "No module named app"
|
||||
|
||||
var fail := 0
|
||||
|
||||
func _ck(ok: bool, message: String) -> void:
|
||||
if not ok:
|
||||
fail += 1
|
||||
printerr("FAIL: " + message)
|
||||
else:
|
||||
print("ok: " + message)
|
||||
|
||||
# 返回失败数;0 表示落在预期的停止点上(或按上面的规则跳过)。
|
||||
func run() -> int:
|
||||
if not ClassDB.class_exists("Metin2PythonHost"):
|
||||
printerr("FAIL: Metin2PythonHost extension is not registered")
|
||||
return 1
|
||||
if not FileAccess.file_exists("res://python27.zip"):
|
||||
print("SKIP: python_host_check(没有 res://python27.zip,需要 -DMTGODOT_EMBED_PYTHON=ON 的构建)")
|
||||
return 0
|
||||
|
||||
# 40250 Main() 先注册 pack,再造 CPythonLauncher —— system.py 是从 pack 里读出来的。
|
||||
var has_pack := AssetRoot.pack_ready()
|
||||
|
||||
var start_error: String = Metin2PythonHost.start("")
|
||||
_ck(start_error == "", "解释器在 app 进程里起来了:" + start_error)
|
||||
if start_error != "":
|
||||
return fail
|
||||
_ck(Metin2PythonHost.is_running(), "is_running() 为真")
|
||||
_ck(Metin2PythonHost.start("") == "", "重复 start() 是空操作")
|
||||
|
||||
# 标准库真的在 sys.path 上(真机是沙盒里那份 python27.zip),codec 注册表也重建过。
|
||||
_ck(Metin2PythonHost.run_line("import os, string, types, codecs") == "", "标准库可 import")
|
||||
_ck(Metin2PythonHost.run_line("assert u'\\xe4'.encode('cp1252') == '\\xe4'") == "", "codec 注册表可用")
|
||||
_ck(Metin2PythonHost.run_line("import sys; assert sys.path") == "", "sys.path 非空")
|
||||
|
||||
if has_pack:
|
||||
# RunMainScript:initpack() + __DEBUG__ + __COMMAND_LINE__ + RunFile("system.py")。
|
||||
var err: String = Metin2PythonHost.run_main_script("")
|
||||
print("system.py -> %s" % err)
|
||||
_ck(err.contains(EXPECTED_STOP),
|
||||
"system.py 停在预期的 '%s'(2V0 之后这里会往前走)" % EXPECTED_STOP)
|
||||
else:
|
||||
print("SKIP: system.py 这一段(没有 40250 Client/pack:%s)" % AssetRoot.client_path())
|
||||
|
||||
Metin2PythonHost.stop()
|
||||
_ck(not Metin2PythonHost.is_running(), "stop() 之后 is_running() 为假")
|
||||
return fail
|
||||
@@ -0,0 +1,15 @@
|
||||
# python_host_test —— 桌面上跑 python_host_check(docs/PORT-PLAN.md 批次 2P step 4)。
|
||||
# godot --headless --path project --script python_host_test.gd
|
||||
# 真机上同一份检查从 client_main.gd 的 MT_TEST_MODE=python 进来。
|
||||
extends SceneTree
|
||||
|
||||
const PythonHostCheck = preload("res://python_host_check.gd")
|
||||
|
||||
func _init() -> void:
|
||||
var fail: int = PythonHostCheck.new().run()
|
||||
if fail == 0:
|
||||
print("PASS: python_host_test (app 进程内 system.py)")
|
||||
quit(0)
|
||||
else:
|
||||
printerr("%d check(s) failed" % fail)
|
||||
quit(1)
|
||||
Reference in New Issue
Block a user