Files
metin2-client/src/platform/ScriptLib/PythonBoot.cpp
T
shenleiandClaude Opus 5.5 f9be09d2c2 fix(touch): show the joystick and skill buttons only in the game phase
The controls keyed off CPythonBackground's warp map name, which the
loading window sets before the game window exists, so the loading screen
already showed them. Also require CPythonNetworkStream's phase to be
"Game" (PORT_IsGamePhase, exposed as PythonBoot::InGamePhase).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:33:46 +09:00

1111 lines
35 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 "EterPythonLib/PythonWindow.h"
#include "EterBase/Timer.h"
#include "EterLib/Util.h"
#include "EterLib/GrpBase.h"
#include "EterLib/GrpImage.h"
#include "EterLib/Camera.h"
#include "../EterLib/HostCursor.h"
#include "UserInterface/PythonNetworkStream.h"
#include "UserInterface/AccountConnector.h"
#include "UserInterface/PythonExceptionSender.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 "../UserInterface/UserInterface.h"
#include "PythonHost.h"
#include "../EterLib/UIRenderCommands.h"
#include "../EterBase/LogBox.h"
#include "../UserInterface/ServerClock.h"
#include <atomic>
#include <condition_variable>
#include <cstdlib>
#include <filesystem>
#include <memory>
#include <mutex>
#include <pthread.h>
#if defined(__APPLE__)
#include <execinfo.h>
#include <signal.h>
#include <unistd.h>
#endif
#if defined(__linux__) || defined(__ANDROID__)
#include <unistd.h>
#endif
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;
// 40250 Main(): CPythonExceptionSender pyExceptionSender; SetExceptionSender(&pyExceptionSender);
std::unique_ptr<CPythonExceptionSender> g_exception_sender;
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)
CPythonApplication::Instance().GetMousePosition(point);
}
float GetGlobalTime() override { return CTimer::Instance().GetCurrentSecond(); }
float GetGlobalElapsedTime() override { return CTimer::Instance().GetElapsedSecond(); }
void SkipRenderBuffering(DWORD) override {}
void SetServerTime(time_t server_time) override { SetPlatformServerTime(server_time); }
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 g_host_hardware_cursor_enabled = false;
bool g_touch_input = false;
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;
std::atomic<int> g_script_thread_id{0};
// 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;
#if defined(__linux__) || defined(__ANDROID__)
g_script_thread_id = int(gettid());
#endif
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
{
void SetHostHardwareCursorEnabled(bool enabled)
{
g_host_hardware_cursor_enabled = enabled;
}
bool HostHardwareCursorEnabled()
{
return g_host_hardware_cursor_enabled;
}
int CursorShape()
{
return g_game_singletons ? CPythonApplication::Instance().GetCursorNum() : 0;
}
bool CursorVisible()
{
return !g_game_singletons || CPythonApplication::Instance().GetCursorVisible();
}
std::string CurrentMapName()
{
if (!g_game_singletons)
return {};
return CPythonBackground::Instance().GetWarpMapName();
}
bool InGamePhase()
{
return g_network_stream && g_network_stream->PORT_IsGamePhase();
}
bool IsRunning()
{
return g_launcher != nullptr;
}
static int g_ui_safe_inset = 0;
void SetUISafeInset(int inset)
{
g_ui_safe_inset = std::max(0, inset);
}
void SetUISize(int width, int height)
{
if (!g_window_manager || width <= 0 || height <= 0) return;
const int inset = std::min(g_ui_safe_inset, width / 8);
g_window_manager->SetScreenSize(width - 2 * inset, height);
g_window_manager->SetResolution(width, height);
g_window_manager->SetScreenOffsetX(inset);
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 update_3d_pick_ray()
{
if (!g_game_singletons || !g_window_manager)
return;
if (!CPythonBackground::Instance().IsMapReady())
return;
long lx = 0, ly = 0;
g_window_manager->GetMousePosition(lx, ly);
// The pick ray goes through the full-screen 3D view: screen coordinates and resolution.
lx += g_window_manager->GetScreenOffsetX();
long sw = 0, sh = 0;
g_window_manager->GetResolution(sw, sh);
if (sw <= 0 || sh <= 0)
return;
CScreen s;
s.SetPerspective(30.0f, g_window_manager->GetAspect(), 100.0f, CPythonBackground::Instance().GetFarClip());
s.UpdateViewMatrix();
s.BuildViewFrustum();
s.SetCursorPosition(lx, ly, sw, sh);
POINT ptMouse{static_cast<LONG>(lx), static_cast<LONG>(ly)};
CPythonItem::Instance().Update(ptMouse);
CPythonCharacterManager::Instance().Pick();
}
void UIMouseMove(int x, int y)
{
MtHostSetCursor(x, y);
if (g_game_singletons)
{
CPythonApplication::Instance().OnMouseMove(x, y);
if (g_window_manager)
{
long lx = x, ly = y;
g_window_manager->GetMousePosition(lx, ly);
MtHostSetCursor(static_cast<int>(lx), static_cast<int>(ly));
}
update_3d_pick_ray();
return;
}
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;
if (g_game_singletons)
CPythonApplication::Instance().OnMouseMove(x, y);
else
g_window_manager->RunMouseMove(x, y);
{
long lx = x, ly = y;
g_window_manager->GetMousePosition(lx, ly);
MtHostSetCursor(static_cast<int>(lx), static_cast<int>(ly));
}
update_3d_pick_ray();
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);
if (g_game_singletons)
{
CCamera* pkCmrCur = CCameraManager::Instance().GetCurrentCamera();
if (pkCmrCur && pkCmrCur->IsDraging())
CPythonApplication::Instance().OnMouseMiddleButtonUp(x, y);
}
}
break;
case 3:
if (g_game_singletons)
{
if (pressed) CPythonApplication::Instance().OnMouseMiddleButtonDown(x, y);
else CPythonApplication::Instance().OnMouseMiddleButtonUp(x, y);
}
if (pressed) g_window_manager->RunMouseMiddleButtonDown(x, y); else g_window_manager->RunMouseMiddleButtonUp(x, y);
break;
default: break;
}
}
void UIMouseWheel(int nLen)
{
CCameraManager& rkCmrMgr = CCameraManager::Instance();
CCamera* pkCmrCur = rkCmrMgr.GetCurrentCamera();
if (pkCmrCur)
pkCmrCur->Wheel(nLen);
}
// 40250 CPythonApplication::WindowProcedure WM_ACTIVATEAPP (PythonApplicationProcedure.cpp:58): the sound
// is muted while the window is inactive.
void UIActivate(bool active)
{
if (!g_sound_manager) return;
if (active)
g_sound_manager->RestoreVolume();
else
g_sound_manager->SaveVolume();
}
// 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)
{
CPythonApplication::Instance().KeyDown(key);
if (DIK_ESCAPE == key)
g_window_manager->RunPressEscapeKey();
g_window_manager->RunKeyDown(key);
}
else
{
CPythonApplication::Instance().KeyUp(key);
g_window_manager->RunKeyUp(key);
}
}
void SetMoveDirection(float angleDeg, bool moving)
{
if (!g_game_singletons) return;
if (moving)
// The touch stick only steers the player; the camera turns by the right-hand drag.
CPythonPlayer::Instance().NEW_MoveToDirection(angleDeg, false);
else
CPythonPlayer::Instance().NEW_Stop();
}
void SetAttackKey(bool pressed)
{
if (!g_game_singletons) return;
CPythonPlayer::Instance().SetAttackKeyState(pressed);
}
void UseLocalQuickSlot(int localSlotIndex)
{
if (!g_game_singletons || localSlotIndex < 0 || localSlotIndex >= 8) return;
CPythonPlayer::Instance().RequestUseLocalQuickSlot(static_cast<DWORD>(localSlotIndex));
}
void AutoHuntToggle()
{
std::string error;
if (g_game_singletons)
RunLine("__import__('mt_autohunt').Toggle()", &error);
}
void AutoHuntOpenSettings()
{
std::string error;
if (g_game_singletons)
RunLine("__import__('mt_autohunt').OpenSettings()", &error);
}
bool AutoHuntIsEnabled()
{
return g_game_singletons && CPythonPlayer::Instance().AutoHunt_IsEnabled();
}
bool AutoHuntAvailable()
{
return g_game_singletons && CPythonPlayer::Instance().NEW_GetMainActorPtr() != nullptr;
}
bool TryAssignAttachedObjectToLocalQuickSlot(int localSlotIndex)
{
if (!g_game_singletons || localSlotIndex < 0 || localSlotIndex >= 8) return false;
std::string attached;
std::string error;
if (!Evaluate("__import__('mouseModule').mouseController.isAttached()", &attached, &error) ||
(attached != "True" && attached != "1"))
return false;
const std::string command =
"_stream.curPhaseWindow.interface.wndTaskBar.AddQuickSlot(" + std::to_string(localSlotIndex) + ")";
return RunLine(command.c_str(), &error);
}
namespace
{
// The icons are .sub images (a rect of a shared texture). A resource only loads while referenced, and
// on touch hosts the taskbar slots that would reference it are hidden, so the overlay holds one.
bool quick_slot_image_uv(CGraphicImage* image, std::string* texture, float uv[4])
{
if (!image) return false;
static std::vector<CGraphicImage::TRef> s_heldIcons;
if (std::none_of(s_heldIcons.begin(), s_heldIcons.end(),
[image](const CGraphicImage::TRef& ref) { return ref.GetPointer() == image; }))
s_heldIcons.emplace_back(image);
CGraphicTexture* tex = image->GetTexturePointer();
if (image->IsEmpty() || !tex || tex->GetWidth() <= 0 || tex->GetHeight() <= 0) return false;
*texture = UIRenderTextureName(tex);
if (texture->empty()) return false;
// CGraphicImageInstance::OnRender's texture coordinates.
const RECT& rc = image->GetRectReference();
uv[0] = float(rc.left) / float(tex->GetWidth());
uv[1] = float(rc.top) / float(tex->GetHeight());
uv[2] = float(rc.right) / float(tex->GetWidth());
uv[3] = float(rc.bottom) / float(tex->GetHeight());
return true;
}
}
bool LocalQuickSlotIcon(int localSlotIndex, std::string* texture, float uv[4], int* count)
{
if (!g_game_singletons || localSlotIndex < 0 || localSlotIndex >= 8) return false;
DWORD type = SLOT_TYPE_NONE;
DWORD position = 0;
CPythonPlayer& player = CPythonPlayer::Instance();
player.GetLocalQuickSlotData(static_cast<DWORD>(localSlotIndex), &type, &position);
*count = 0;
if (type == SLOT_TYPE_SKILL) {
CPythonSkill::TSkillData* skillData = nullptr;
if (!CPythonSkill::Instance().GetSkillData(player.GetSkillIndex(position), &skillData) || !skillData)
return false;
const int grade = std::clamp(player.GetSkillGrade(position), 0, CPythonSkill::SKILL_GRADE_COUNT - 1);
CGraphicImage* image = skillData->GradeData[grade].pImage;
if (!image) image = skillData->pImage;
return quick_slot_image_uv(image, texture, uv);
}
if (type == SLOT_TYPE_INVENTORY) {
// uitaskbar.RefreshQuickSlot: the item's icon and, above one, its count.
const TItemPos cell(INVENTORY, static_cast<WORD>(position));
CItemData* itemData = nullptr;
if (!CItemManager::Instance().GetItemDataPointer(player.GetItemIndex(cell), &itemData) || !itemData)
return false;
const DWORD itemCount = player.GetItemCount(cell);
*count = itemCount > 1 ? static_cast<int>(itemCount) : 0;
return quick_slot_image_uv(itemData->GetIconImage(), texture, uv);
}
return false;
}
void UIMouseDoubleClick(int x, int y)
{
MtHostSetCursor(x, y);
if (!g_window_manager) return;
if (g_game_singletons)
CPythonApplication::Instance().OnMouseMove(x, y);
else
g_window_manager->RunMouseMove(x, y);
update_3d_pick_ray();
g_window_manager->RunMouseLeftButtonDoubleClick(x, y);
}
bool UIIsAttaching()
{
return g_window_manager && g_window_manager->IsAttaching();
}
void UIDeattachObject()
{
std::string error;
RunLine("__import__('mouseModule').mouseController.DeattachObject()", &error);
}
void CameraRotateBy(float rollDeg, float pitchDeg)
{
CCamera* pkCmrCur = CCameraManager::Instance().GetCurrentCamera();
if (!pkCmrCur || pkCmrCur->IsLock())
return;
// Same collision limits CCamera::Drag applies to its velocities.
switch (pkCmrCur->GetCameraState())
{
case CAMERA_STATE_CANTGOLEFT: rollDeg = std::max(0.0f, rollDeg); break;
case CAMERA_STATE_CANTGORIGHT: rollDeg = std::min(0.0f, rollDeg); break;
case CAMERA_STATE_CANTGODOWN: pitchDeg = std::max(0.0f, pitchDeg); break;
default: break;
}
pkCmrCur->RotateEyeAroundTarget(pitchDeg, rollDeg);
}
void SetTouchInput(bool touch)
{
g_touch_input = touch;
UI::CWindow::SetHideVirtualKeyboard(touch);
}
bool TouchInput()
{
return g_touch_input;
}
bool TextInputFocused()
{
return g_window_manager && CIME::ms_bCaptureInput;
}
bool IsPointInsideFocusedWindow(int x, int y)
{
if (!g_window_manager) return false;
UI::CWindow* pWin = g_window_manager->GetActivateWindow();
if (!pWin || !pWin->IsRendering())
return false;
// The focus can outlive the EditLine's use: UnlockWindow after an NPC dialog and DeactivateWindow
// hand it back to a chat line whose board is hidden (capture on again), and the full-screen
// GameWindow can hold it. Only a tap that picks the focused window itself (or its child) is one on it.
for (UI::CWindow* pPicked = g_window_manager->PickWindow(x - g_window_manager->GetScreenOffsetX(), y);
pPicked; pPicked = pPicked->GetParent())
if (pPicked == pWin)
return true;
return false;
}
bool IsPointInsideActiveUI(int x, int y)
{
if (!g_window_manager) return false;
return g_window_manager->IsPointInsideActiveUI(x - g_window_manager->GetScreenOffsetX(), y);
}
PlayerStatusInfo GetPlayerStatusInfo()
{
PlayerStatusInfo info{};
if (!g_game_singletons) return info;
info.level = CPythonPlayer::Instance().GetStatus(POINT_LEVEL);
info.hp = CPythonPlayer::Instance().GetStatus(POINT_HP);
info.max_hp = CPythonPlayer::Instance().GetStatus(POINT_MAX_HP);
info.sp = CPythonPlayer::Instance().GetStatus(POINT_SP);
info.max_sp = CPythonPlayer::Instance().GetStatus(POINT_MAX_SP);
info.exp = CPythonPlayer::Instance().GetStatus(POINT_EXP);
info.max_exp = CPythonPlayer::Instance().GetStatus(POINT_NEXT_EXP);
const char* szName = CPythonPlayer::Instance().GetName();
if (szName) info.name = szName;
return info;
}
// 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 IsSoftwareCursorVisible()
{
if (!g_game_singletons)
return false;
return CPythonApplication::Instance().GetCursorMode() == CPythonApplication::CURSOR_MODE_SOFTWARE &&
CPythonApplication::Instance().GetCursorVisible();
}
bool Start(const char* stdlib_path, std::string* error)
{
if (g_launcher)
return true;
#if defined(__APPLE__)
if (std::getenv("MT_BETA_DIAGNOSTIC"))
signal(SIGSEGV, [](int) {
void* frames[64];
int count = backtrace(frames, 64);
backtrace_symbols_fd(frames, count, STDERR_FILENO);
std::_Exit(139);
});
#endif
// 40250 WinMain (UserInterface.cpp:524): ApplicationStringTable_Initialize(hInstance) before locale.cfg.
ApplicationStringTable_Initialize(NULL);
// 40250 Main (UserInterface.cpp:410): if (LocaleService_LoadGlobal(hInstance)) SetDefaultCodePage(...).
std::string locale_config = "locale.cfg";
if (const char* client = std::getenv("MT_40250_CLIENT"))
locale_config = (std::filesystem::path(client) / "locale.cfg").string();
LocaleService_LoadConfig(locale_config.c_str());
SetDefaultCodePage(LocaleService_GetCodePage());
if (const char* data_dir = std::getenv("MT_40250_USER_DATA_DIR"))
{
std::error_code ec;
std::filesystem::current_path(data_dir, ec);
if (ec)
return fail(error, "cannot use player settings directory: " + ec.message());
}
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
g_exception_sender = std::make_unique<CPythonExceptionSender>();
SetExceptionSender(g_exception_sender.get());
// 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;
}
// 40250 CPythonApplication::Create (PythonApplication.cpp:1186): no warning box when it fails.
if (!g_game_singletons->pySystem.IsNoSoundCard())
g_sound_manager->Create();
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
{
// PORT: the auto hunt window (AutoHuntScript.inc) as module mt_autohunt and the account registration
// window (RegisterScript.inc) as mt_register. They hook game.GameWindow / introLogin.LoginWindow, so they
// have to run once the root scripts are importable; a failure leaves the game without them, not broken.
#include "AutoHuntScript.inc"
#include "RegisterScript.inc"
#include "FrameRateOptionScript.inc"
#include "UIScaleOptionScript.inc"
#include "InventoryPagesScript.inc"
#include "MallHintScript.inc"
static void install_script_module(const char* c_szModule, const char* c_szScript)
{
PyObject* module = PyImport_AddModule(c_szModule);
PyObject* dict = module ? PyModule_GetDict(module) : nullptr;
if (!dict)
{
PyErr_Clear();
return;
}
if (!PyDict_GetItemString(dict, "__builtins__"))
PyDict_SetItemString(dict, "__builtins__", PyEval_GetBuiltins());
PyObject* result = PyRun_String(c_szScript, Py_file_input, dict, dict);
if (!result)
{
TraceError("%s: install failed", c_szModule);
PyErr_Print();
}
Py_XDECREF(result);
}
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);
}
if (IsAppLooping())
{
install_script_module("mt_autohunt", c_szAutoHuntScript);
install_script_module("mt_register", c_szRegisterScript);
install_script_module("mt_framerate", c_szFrameRateOptionScript);
install_script_module("mt_uiscale", c_szUIScaleOptionScript);
install_script_module("mt_inventory", c_szInventoryPagesScript);
install_script_module("mt_mallhint", c_szMallHintScript);
}
return true;
}
int ScriptThreadId()
{
return g_script_thread_id;
}
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__.
if (Py_IsInitialized())
{
PyGILState_STATE gil = PyGILState_Ensure();
PyRun_SimpleString(
"import sys\n"
"_mm = sys.modules.get('mouseModule')\n"
"if _mm and hasattr(_mm, 'mouseController'):\n"
" getattr(_mm.mouseController, 'cursorDict', {}).clear()\n");
PyGILState_Release(gil);
}
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->Destroy(); // 40250 CPythonApplication::Destroy (PythonApplication.cpp:1442)
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();
// PORT: 40250 leaves g_pkExceptionSender dangling once Main() returns; Boot can run again here.
SetExceptionSender(NULL);
g_exception_sender.reset();
g_launcher.reset();
}
}
// winuser.h (common/shim/win32/winuser.h). WM_QUIT ends 40250's CPythonApplication::Loop; here the script
// fiber's loop ends the same way. The WM_QUIT is also queued for a GetMessage loop (CMSApplication).
void PostQuitMessage(int nExitCode)
{
PostMessage(NULL, WM_QUIT, WPARAM(nExitCode), 0);
PythonBoot::AppRequestQuit();
}