Files
mtgodot-poc/extension/src/platform/ScriptLib/PythonBoot.cpp
T
shenandshen 3981cbe5e6 port: purge obsolete GDScript UI, legacy flows, and deprecated controllers
- Purged 66 obsolete GDScript UI components and mobile prototype UI from project/ui/ (retaining ui_assets.gd)
- Purged deprecated GDScript game scene, flow state machines, and local gameplay controllers (app_flow, game_scene, player_controller, net_world, etc.)
- Purged deprecated local system simulation scripts (consumable_system, chest_system, equip_rules, etc.) and project/fx/
- Purged legacy GDScript unit tests targeting obsolete classes
- Unified viewport back buffer sizing in CGraphicBase/CGraphicDevice/PythonBoot
- Cleaned up client_main.gd and python_host_check.gd for pure native port architecture
- Verified all port tests passing: port_map.py check (0 errors), port_gate.sh macos PASS (19/19 suites), python_game_render_test PASS
2026-09-23 22:48:41 -07:00

650 lines
21 KiB
C++

// 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 "ScriptLib/Resource.h"
#include "UserInterface/StdAfx.h" // initpack and the rest of the module initializer list
#include "EterPythonLib/StdAfx.h"
#include "EterBase/Timer.h"
#include "EterLib/Util.h"
#include "EterLib/GrpBase.h"
#include "../EterLib/HostCursor.h"
#include "UserInterface/PythonNetworkStream.h"
#include "UserInterface/AccountConnector.h"
#include "UserInterface/ServerStateChecker.h"
#include "UserInterface/PythonPlayer.h"
#include "GameLib/FlyingObjectManager.h"
#include "MilesLib/SoundManager.h"
#include "GameLib/RaceManager.h"
#include "GameLib/GameEventManager.h"
#include "EffectLib/EffectManager.h"
#include "GameLib/ItemManager.h"
#include "UserInterface/PythonCharacterManager.h"
#include "UserInterface/PythonItem.h"
#include "UserInterface/PythonShop.h"
#include "UserInterface/PythonExchange.h"
#include "UserInterface/PythonChat.h"
#include "UserInterface/PythonTextTail.h"
#include "UserInterface/PythonNonPlayer.h"
#include "UserInterface/PythonMiniMap.h"
#include "UserInterface/PythonEventManager.h"
#include "UserInterface/PythonBackground.h"
#include "UserInterface/PythonSkill.h"
#include "UserInterface/PythonQuest.h"
#include "UserInterface/PythonMessenger.h"
#include "UserInterface/PythonSafeBox.h"
#include "UserInterface/PythonGuild.h"
#include "UserInterface/MarkManager.h"
#include "UserInterface/GuildMarkDownloader.h"
#include "UserInterface/GuildMarkUploader.h"
#include "UserInterface/PythonSystem.h"
#include "UserInterface/AbstractApplication.h"
#include "UserInterface/PythonApplication.h"
#include "EterBase/Timer.h"
#include "PythonBoot.h"
#include "PythonHost.h"
#include "../EterLib/UIRenderCommands.h"
#include "../EterBase/LogBox.h"
#include <condition_variable>
#include <cstdlib>
#include <memory>
#include <mutex>
#include <pthread.h>
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;
std::unique_ptr<UI::CWindowManager> g_window_manager;
std::unique_ptr<CPythonResource> g_resource;
// 40250 CPythonApplication::m_timer; CSlotWindow's cool time and Process() read it via Instance().
std::unique_ptr<CTimer> g_timer;
// 40250 CPythonApplication::m_pyNetworkStream and m_kAccountConnector (CSingletons); the app loop
// processes them every frame and the net module reaches them through Instance().
std::unique_ptr<CPythonNetworkStream> g_network_stream;
std::unique_ptr<CAccountConnector> g_account_connector;
// 40250 CPythonApplication::m_pyPlayer, m_FlyingManager and m_kEftMgr: the loading phase clears them
// (CPythonNetworkStream::SetLoadingPhase).
std::unique_ptr<CPythonPlayer> g_player;
std::unique_ptr<CSoundManager> g_sound_manager;
std::unique_ptr<CFlyingManager> g_flying_manager;
// 40250 CPythonApplication::m_kServerStateChecker: intrologin.py's channel status list.
std::unique_ptr<CServerStateChecker> g_server_state_checker;
std::unique_ptr<CRaceManager> g_race_manager;
std::unique_ptr<CGameEventManager> g_game_event_manager;
std::unique_ptr<CEffectManager> g_effect_manager;
// PORT: CPythonApplication is not constructed by the Godot host yet. GamePhase calls the
// 40250 IAbstractApplication singleton, so expose the host's current timer and camera surface.
class GameApplicationAdapter final : public IAbstractApplication
{
public:
void GetMousePosition(POINT* point) override { if (point) *point = {}; }
float GetGlobalTime() override { return CTimer::Instance().GetCurrentSecond(); }
float GetGlobalElapsedTime() override { return CTimer::Instance().GetElapsedSecond(); }
void SkipRenderBuffering(DWORD) override {}
void SetServerTime(time_t) override {}
void SetCenterPosition(float x, float y, float z) override
{
CPythonApplication::Instance().SetCenterPosition(x, y, z);
}
void SetEventCamera(const SCameraSetting& setting) override
{
CPythonApplication::Instance().SetEventCamera(setting);
}
void BlendEventCamera(const SCameraSetting&, float) override {}
void SetDefaultCamera() override { CPythonApplication::Instance().SetDefaultCamera(); }
// 40250 CPythonApplication::RunIME* (PythonApplicationEvent.cpp:148-195): CPythonIME's event sink.
void RunIMEUpdate() override { UI::CWindowManager::Instance().RunIMEUpdate(); }
void RunIMETabEvent() override { UI::CWindowManager::Instance().RunIMETabEvent(); }
void RunIMEReturnEvent() override { UI::CWindowManager::Instance().RunIMEReturnEvent(); }
void RunIMEChangeCodePage() override { UI::CWindowManager::Instance().RunChangeCodePage(); }
void RunIMEOpenCandidateListEvent() override { UI::CWindowManager::Instance().RunOpenCandidate(); }
void RunIMECloseCandidateListEvent() override { UI::CWindowManager::Instance().RunCloseCandidate(); }
void RunIMEOpenReadingWndEvent() override { UI::CWindowManager::Instance().RunOpenReading(); }
void RunIMECloseReadingWndEvent() override { UI::CWindowManager::Instance().RunCloseReading(); }
};
std::unique_ptr<GameApplicationAdapter> g_game_application;
// The CPythonApplication members the game phase reaches through Instance() (批次 2V2), in 40250's member
// order. All are platform/pending stand-ins so far: their stub constructors leave scalar members unset, so
// the object is built in zeroed storage and inline header accessors read null/0 rather than garbage.
struct GameSingletons
{
static void* operator new(size_t size) { return std::calloc(1, size); }
static void operator delete(void* p) { std::free(p); }
CItemManager kItemMgr;
CPythonCharacterManager kChrMgr;
CPythonGraphic pyGraphic; // owns CCullingManager (m_CullingManager)
CPythonIME pyIme;
CPythonItem pyItem;
CPythonShop pyShop;
CPythonExchange pyExchange;
CPythonChat pyChat;
CPythonTextTail pyTextTail;
CPythonNonPlayer pyNonPlayer;
CPythonMiniMap pyMiniMap;
CPythonEventManager pyEventManager;
CPythonBackground pyBackground;
CPythonSkill pySkill;
CPythonQuest pyQuest;
CPythonMessenger pyManager;
CPythonSafeBox pySafeBox;
CPythonGuild pyGuild;
CGuildMarkManager kGuildMarkManager;
CGuildMarkDownloader kGuildMarkDownloader;
CGuildMarkUploader kGuildMarkUploader;
CPythonSystem pySystem; // last CPythonApplication member (m_pySystem)
};
std::unique_ptr<GameSingletons> g_game_singletons;
bool fail(std::string* error, const std::string& text)
{
if (error)
*error = text;
return false;
}
// PORT: 40250 RunApp() blocks inside app.Loop() (CPythonApplication::Loop, PythonApplication.cpp:980)
// and only returns to run mainStream.Destroy() when the window closes. Godot owns the frame loop here,
// so system.py runs on its own thread and the two threads pass a baton: exactly one of them runs at
// any time. Loop() hands the baton back after every Process(), and the host hands it over once per
// Godot frame (AppFrame). The script keeps its original control flow — including the code after
// app.Loop() — without ever blocking the Godot main thread for more than one Process().
struct ScriptFiber
{
enum Turn { Host, Script };
std::mutex mutex;
std::condition_variable cv;
Turn turn = Host;
pthread_t thread{};
bool alive = false; // the thread exists and has not been joined
bool finished = false; // RunMainScript's body returned
bool looping = false; // parked inside CPythonApplication::Loop
bool quit = false; // app.Exit / app.Abort / Stop
bool ok = false;
std::string error;
std::string command_line;
};
ScriptFiber g_fiber;
thread_local bool t_on_fiber = false;
// Caller holds the GIL. Returns with the GIL held again after the script has given the baton back.
void hand_to_script()
{
PyThreadState* state = PyEval_SaveThread();
{
std::unique_lock<std::mutex> lock(g_fiber.mutex);
g_fiber.turn = ScriptFiber::Script;
g_fiber.cv.notify_all();
g_fiber.cv.wait(lock, [] { return g_fiber.turn == ScriptFiber::Host; });
}
PyEval_RestoreThread(state);
}
void join_fiber()
{
if (!g_fiber.alive)
return;
PyThreadState* state = PyEval_SaveThread();
pthread_join(g_fiber.thread, nullptr);
PyEval_RestoreThread(state);
g_fiber.alive = false;
}
bool run_main_script_body(const char* lpCmdLine, std::string* error);
void* fiber_main(void*)
{
{
std::unique_lock<std::mutex> lock(g_fiber.mutex);
g_fiber.cv.wait(lock, [] { return g_fiber.turn == ScriptFiber::Script; });
}
t_on_fiber = true;
PyGILState_STATE gil = PyGILState_Ensure();
std::string error;
const bool ok = run_main_script_body(g_fiber.command_line.c_str(), &error);
PyGILState_Release(gil);
std::lock_guard<std::mutex> lock(g_fiber.mutex);
g_fiber.ok = ok;
g_fiber.error = error;
g_fiber.finished = true;
g_fiber.looping = false;
g_fiber.turn = ScriptFiber::Host;
g_fiber.cv.notify_all();
return nullptr;
}
}
void init_2v0_gameplay_stubs();
namespace PythonBoot
{
std::string CurrentMapName()
{
if (!g_game_singletons)
return {};
return CPythonBackground::Instance().GetWarpMapName();
}
bool IsRunning()
{
return g_launcher != nullptr;
}
void SetUISize(int width, int height)
{
if (!g_window_manager || width <= 0 || height <= 0) return;
g_window_manager->SetScreenSize(width, height);
g_window_manager->SetResolution(width, height);
UIRenderSetSize(width, height);
CGraphicBase::SetBackBufferSize(width, height);
if (g_game_singletons)
{
g_game_singletons->pySystem.GetConfig()->width = width;
g_game_singletons->pySystem.GetConfig()->height = height;
}
}
void UIMouseMove(int x, int y)
{
MtHostSetCursor(x, y);
if (g_window_manager) g_window_manager->RunMouseMove(x, y);
}
void UIMouseButton(int button, bool pressed, int x, int y)
{
MtHostSetCursor(x, y);
if (!g_window_manager) return;
g_window_manager->RunMouseMove(x, y);
switch (button)
{
case 1: if (pressed) g_window_manager->RunMouseLeftButtonDown(x, y); else g_window_manager->RunMouseLeftButtonUp(x, y); break;
case 2: if (pressed) g_window_manager->RunMouseRightButtonDown(x, y); else g_window_manager->RunMouseRightButtonUp(x, y); break;
case 3: if (pressed) g_window_manager->RunMouseMiddleButtonDown(x, y); else g_window_manager->RunMouseMiddleButtonUp(x, y); break;
default: break;
}
}
// 40250 CPythonApplication::OnKeyDown/OnKeyUp (PythonApplicationEvent.cpp:130-146): ESC first goes
// to RunPressEscapeKey (the OnPressEscapeKey chain every dialog closes on), then to RunKeyDown.
void UIKey(int key, bool pressed)
{
if (!g_window_manager) return;
if (pressed)
{
if (DIK_ESCAPE == key)
g_window_manager->RunPressEscapeKey();
g_window_manager->RunKeyDown(key);
}
else
g_window_manager->RunKeyUp(key);
}
// 40250 CPythonApplication::WindowProcedure (PythonApplicationProcedure.cpp:111-117): WM_CHAR goes to
// CPythonIME::WMChar and WM_KEYDOWN to OnIMEKeyDown (the VK code, next to DirectInput's DIK stream). The host
// delivers characters as Unicode code points; the 40250 window is an ANSI window, so a character that the input
// code page holds as one byte arrives as that byte, like WM_CHAR. PORT: anything else is what a Windows IME
// commits through WM_IME_COMPOSITION/GCS_RESULTSTR, which CIME::ResultProcess inserts as a wide string.
void UIChar(unsigned codepoint)
{
if (!g_game_singletons || !codepoint) return;
CPythonIME& ime = CPythonIME::Instance();
if (codepoint < 0x80)
{
ime.WMChar(NULL, WM_CHAR, codepoint, 0);
return;
}
const wchar_t wide = wchar_t(codepoint);
char bytes[8];
BOOL used_default = FALSE;
const int n = WideCharToMultiByte(CIME::ms_uInputCodePage, 0, &wide, 1, bytes, sizeof(bytes), "?", &used_default);
if (n == 1 && !used_default)
{
ime.WMChar(NULL, WM_CHAR, (unsigned char)bytes[0], 0);
return;
}
// 40250 CIME::WMComposition with GCS_RESULTSTR: ResultProcess inserts the committed string, then OnUpdate.
if (!CIME::ms_bCaptureInput)
return;
struct ResultAccess : CPythonIME
{
static void Insert(CPythonIME& ime, wchar_t* text, int len) { (ime.*(&ResultAccess::InsertString))(text, len); }
};
wchar_t text[1] = { wide };
ResultAccess::Insert(ime, text, 1);
if (CIME::ms_pEvent)
CIME::ms_pEvent->OnUpdate();
}
void UIIMEKeyDown(int vkey)
{
if (g_window_manager) g_window_manager->RunIMEKeyDown(vkey);
}
void UIUpdate()
{
// While system.py is inside app.Loop(), one host frame is one CPythonApplication::Process(),
// which updates and renders the window tree itself.
if (IsAppLooping()) { AppFrame(); return; }
if (g_window_manager) g_window_manager->Update();
}
void UIRender()
{
if (IsAppLooping()) return;
UIRenderBeginFrame();
if (g_window_manager) g_window_manager->Render();
}
bool Start(const char* stdlib_path, std::string* error)
{
if (g_launcher)
return true;
// 40250 Main (UserInterface.cpp:410): if (LocaleService_LoadGlobal(hInstance)) SetDefaultCodePage(...).
LocaleService_LoadConfig("locale.cfg");
SetDefaultCodePage(LocaleService_GetCodePage());
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
// The script fiber (RunMainScript) is a second OS thread; 2.7 creates the GIL lazily.
PyEval_InitThreads();
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);
}
g_timer = std::make_unique<CTimer>();
g_resource = std::make_unique<CPythonResource>();
g_window_manager = std::make_unique<UI::CWindowManager>();
g_network_stream = std::make_unique<CPythonNetworkStream>();
g_account_connector = std::make_unique<CAccountConnector>();
g_player = std::make_unique<CPythonPlayer>();
g_sound_manager = std::make_unique<CSoundManager>();
g_flying_manager = std::make_unique<CFlyingManager>();
g_server_state_checker = std::make_unique<CServerStateChecker>();
g_race_manager = std::make_unique<CRaceManager>();
g_game_event_manager = std::make_unique<CGameEventManager>();
g_effect_manager = std::make_unique<CEffectManager>();
g_game_application = std::make_unique<GameApplicationAdapter>();
g_game_singletons = std::make_unique<GameSingletons>();
unsigned ui_w = 0, ui_h = 0;
UIRenderGetSize(&ui_w, &ui_h);
if (ui_w && ui_h)
{
g_game_singletons->pySystem.GetConfig()->width = ui_w;
g_game_singletons->pySystem.GetConfig()->height = ui_h;
}
g_game_singletons->pySystem.GetDisplaySettings(); // 40250 CPythonApplication::Create (PythonApplication.cpp:1233)
g_flying_manager->SetMapManagerPtr(&g_game_singletons->pyBackground); // 40250 CPythonApplication ctor (PythonApplication.cpp:82)
return true;
}
}
namespace
{
// Mirrors 40250 RunMainScript. Runs on the script fiber.
bool run_main_script_body(const char* lpCmdLine, std::string* error)
{
CPythonLauncher& pyLauncher = *g_launcher;
// The module initializers follow 40250's order; missing modules arrive in later 2V steps.
initpack();
initdbg();
initime();
initgrp();
initgrpImage();
initgrpText();
initwndMgr();
initapp();
initsystem();
initchr();
initchrmgr();
initPlayer();
initItem();
initNonPlayer();
initTrade();
initChat();
initTextTail();
initnet();
initMiniMap();
initEvent();
initeffect();
initfly();
initsnd();
initeventmgr();
initshop();
initskill();
initquest();
initBackground();
initMessenger();
initsafebox();
initguild();
initServerStateChecker();
init_2v0_gameplay_stubs();
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());
}
// system.py catches failures in prototype.py and calls dbg.LogBox. Its outer
// RunFile then succeeds, so surface that recorded failure to the Godot host.
if (!LastLogBoxMessage().empty())
return fail(error, "RunMain Error: " + LastLogBoxMessage());
}
NANOEND
return true;
}
}
namespace PythonBoot
{
bool RunMainScript(const char* lpCmdLine, std::string* error)
{
if (!g_launcher)
return fail(error, "PythonBoot::Start has not run");
if (g_fiber.alive)
return fail(error, "RunMainScript is already running");
g_fiber.turn = ScriptFiber::Host;
g_fiber.finished = g_fiber.looping = g_fiber.quit = g_fiber.ok = false;
g_fiber.error.clear();
g_fiber.command_line = lpCmdLine ? lpCmdLine : "";
// 40250's main thread has the default 1 MB Windows stack; Python frames are deep during
// uiScriptLocale/ui imports, so give the script thread more than a secondary thread's default.
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 16u * 1024u * 1024u);
const int rc = pthread_create(&g_fiber.thread, &attr, fiber_main, nullptr);
pthread_attr_destroy(&attr);
if (rc != 0)
return fail(error, "cannot start the script thread");
g_fiber.alive = true;
hand_to_script(); // until system.py parks in app.Loop() or returns
if (g_fiber.finished)
{
join_fiber();
if (!g_fiber.ok)
return fail(error, g_fiber.error);
}
return true;
}
bool IsAppLooping()
{
return g_fiber.alive && g_fiber.looping && !g_fiber.finished;
}
bool AppFrame()
{
if (!IsAppLooping())
return false;
hand_to_script();
if (g_fiber.finished)
join_fiber();
return IsAppLooping();
}
bool AppLoopWait()
{
if (!t_on_fiber)
return false;
g_fiber.looping = true;
if (g_fiber.quit)
return false;
PyThreadState* state = PyEval_SaveThread();
{
std::unique_lock<std::mutex> lock(g_fiber.mutex);
g_fiber.turn = ScriptFiber::Host;
g_fiber.cv.notify_all();
g_fiber.cv.wait(lock, [] { return g_fiber.turn == ScriptFiber::Script; });
}
PyEval_RestoreThread(state);
return !g_fiber.quit;
}
void AppRequestQuit()
{
g_fiber.quit = 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;
}
bool Evaluate(const char* expression, std::string* result, std::string* error)
{
// A host/test helper with no 40250 counterpart: RunLine stores str(expression) in __main__, read back here.
if (!RunLine(("__mt_evaluate = str(" + std::string(expression ? expression : "None") + ")").c_str(), error))
return false;
PyObject* main = PyImport_AddModule("__main__");
PyObject* value = main ? PyObject_GetAttrString(main, "__mt_evaluate") : nullptr;
if (!value || !PyString_Check(value))
{
Py_XDECREF(value);
PyErr_Clear();
return fail(error, "evaluate: no result");
}
result->assign(PyString_AsString(value));
Py_DECREF(value);
PyObject_DelAttrString(main, "__mt_evaluate");
return true;
}
void Stop()
{
if (!g_launcher)
return;
// In 40250, closing the window ends Loop(); RunApp() then runs mainStream.Destroy() and returns.
if (g_fiber.alive)
{
AppRequestQuit();
while (g_fiber.alive && !g_fiber.finished)
hand_to_script();
join_fiber();
}
g_fiber.finished = false;
// 40250 Main(): pyLauncher.Clear() runs before app->Destroy()/delete app, so the window manager
// (a CPythonApplication member) is still alive while Py_Finalize runs the windows' __del__.
g_launcher->Clear();
g_game_singletons.reset();
g_game_application.reset();
g_effect_manager.reset();
g_game_event_manager.reset();
g_race_manager.reset();
g_server_state_checker.reset();
g_flying_manager.reset();
g_sound_manager.reset();
g_player.reset();
g_account_connector.reset();
g_network_stream.reset();
g_window_manager.reset();
g_resource.reset();
g_timer.reset();
g_launcher.reset();
}
}
// winuser.h (declared in the port's windows.h shim). WM_QUIT ends 40250's CPythonApplication::Loop;
// here the script fiber's loop ends the same way.
void PostQuitMessage(int)
{
PythonBoot::AppRequestQuit();
}