Port PythonNetworkStream with its HandShake/Login/Select/Loading/Offline phases, AccountConnector and PythonNetworkStreamModule verbatim; the net stub goes. PythonBoot creates the network stream, account connector, player, flying manager and effect manager like CPythonApplication, and app.Create/Process drive CNetworkDevice and both connectors. Packet.h long fields become 32-bit LONG; port.packet checks 38 login/select/loading packet sizes against wire_classic.h. Unported units in the link closure live under platform/pending/ as stand-ins. The macOS gate now builds all and finds iconv. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
433 lines
13 KiB
C++
433 lines
13 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 "UserInterface/PythonNetworkStream.h"
|
|
#include "UserInterface/AccountConnector.h"
|
|
#include "UserInterface/PythonPlayer.h"
|
|
#include "GameLib/FlyingObjectManager.h"
|
|
#include "EffectLib/EffectManager.h"
|
|
#include "PythonBoot.h"
|
|
#include "PythonHost.h"
|
|
#include "../EterLib/UIRenderCommands.h"
|
|
#include "../EterBase/LogBox.h"
|
|
|
|
#include <condition_variable>
|
|
#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). PythonPlayer and FlyingObjectManager are platform/pending stand-ins.
|
|
std::unique_ptr<CPythonPlayer> g_player;
|
|
std::unique_ptr<CFlyingManager> g_flying_manager;
|
|
std::unique_ptr<CEffectManager> g_effect_manager;
|
|
|
|
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
|
|
{
|
|
|
|
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);
|
|
}
|
|
|
|
void UIMouseMove(int x, int y)
|
|
{
|
|
if (g_window_manager) g_window_manager->RunMouseMove(x, y);
|
|
}
|
|
|
|
void UIMouseButton(int button, bool pressed, int x, int 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);
|
|
}
|
|
|
|
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(...).
|
|
// PORT: the platform LocaleService is already the supplied locale.cfg (en, code page 1252).
|
|
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_flying_manager = std::make_unique<CFlyingManager>();
|
|
g_effect_manager = std::make_unique<CEffectManager>();
|
|
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();
|
|
initgrp();
|
|
initgrpImage();
|
|
initgrpText();
|
|
initwndMgr();
|
|
initapp();
|
|
initnet();
|
|
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;
|
|
}
|
|
|
|
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_effect_manager.reset();
|
|
g_flying_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();
|
|
}
|