1097 lines
35 KiB
C++
1097 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 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();
|
|
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 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();
|
|
}
|