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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user