2V0-f: run the real system.py app loop in Godot

- system.py runs app.Loop() on the script fiber; each UIUpdate is one
  CPythonApplication::Process(). Logo -> Login, popup ESC/click and
  app.Exit teardown (launcher cleared first, as in 40250 Main) work.
- Image commands are base-texture quads with UVs (a .sub shares its parent
  texture); ImageInstance/ExpandedImageInstance OnRender and SetRenderingRect
  ported verbatim, so scale/rotation/rendering rect are drawn.
- CGraphicBase::GetColor / grp.GenerateColor ported (ThinBoard background).
- port.app_loop test, python_host_test app-loop checks, MT_TEST_MODE=python_ui.
- TODO: .fnt/CGraphicText font pipeline, ExpandedImage blend modes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-23 13:38:13 +09:00
co-authored by Claude Opus 5.5
parent 5590714b16
commit 141206cb69
20 changed files with 916 additions and 57 deletions
@@ -3,9 +3,18 @@
#include "UserInterface/MarkManager.h"
#include "GameLib/GameUtil.h"
#include "EterLib/Camera.h"
#include "EterBase/Timer.h"
#include "EterLib/ResourceManager.h"
#include "../PlatformStub.h"
#include "../EterLib/UIRenderCommands.h"
#include "../ScriptLib/PythonBoot.h"
// 2V0-d link surface. The application event loop and UI rendering are implemented in 2V0-f.
// 2V0-d link surface; 2V0-f adds the application lifecycle (Create/Loop/Process/Exit) that
// prototype.RunApp() drives. The rest of the members still wait for the game slices (2V1-2V3).
//
// PORT: there is no CPythonApplication object yet (its members are the whole game: background,
// network stream, player, ...), so these functions only use the singletons PythonBoot owns
// (UI::CWindowManager, CTimer, CResourceManager) and never touch a data member.
CPythonApplication* CPythonApplication::ms_pInstance = nullptr;
void CPythonApplication::ShowWebPage(const char*, const RECT&) { MT_PLATFORM_STUB(); }
@@ -13,21 +22,84 @@ void CPythonApplication::MoveWebPage(const RECT&) { MT_PLATFORM_STUB(); }
void CPythonApplication::HideWebPage() { MT_PLATFORM_STUB(); }
bool CPythonApplication::IsWebPageMode() { MT_PLATFORM_STUB(); return false; }
void CPythonApplication::GetInfo(UINT, std::string* out) { MT_PLATFORM_STUB(); if (out) out->clear(); }
void CPythonApplication::Loop() { MT_PLATFORM_STUB(); }
void CPythonApplication::Exit() { MT_PLATFORM_STUB(); }
void CPythonApplication::Abort() { MT_PLATFORM_STUB(); }
// In 40250, Loop() is: while (1) { if (IsMessage()) { if (!MessageProcess()) break; } else { if (!Process()) break; } }
// PORT: Godot owns the message pump and forwards input to the window manager between frames
// (PythonBoot::UIMouseMove & co.), so the loop here only waits for the host's next frame. It runs
// on PythonBoot's script thread; WM_QUIT is PythonBoot::AppRequestQuit.
void CPythonApplication::Loop()
{
while (PythonBoot::AppLoopWait())
{
if (!Process())
break;
}
}
// In 40250 this is PostQuitMessage(0).
void CPythonApplication::Exit() { PythonBoot::AppRequestQuit(); }
// 40250 also writes a TraceError banner first; the caller (system.py) has already logged the reason.
void CPythonApplication::Abort()
{
TraceError("============================================================================================================");
TraceError("Abort!!!!\n\n");
PythonBoot::AppRequestQuit();
}
void CPythonApplication::SetMinFog(float) { MT_PLATFORM_STUB(); }
void CPythonApplication::SetFrameSkip(bool) { MT_PLATFORM_STUB(); }
bool CPythonApplication::Create(PyObject*, const char*, int, int, int) {
MT_PLATFORM_STUB();
// appCreate returns NULL on failure. Give Python the exception that
// prototype.RunApp expects rather than a SystemError with no exception set.
PyErr_SetString(PyExc_RuntimeError, "CREATE_DEVICE");
return false;
// PORT: 40250 creates the Win32 window, the D3D8 device, cursors, sound, keyboard, the net device,
// fonts and the IME here. The Godot window and its canvas are the device on this platform and they
// already exist when the host starts the script, so the window size is the canvas size
// (PythonBoot::SetUISize) rather than the width/height passed from metin2.cfg. What remains of
// Create belongs to the subsystems that are not ported yet (sound 2V4, net 2V1, game render 2V3).
bool CPythonApplication::Create(PyObject*, const char*, int, int, int)
{
unsigned width = 0, height = 0;
UIRenderGetSize(&width, &height);
if (!width || !height)
{
// appCreate returns NULL on failure; RunApp turns this into the original LogBox.
TraceError("CreateDevice: GraphicDevice create failed");
PyErr_SetString(PyExc_RuntimeError, "CREATE_DEVICE");
return false;
}
return true;
}
void CPythonApplication::UpdateGame() { MT_PLATFORM_STUB(); }
void CPythonApplication::RenderGame() { MT_PLATFORM_STUB(); }
bool CPythonApplication::Process() { MT_PLATFORM_STUB(); return false; }
// 40250 PythonApplication.cpp:399. Kept in order: timer, network (2V1), input polling (Godot pushes
// input instead), camera (2V3), resource GC, mouse/UI update, then the render block's interface pass.
// PORT: the frame-skip bookkeeping and the game render pass are left out; the Godot frame clock
// decides when this runs, and RenderGame belongs to 2V3.
bool CPythonApplication::Process()
{
ELTimer_SetFrameMSec();
CTimer& rkTimer=CTimer::Instance();
rkTimer.Advance();
CResourceManager::Instance().Update();
OnUIUpdate();
UIRenderBeginFrame();
OnUIRender();
return true;
}
// 40250 PythonApplicationEvent.cpp:15-25
void CPythonApplication::OnUIUpdate()
{
UI::CWindowManager& rkUIMgr=UI::CWindowManager::Instance();
rkUIMgr.Update();
}
void CPythonApplication::OnUIRender()
{
UI::CWindowManager& rkUIMgr=UI::CWindowManager::Instance();
rkUIMgr.Render();
}
BOOL CPythonApplication::SetCursorNum(int) { MT_PLATFORM_STUB(); return FALSE; }
void CPythonApplication::SetCursorVisible(BOOL, bool) { MT_PLATFORM_STUB(); }
BOOL CPythonApplication::GetCursorVisible() { MT_PLATFORM_STUB(); return FALSE; }