# 内嵌 Python 运行 40250 UI 的评估 日期:2026-09-22。目的:判断能否不翻译 40250 的 Python UI,而是内嵌解释器原样运行 `Client/Eternexus/root/*.py` 和 `uiscript/`,只移植它调用的 C++ 模块。 ## 40250 的实际情况 | 项 | 数据 | | --- | --- | | 解释器 | CPython 2.7.6 内嵌(`extern/include/Python-2.7`、`python27.lib`) | | 启动 | `ScriptLib/PythonLauncher.cpp`:`CPythonLauncher::Create` 注入 `TRUE`/`FALSE` 到 `__builtin__`,然后把 `system.py` 作为 `__main__` 运行(`__main__` 的 `__builtins__` 是模块,不是 dict);`system.py` 替换 `__import__`,从 pack 读源码并 `compile` | | 脚本 | `root/*.py` 90 个(其中 74 个 `.py` 模块),`uiscript/uiscript/*.py` 94 个 | | 语法 | 154 个文件在 Python 2.7.18 下全部能编译;Python 3 下有 56 个编译失败;约 292 行只在 Python 2 下成立(`print` 语句、`except X, e`、`has_key`、`iteritems` 等) | | 语义风险 | 约 185 行在整数之间用 `/`(Python 2 是整除,Python 3 是浮点);字符串是 CP949 字节串 | | C++ 暴露的模块 | 35 个(`app` `net` `player` `chr` `chrmgr` `wndMgr` `grp` `item` `skill` 等),约 1160 个函数、约 994 个常量 | | 需要移植的 C++ | `UserInterface/*Module.cpp` + `EterPythonLib`(窗口系统)+ `ScriptLib`,共约 2.44 万行 | | 脚本实际引用 | 1627 个不同的 `模块.属性`(含常量) | 对比:如果逐个翻译成 GDScript,需要翻译 `root` 下的 3350 个函数,外加 uiscript 数据文件;而这些 C++ 模块在任何方案里都要移植(它们就是 UI 和游戏逻辑的接口)。现在仓库里 `project/ui/*.gd` 有 60 个文件、 2.39 万行,当初依据的是错误版本的脚本(m2dev `assets/root`),无论选哪条路都要重做。 ## 原型验证(已完成) `.agents/skills/metin2-40250-parity-audit/scripts/py_embed_spike.py`,在本机 Python 2.7.18 下: - 按 `CPythonLauncher::Create` 注入 `TRUE`/`FALSE`,所有 C++ 模块用返回 0 的桩代替,`pack` 用目录代替; - 原样运行 40250 的 `system.py`,再通过它自己的 `__pack_import` 导入全部 74 个 root 模块。 结果:**不改任何脚本**,`system.py` 引导后 `prototype.py` 完整执行了 `RunApp()`:创建 MainStream、进入 logo 阶段、加载 `UIScript/PopupDialog.py`、`app.Loop`、Destroy。之后逐个导入全部 74 个 root 模块,66 个成功。 剩下 8 个失败都不是解释器或脚本的问题: - `prototype`:已经由 `system.py` 执行过,再导入时 `RunApp` 结束触发 `SystemExit`,属正常; - `game`、`interfacemodule`:`chr.MOTION_SKILL + 121` 这类常量需要真实整数(真实模块用 `PyModule_AddIntConstant` 提供); - `introloading`、`consolemodule`:locale 数据路径(`app.GetLocaleServiceName` 等返回值决定走哪个 locale 目录); - `system`:只能作为 `__main__` 运行;`rootlibcythonizer`、`test_affect` 是开发用脚本,40250 运行时也不加载。 引导过程中脚本调用了 33 个 C++ 模块函数(`app.Create`、`wndMgr.Register`、`net.SetHandler` 等)。 结论:在 Python 2.7 上,40250 的脚本和导入机制可以原封不动运行;工作量集中在 C++ 模块本身。 ## 方案对比 | 方案 | 脚本改动 | 与 40250 一致性 | 平台风险 | 结论 | | --- | --- | --- | --- | --- | | A. 内嵌 CPython 2.7.18(静态链接) | 无 | 最高:解释器语义、`/` 整除、字节串、C API(`Py_InitModule`、`PyTuple_GetInteger`)都和 40250 相同,`*Module.cpp` 可以几乎原样复制 | 需要自己为 Android/iOS 交叉编译;2.7 已停止维护 | **推荐** | | B. 内嵌 CPython 3.13+ | 56 个文件要转换,约 185 处 `/` 需逐一判断 | 中:字符串/整除/字典顺序语义不同,转换本身会引入偏差;C API 要改写 | 3.13 起官方支持 Android/iOS | A 失败时的备选 | | C. pocketpy 等轻量解释器 | 需要大量改动 | 低:只支持 Python 3 子集 | 小 | 不推荐 | | D. 翻译成 GDScript | 全部重写 3350 个函数 | 取决于翻译质量,难以机械核对 | 无 | 不推荐 | ### 方案 A 的平台问题 - macOS / Linux / Windows:都需要各自编译和验证;Windows 使用 `PCbuild`/`PC/pyconfig.h`,不走 Unix configure。 - Android(arm64,NDK 27):CPython 2.7 能用 NDK 编译(python-for-android、Kivy 曾长期支持),但需要补丁: 关闭 `dlopen` 扩展、把用到的标准库 C 模块静态编进去、`pyconfig.h` 按 NDK 调整。 - iOS:同样静态链接(Kivy-ios 曾支持 2.7)。App Store 允许包内自带的解释型代码(不下载代码即可)。 - 标准库只需要极少一部分:脚本用到的是 `sys` `os` `marshal` `imp` `types` `copy` `math` `traceback` `_weakref` `__builtin__` `locale`,`psyco` 有 try 保护。可以冻结为内置模块,不需要带整个 `Lib/`。 - 2.7 停止维护的风险:解释器只运行包内自带的脚本;服务器数据先经过 C++ 解析再以普通值交给 Python, 不会执行远端代码。 ## 采用方案 A 后的结构 ``` extension/src/port/ScriptLib/PythonLauncher.cpp # CPythonLauncher,几乎原样 extension/src/port/EterPythonLib/PythonWindow*.cpp # 窗口树、焦点、命中、事件:逻辑照搬 extension/src/port/UserInterface/*Module.cpp # net/player/chr/... 模块,调用已移植的 CPython* 单例 extension/src/platform/ui/ # 适配层:CGraphicImageInstance/CGraphicTextInstance # 的绘制 -> Godot RenderingServer/CanvasItem extension/third_party/cpython-2.7.18/ # 静态库 ``` Godot 这边只剩一个宿主 Control:把输入转发给 `CPythonWindowManager`,每帧调用它的 Update/Render。 `root/*.py` 和 `uiscript/` 直接从 pack 读取,和 40250 相同。port-map 中 `Client/root` 的函数在其 参考字节、资源清单和目标平台运行证据都成立后记为 `RUN_AS_IS`,不能记为 `N_A`;python 层剩下约 1540 个 C++ 函数要移植。 ## Android 验证(2026-09-22,已通过) 设备:一加 13(PJZ110),Android API 36,arm64-v8a。复现:`tools/py_embed_android/build-and-run.sh`。 - **编译**:CPython 2.7.18 官方源码,用 NDK 27.2 的 `aarch64-linux-android24-clang` 交叉 configure, `make libpython2.7.a` 一次通过。只需要一处改动:关掉 `HAVE_LANGINFO_H`,因为 bionic 从 API 26 才声明 `nl_langinfo`。 另外 30 个 C 模块静态编进去,包括 `posix` `math` `time` `_struct` `operator` `itertools` `_collections` `cStringIO` `cPickle` `_locale` `binascii` `datetime` `select` `fcntl` 等。全部编译通过,库大小 13 MB(含调试信息)。 - **链接**:测试程序 `tools/py_embed_android/main.c` 链接 `libpython2.7.a -lm -ldl`,没有任何未定义符号。 - **运行**:`Py_NoSiteFlag`/`Py_IgnoreEnvironmentFlag`/`Py_FrozenFlag` 启动,标准库以不压缩的 zip(8 MB) 加入 `sys.path`,然后运行 `py_embed_spike.py`:**结果与 macOS 逐项相同**,66/74 个模块加载,失败的 8 个完全相同, 引导期调用的 33 个 C++ 函数也完全相同。 - **尚未验证**: - 这次是用 `adb shell` 运行的独立可执行文件,还没有在 APK 的 app 进程里跑过; - 标准库是从文件系统读的,还没有走 PCK 或 `asset_io`。 这两点属于集成工作,不涉及解释器本身能不能跑。 ## 下一步 1. 把 CPython 2.7.18 放进 `extension/third_party/`,共用源码/静态模块清单,但为 macOS、Linux、Windows、 Android、iOS 分别生成或维护 `pyconfig.h`,静态链接进 `libmtgodot`; 2. `pack` 模块走 `asset_io`,继续负责 40250 的 root/uiscript。标准库沿用已验证的 `python27.zip + sys.path` 方案:桌面端放在解释器可读的真实文件系统路径,移动端首次启动从应用资源复制到应用沙盒并校验 sha256;静态启用 `zipimport`。不要依赖 `system.py` 的简单 `name + '.py'` 钩子加载带 package/dotted import 的标准库; 3. 在 APK app 进程里跑同样的引导流程(C++ 桩模块),然后分别验证 iOS、Linux 和 Windows; 4. 按 `docs/PORT-PLAN.md` 的 2V0-2V3 切片依次替换桩模块和旧运行路径。