// 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 "PythonHost.h" #include "../EterLib/UIRenderCommands.h" #include "../EterBase/LogBox.h" #include "../UserInterface/ServerClock.h" #include #include #include #include #include #include #include #if defined(__APPLE__) #include #include #include #endif #if defined(__linux__) || defined(__ANDROID__) #include #endif namespace { // 40250 has this on Main()'s stack. CPythonLauncher is a CSingleton, so there is still only ever one. std::unique_ptr g_launcher; // 40250 Main(): CPythonExceptionSender pyExceptionSender; SetExceptionSender(&pyExceptionSender); std::unique_ptr g_exception_sender; std::unique_ptr g_window_manager; std::unique_ptr g_resource; // 40250 CPythonApplication::m_timer; CSlotWindow's cool time and Process() read it via Instance(). std::unique_ptr 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 g_network_stream; std::unique_ptr g_account_connector; // 40250 CPythonApplication::m_pyPlayer, m_FlyingManager and m_kEftMgr: the loading phase clears them // (CPythonNetworkStream::SetLoadingPhase). std::unique_ptr g_player; std::unique_ptr g_sound_manager; std::unique_ptr g_flying_manager; // 40250 CPythonApplication::m_kServerStateChecker: intrologin.py's channel status list. std::unique_ptr g_server_state_checker; std::unique_ptr g_race_manager; std::unique_ptr g_game_event_manager; std::unique_ptr 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 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 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 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 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 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 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 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(lx), static_cast(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(lx), static_cast(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(lx), static_cast(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(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 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(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(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(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(); return pWin && pWin->IsShow() && pWin->IsIn(x - g_window_manager->GetScreenOffsetX(), y); } 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 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(); // Py_Initialize g_exception_sender = std::make_unique(); 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(); g_resource = std::make_unique(); g_window_manager = std::make_unique(); g_network_stream = std::make_unique(); g_account_connector = std::make_unique(); g_player = std::make_unique(); g_sound_manager = std::make_unique(); g_flying_manager = std::make_unique(); g_server_state_checker = std::make_unique(); g_race_manager = std::make_unique(); g_game_event_manager = std::make_unique(); g_effect_manager = std::make_unique(); g_game_application = std::make_unique(); g_game_singletons = std::make_unique(); 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 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(); }