Files
mtgodot-poc/docs/PYTHON-EMBED-EVAL.md
T
shenleiandClaude Opus 5 b02c49bb2f spike(python): CPython 2.7.18 runs the 40250 root scripts on Android arm64
libpython2.7.a cross-builds with NDK 27.2 (API 24) after disabling
HAVE_LANGINFO_H; 30 C modules built in. On a OnePlus 13 (API 36) the
harness runs system.py -> prototype.RunApp() and imports the root modules
with results identical to macOS (66/74; the 8 failures are stub values or
dev-only scripts). py_embed_spike.py now isolates __main__ like
CPythonLauncher and reports stdlib usage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 18:16:44 +09:00

7.8 KiB
Raw Blame History

内嵌 Python 运行 40250 UI 的评估

日期:2026-09-22。目的:判断能否不翻译 40250 的 Python UI,而是内嵌解释器原样运行 Client/Eternexus/root/*.pyuiscript/,只移植它调用的 C++ 模块。

40250 的实际情况

数据
解释器 CPython 2.7.6 内嵌(extern/include/Python-2.7python27.lib
启动 ScriptLib/PythonLauncher.cppCPythonLauncher::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, ehas_keyiteritems 等)
语义风险 约 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.pyapp.Loop、Destroy。之后逐个导入全部 74 个 root 模块,66 个成功。 剩下 8 个失败都不是解释器或脚本的问题:

  • prototype:已经由 system.py 执行过,再导入时 RunApp 结束触发 SystemExit,属正常;
  • gameinterfacemodulechr.MOTION_SKILL + 121 这类常量需要真实整数(真实模块用 PyModule_AddIntConstant 提供);
  • introloadingconsolemodulelocale 数据路径(app.GetLocaleServiceName 等返回值决定走哪个 locale 目录);
  • system:只能作为 __main__ 运行;rootlibcythonizertest_affect 是开发用脚本,40250 运行时也不加载。

引导过程中脚本调用了 33 个 C++ 模块函数(app.CreatewndMgr.Registernet.SetHandler 等)。

结论:在 Python 2.7 上,40250 的脚本和导入机制可以原封不动运行;工作量集中在 C++ 模块本身。

方案对比

方案 脚本改动 与 40250 一致性 平台风险 结论
A. 内嵌 CPython 2.7.18(静态链接) 最高:解释器语义、/ 整除、字节串、C APIPy_InitModulePyTuple_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:直接编译,无风险。
  • Androidarm64NDK 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__ localepsyco 有 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/*.pyuiscript/ 直接从 pack 读取,和 40250 相同。port-map 中 Client/root 的 3350 个函数 全部记为 N_A(原样运行,无需移植),python 层剩下约 1540 个 C++ 函数要移植。

Android 验证(2026-09-22,已通过)

设备:一加 13PJZ110),Android API 36arm64-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/,用我们自己的 CMake 编译(macOS / Android / iOS 共用一份源码清单和 pyconfig.h),静态链接进 libmtgodot
  2. pack 模块走 asset_io,标准库的纯 Python 文件放进资源包,用 system.py 自带的导入钩子加载;
  3. 在 APK 里跑同样的引导流程(C++ 桩模块),然后按同样方法验证 iOS;
  4. 之后 python 层才开始移植:ScriptLib/PythonLauncher.cppEterPythonLib 窗口系统 → *Module.cpp