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
+14 -3
View File
@@ -4,6 +4,7 @@
#include "EterLib/GrpBase.h"
#include "../PlatformStub.h"
#include <cstring>
#include "UIRenderCommands.h"
auto CGraphicBase::GetAvailableTextureMemory() -> DWORD
@@ -171,10 +172,20 @@ auto CGraphicBase::SetScreenEffectFlashing(float, const D3DXCOLOR &) -> void
MT_PLATFORM_STUB();
}
auto CGraphicBase::GetColor(float, float, float, float) -> DWORD
// 40250 GrpBase.cpp:458: the bytes B, G, R, A of a little-endian D3DCOLOR.
auto CGraphicBase::GetColor(float r, float g, float b, float a) -> DWORD
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<DWORD>();
BYTE argb[4] =
{
(BYTE) (255.0f * b),
(BYTE) (255.0f * g),
(BYTE) (255.0f * r),
(BYTE) (255.0f * a)
};
DWORD color;
std::memcpy(&color, argb, sizeof(color));
return color;
}
auto CGraphicBase::GetFaceCount() -> DWORD
@@ -5,6 +5,9 @@
#include "../PlatformStub.h"
#include "EterBase/CRC32.h"
#include "UIRenderCommands.h"
#include <cmath>
auto CGraphicExpandedImageInstance::Type() -> DWORD
{
@@ -19,7 +22,7 @@ CGraphicExpandedImageInstance::CGraphicExpandedImageInstance()
CGraphicExpandedImageInstance::~CGraphicExpandedImageInstance()
{
MT_PLATFORM_STUB();
Destroy();
}
auto CGraphicExpandedImageInstance::Destroy() -> void
@@ -53,10 +56,19 @@ auto CGraphicExpandedImageInstance::SetScale(float x, float y) -> void
m_v2Scale = D3DXVECTOR2(x, y);
}
auto CGraphicExpandedImageInstance::SetRenderingRect(float left, float top, float right, float bottom) -> void
// In 40250, the arguments are fractions of the image size (uiToolTip gauges, SetPercentage).
auto CGraphicExpandedImageInstance::SetRenderingRect(float fLeft, float fTop, float fRight, float fBottom) -> void
{
m_RenderingRect = {static_cast<LONG>(left), static_cast<LONG>(top),
static_cast<LONG>(right), static_cast<LONG>(bottom)};
if (IsEmpty())
return;
float fWidth = float(GetWidth());
float fHeight = float(GetHeight());
m_RenderingRect.left = fWidth * fLeft;
m_RenderingRect.top = fHeight * fTop;
m_RenderingRect.right = fWidth * fRight;
m_RenderingRect.bottom = fHeight * fBottom;
}
auto CGraphicExpandedImageInstance::SetRenderingMode(int mode) -> void
@@ -74,9 +86,68 @@ auto CGraphicExpandedImageInstance::Initialize() -> void
m_iRenderingMode = RENDERING_MODE_NORMAL;
}
// 40250 GrpExpandedImageInstance.cpp:29-142. The vertex and texture-coordinate arithmetic is
// verbatim; the D3D stream/blend-state calls become one UIRenderCommand with the four vertices.
auto CGraphicExpandedImageInstance::OnRender() -> void
{
CGraphicImageInstance::OnRender();
CGraphicImage * pImage = m_roImage.GetPointer();
CGraphicTexture * pTexture = pImage->GetTexturePointer();
if (pTexture->GetWidth() <= 0 || pTexture->GetHeight() <= 0)
return;
const RECT& c_rRect = pImage->GetRectReference();
float texReverseWidth = 1.0f / float(pTexture->GetWidth());
float texReverseHeight = 1.0f / float(pTexture->GetHeight());
float su = (c_rRect.left - m_RenderingRect.left) * texReverseWidth;
float sv = (c_rRect.top - m_RenderingRect.top) * texReverseHeight;
float eu = (c_rRect.left + m_RenderingRect.right + (c_rRect.right-c_rRect.left)) * texReverseWidth;
float ev = (c_rRect.top + m_RenderingRect.bottom + (c_rRect.bottom-c_rRect.top)) * texReverseHeight;
float x[4], y[4];
for (int i = 0; i < 4; ++i)
{
x[i] = m_v2Position.x-0.5f;
y[i] = m_v2Position.y-0.5f;
}
if (0.0f == m_fRotation)
{
float fimgWidth = float(pImage->GetWidth()) * m_v2Scale.x;
float fimgHeight = float(pImage->GetHeight()) * m_v2Scale.y;
x[0] -= m_RenderingRect.left;
y[0] -= m_RenderingRect.top;
x[1] += fimgWidth + m_RenderingRect.right;
y[1] -= m_RenderingRect.top;
x[2] -= m_RenderingRect.left;
y[2] += fimgHeight + m_RenderingRect.bottom;
x[3] += fimgWidth + m_RenderingRect.right;
y[3] += fimgHeight + m_RenderingRect.bottom;
// PORT: 40250 flips D3DRS_CULLMODE for a mirrored scale; the Godot canvas has no culling.
}
else
{
float fimgHalfWidth = float(pImage->GetWidth())/2.0f * m_v2Scale.x;
float fimgHalfHeight = float(pImage->GetHeight())/2.0f * m_v2Scale.y;
for (int i = 0; i < 4; ++i)
{
x[i] += m_v2Origin.x;
y[i] += m_v2Origin.y;
}
float fRadian = D3DXToRadian(m_fRotation);
x[0] += (-fimgHalfWidth*cosf(fRadian)) - (-fimgHalfHeight*sinf(fRadian));
y[0] += (-fimgHalfWidth*sinf(fRadian)) + (-fimgHalfHeight*cosf(fRadian));
x[1] += (+fimgHalfWidth*cosf(fRadian)) - (-fimgHalfHeight*sinf(fRadian));
y[1] += (+fimgHalfWidth*sinf(fRadian)) + (-fimgHalfHeight*cosf(fRadian));
x[2] += (-fimgHalfWidth*cosf(fRadian)) - (+fimgHalfHeight*sinf(fRadian));
y[2] += (-fimgHalfWidth*sinf(fRadian)) + (+fimgHalfHeight*cosf(fRadian));
x[3] += (+fimgHalfWidth*cosf(fRadian)) - (+fimgHalfHeight*sinf(fRadian));
y[3] += (+fimgHalfWidth*sinf(fRadian)) + (+fimgHalfHeight*cosf(fRadian));
}
UIRenderAddImage(pTexture, x, y, su, sv, eu, ev, UIRenderColor(m_DiffuseColor), m_iRenderingMode);
}
auto CGraphicExpandedImageInstance::OnSetImagePointer() -> void
@@ -6,7 +6,6 @@
#include "../PlatformStub.h"
#include "UIRenderCommands.h"
#include "EterBase/CRC32.h"
#include <algorithm>
auto CGraphicImageInstance::Type() -> DWORD
{
@@ -27,12 +26,15 @@ CGraphicImageInstance::CGraphicImageInstance()
CGraphicImageInstance::~CGraphicImageInstance()
{
MT_PLATFORM_STUB();
Destroy();
}
// 40250 Destroy(): drop the image reference, then Initialize() (white, at the origin).
auto CGraphicImageInstance::Destroy() -> void
{
m_roImage.Clear();
m_DiffuseColor = D3DXCOLOR(1, 1, 1, 1);
m_v2Position = D3DXVECTOR2(0, 0);
}
auto CGraphicImageInstance::Render() -> void
@@ -78,14 +80,13 @@ auto CGraphicImageInstance::GetHeight() -> int
auto CGraphicImageInstance::GetTexturePointer() -> CGraphicTexture *
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<CGraphicTexture *>();
CGraphicImage* pkImage = m_roImage.GetPointer();
return pkImage ? pkImage->GetTexturePointer() : NULL;
}
auto CGraphicImageInstance::GetTextureReference() const -> const CGraphicTexture &
{
MT_PLATFORM_STUB();
return mt_platform_stub_return<const CGraphicTexture &>();
return m_roImage->GetTextureReference();
}
auto CGraphicImageInstance::GetGraphicImagePointer() -> CGraphicImage *
@@ -104,20 +105,31 @@ auto CGraphicImageInstance::Initialize() -> void
MT_PLATFORM_STUB();
}
// 40250 GrpImageInstance.cpp:44-96: the image's rect inside its texture (a .sub shares its parent's)
// becomes the texture coordinates of one quad.
auto CGraphicImageInstance::OnRender() -> void
{
CGraphicImage* image = m_roImage.GetPointer();
if (!image) return;
auto channel = [](float value) -> std::uint32_t {
return static_cast<std::uint32_t>(std::clamp(value, 0.0f, 1.0f) * 255.0f + 0.5f);
};
const std::uint32_t color = (channel(m_DiffuseColor.a) << 24) |
(channel(m_DiffuseColor.r) << 16) | (channel(m_DiffuseColor.g) << 8) |
channel(m_DiffuseColor.b);
UIRenderCommand command{UIRenderCommand::Image, m_v2Position.x, m_v2Position.y,
m_v2Position.x + image->GetWidth(), m_v2Position.y + image->GetHeight(), color};
command.text = image->GetFileName();
UIRenderAdd(std::move(command));
CGraphicImage * pImage = m_roImage.GetPointer();
CGraphicTexture * pTexture = pImage->GetTexturePointer();
if (pTexture->GetWidth() <= 0 || pTexture->GetHeight() <= 0)
return;
float fimgWidth = pImage->GetWidth();
float fimgHeight = pImage->GetHeight();
const RECT& c_rRect = pImage->GetRectReference();
float texReverseWidth = 1.0f / float(pTexture->GetWidth());
float texReverseHeight = 1.0f / float(pTexture->GetHeight());
float su = c_rRect.left * texReverseWidth;
float sv = c_rRect.top * texReverseHeight;
float eu = (c_rRect.left + (c_rRect.right-c_rRect.left)) * texReverseWidth;
float ev = (c_rRect.top + (c_rRect.bottom-c_rRect.top)) * texReverseHeight;
const float x[4] = {m_v2Position.x-0.5f, m_v2Position.x + fimgWidth-0.5f,
m_v2Position.x-0.5f, m_v2Position.x + fimgWidth-0.5f};
const float y[4] = {m_v2Position.y-0.5f, m_v2Position.y-0.5f,
m_v2Position.y + fimgHeight-0.5f, m_v2Position.y + fimgHeight-0.5f};
UIRenderAddImage(pTexture, x, y, su, sv, eu, ev, UIRenderColor(m_DiffuseColor));
}
auto CGraphicImageInstance::OnSetImagePointer() -> void
@@ -9,7 +9,33 @@
namespace {
unsigned little16(const unsigned char* data) { return data[0] | (unsigned(data[1]) << 8); }
unsigned little32(const unsigned char* data) { return little16(data) | (little16(data + 2) << 16); }
unsigned big16(const unsigned char* data) { return (unsigned(data[0]) << 8) | data[1]; }
unsigned big32(const unsigned char* data) { return (unsigned(data[0]) << 24) | (unsigned(data[1]) << 16) | (unsigned(data[2]) << 8) | data[3]; }
// Walks the JPEG markers to the first SOFn (the frame header holds the size), as D3DX does when it
// sniffs a .jpg; login/loading backgrounds (serverlist.jpg, loading/*.jpg) are JPEGs.
bool jpeg_size(const unsigned char* data, unsigned size, unsigned* width, unsigned* height)
{
if (size < 4 || data[0] != 0xFF || data[1] != 0xD8) return false;
unsigned pos = 2;
while (pos + 4 <= size) {
if (data[pos] != 0xFF) return false;
const unsigned char marker = data[pos + 1];
if (marker == 0xFF) { ++pos; continue; }
if (marker == 0xD8 || marker == 0x01 || (marker >= 0xD0 && marker <= 0xD7)) { pos += 2; continue; }
const unsigned length = big16(data + pos + 2);
if (length < 2) return false;
const bool sof = marker >= 0xC0 && marker <= 0xCF && marker != 0xC4 && marker != 0xC8 && marker != 0xCC;
if (sof) {
if (pos + 9 > size) return false;
*height = big16(data + pos + 5);
*width = big16(data + pos + 7);
return true;
}
pos += 2 + length;
}
return false;
}
}
CGraphicImageTexture::CGraphicImageTexture()
@@ -45,6 +71,9 @@ auto CGraphicImageTexture::CreateFromTexturePointer(const CGraphicTexture *sourc
m_width = source->GetWidth();
m_height = source->GetHeight();
m_bEmpty = source->IsEmpty();
// 40250 shares the source's D3D texture; the canvas names textures by pack path instead.
if (const auto* image = dynamic_cast<const CGraphicImageTexture*>(source))
m_stFileName = image->m_stFileName;
}
auto CGraphicImageTexture::CreateFromDiskFile(const char *, D3DFORMAT, DWORD) -> bool
@@ -62,6 +91,11 @@ auto CGraphicImageTexture::CreateFromMemoryFile(UINT size, const void *bytes, D3
height = little32(data + 12); width = little32(data + 16);
} else if (size >= 24 && std::memcmp(data, "\x89PNG\r\n\x1a\n", 8) == 0) {
width = big32(data + 16); height = big32(data + 20);
} else if (jpeg_size(data, size, &width, &height)) {
} else if (size >= 26 && data[0] == 'B' && data[1] == 'M') {
width = little32(data + 18);
const int bmp_height = static_cast<int>(little32(data + 22)); // negative: top-down rows
height = static_cast<unsigned>(bmp_height < 0 ? -bmp_height : bmp_height);
} else if (size >= 18 && (data[2] == 2 || data[2] == 10 || data[2] == 3)) {
width = little16(data + 12); height = little16(data + 14);
}
@@ -82,6 +116,17 @@ auto CGraphicImageTexture::SetFileName(const char *name) -> void
m_stFileName = name ? name : "";
}
// PORT: where 40250 binds pTexture->GetD3DTexture(), the canvas gets the pack path of the file the
// texture was decoded from (m_stFileName is protected, hence the derived accessor).
std::string UIRenderTextureName(const CGraphicTexture* texture)
{
struct Access : CGraphicImageTexture {
static const std::string& name(const CGraphicImageTexture* t) { return t->*(&Access::m_stFileName); }
};
const auto* image = dynamic_cast<const CGraphicImageTexture*>(texture);
return image ? Access::name(image) : std::string();
}
auto CGraphicImageTexture::Lock(int *, void **, int) -> bool
{
MT_PLATFORM_STUB();
@@ -1,5 +1,7 @@
#include "UIRenderCommands.h"
#include <algorithm>
namespace {
std::vector<UIRenderCommand> commands;
int canvas_width = 0;
@@ -34,3 +36,21 @@ void UIRenderRestoreClip() {
clip_x1 = saved_x1; clip_y1 = saved_y1;
clip_x2 = saved_x2; clip_y2 = saved_y2;
}
void UIRenderAddImage(const CGraphicTexture* texture, const float x[4], const float y[4],
float su, float sv, float eu, float ev, std::uint32_t argb, int blend) {
UIRenderCommand command{UIRenderCommand::Image, 0, 0, 0, 0, argb};
for (int i = 0; i < 4; ++i) {
command.qx[i] = x[i] + 0.5f;
command.qy[i] = y[i] + 0.5f;
}
command.x1 = std::min({command.qx[0], command.qx[1], command.qx[2], command.qx[3]});
command.y1 = std::min({command.qy[0], command.qy[1], command.qy[2], command.qy[3]});
command.x2 = std::max({command.qx[0], command.qx[1], command.qx[2], command.qx[3]});
command.y2 = std::max({command.qy[0], command.qy[1], command.qy[2], command.qy[3]});
command.text = UIRenderTextureName(texture);
command.quad = true;
command.su = su; command.sv = sv; command.eu = eu; command.ev = ev;
command.blend = blend;
UIRenderAdd(std::move(command));
}
@@ -12,8 +12,32 @@ struct UIRenderCommand {
std::uint32_t argb;
float clip_x1 = 0, clip_y1 = 0, clip_x2 = 0, clip_y2 = 0;
std::string text;
// Image only: text is the texture's file, drawn as the quad (vertices 0..3 = TL, TR, BL, BR)
// with texture coordinates su,sv..eu,ev; x1..y2 is the quad's bounding box.
bool quad = false;
float qx[4] = {}, qy[4] = {};
float su = 0, sv = 0, eu = 1, ev = 1;
int blend = 0; // CGraphicExpandedImageInstance::RENDERING_MODE_*
};
// D3DXCOLOR (0..1 floats) -> the 0xAARRGGBB the commands carry.
template <class Color>
std::uint32_t UIRenderColor(const Color& color) {
auto channel = [](float value) -> std::uint32_t {
return static_cast<std::uint32_t>((value < 0 ? 0 : value > 1 ? 1 : value) * 255.0f + 0.5f);
};
return (channel(color.a) << 24) | (channel(color.r) << 16) | (channel(color.g) << 8) | channel(color.b);
}
class CGraphicTexture;
std::string UIRenderTextureName(const CGraphicTexture* texture);
// A textured quad from 40250's TPDTVertex[4] (TL, TR, BL, BR) positions and texture coordinates.
// PORT: D3D8 puts pixel centres on integers, which is why 40250 subtracts 0.5 from every vertex; the
// Godot canvas puts them on .5, so the quad is shifted back by +0.5 here.
void UIRenderAddImage(const CGraphicTexture* texture, const float x[4], const float y[4],
float su, float sv, float eu, float ev, std::uint32_t argb, int blend = 0);
void UIRenderSetSize(int width, int height);
void UIRenderGetSize(unsigned* width, unsigned* height);
void UIRenderBeginFrame();
@@ -15,7 +15,7 @@
void CPythonGraphic::SetViewport(float x, float y, float width, float height) { UIRenderSetClip(x, y, width, height); }
void CPythonGraphic::RestoreViewport() { UIRenderRestoreClip(); }
void CPythonGraphic::SetOmniLight() { MT_PLATFORM_STUB(); }
long CPythonGraphic::GenerateColor(float, float, float, float) { MT_PLATFORM_STUB(); return 0; }
long CPythonGraphic::GenerateColor(float r, float g, float b, float a) { return GetColor(r, g, b, a); }
void CPythonGraphic::RenderUpButton(float, float, float, float) { MT_PLATFORM_STUB(); }
void CPythonGraphic::RenderDownButton(float, float, float, float) { MT_PLATFORM_STUB(); }
bool CPythonGraphic::SaveScreenShot(const char*) { MT_PLATFORM_STUB(); return false; }
@@ -2,6 +2,7 @@
// These modules expose names for script loading; calls record that they still need the real 2V1-2V3 implementation.
#include "ScriptLib/StdAfx.h"
#include <cstdio>
#include "../EterLib/UIRenderCommands.h"
namespace {
PyObject* stub_call_named(const char* name) {
@@ -1629,8 +1630,19 @@ PyMethodDef methods_snd[] = {
const char* ints_snd[] = {NULL};
const char* strings_snd[] = {NULL};
// 40250 UserInterface/PythonSystemModule.cpp: systemSetting export names.
PyObject* stub_systemSetting_GetWidth(PyObject*, PyObject*) { return stub_call_named("systemSetting.GetWidth"); }
PyObject* stub_systemSetting_GetHeight(PyObject*, PyObject*) { return stub_call_named("systemSetting.GetHeight"); }
// 40250 CPythonSystem::GetWidth/GetHeight return metin2.cfg's WIDTH/HEIGHT (default 800x600), which
// RunApp passes to wndMgr.SetScreenSize and app.Create. PORT (2V0-f): the Godot canvas is the window,
// so report its size; the rest of systemSetting stays a stub until CPythonSystem is ported.
PyObject* stub_systemSetting_GetWidth(PyObject*, PyObject*) {
unsigned width = 0;
UIRenderGetSize(&width, nullptr);
return Py_BuildValue("i", width ? int(width) : 800);
}
PyObject* stub_systemSetting_GetHeight(PyObject*, PyObject*) {
unsigned height = 0;
UIRenderGetSize(nullptr, &height);
return Py_BuildValue("i", height ? int(height) : 600);
}
PyObject* stub_systemSetting_SetInterfaceHandler(PyObject*, PyObject*) { return stub_call_named("systemSetting.SetInterfaceHandler"); }
PyObject* stub_systemSetting_DestroyInterfaceHandler(PyObject*, PyObject*) { return stub_call_named("systemSetting.DestroyInterfaceHandler"); }
PyObject* stub_systemSetting_ReserveResource(PyObject*, PyObject*) { return stub_call_named("systemSetting.ReserveResource"); }
+189 -8
View File
@@ -5,12 +5,16 @@
#include "ScriptLib/Resource.h"
#include "UserInterface/StdAfx.h" // initpack and the rest of the module initializer list
#include "EterPythonLib/StdAfx.h"
#include "EterBase/Timer.h"
#include "PythonBoot.h"
#include "PythonHost.h"
#include "../EterLib/UIRenderCommands.h"
#include "../EterBase/LogBox.h"
#include <condition_variable>
#include <memory>
#include <mutex>
#include <pthread.h>
namespace
{
@@ -18,6 +22,8 @@ namespace
std::unique_ptr<CPythonLauncher> g_launcher;
std::unique_ptr<UI::CWindowManager> g_window_manager;
std::unique_ptr<CPythonResource> g_resource;
// 40250 CPythonApplication::m_timer; CSlotWindow's cool time and Process() read it via Instance().
std::unique_ptr<CTimer> g_timer;
bool fail(std::string* error, const std::string& text)
{
@@ -25,6 +31,78 @@ bool fail(std::string* error, const std::string& text)
*error = text;
return false;
}
// PORT: 40250 RunApp() blocks inside app.Loop() (CPythonApplication::Loop, PythonApplication.cpp:980)
// and only returns to run mainStream.Destroy() when the window closes. Godot owns the frame loop here,
// so system.py runs on its own thread and the two threads pass a baton: exactly one of them runs at
// any time. Loop() hands the baton back after every Process(), and the host hands it over once per
// Godot frame (AppFrame). The script keeps its original control flow — including the code after
// app.Loop() — without ever blocking the Godot main thread for more than one Process().
struct ScriptFiber
{
enum Turn { Host, Script };
std::mutex mutex;
std::condition_variable cv;
Turn turn = Host;
pthread_t thread{};
bool alive = false; // the thread exists and has not been joined
bool finished = false; // RunMainScript's body returned
bool looping = false; // parked inside CPythonApplication::Loop
bool quit = false; // app.Exit / app.Abort / Stop
bool ok = false;
std::string error;
std::string command_line;
};
ScriptFiber g_fiber;
thread_local bool t_on_fiber = false;
// Caller holds the GIL. Returns with the GIL held again after the script has given the baton back.
void hand_to_script()
{
PyThreadState* state = PyEval_SaveThread();
{
std::unique_lock<std::mutex> lock(g_fiber.mutex);
g_fiber.turn = ScriptFiber::Script;
g_fiber.cv.notify_all();
g_fiber.cv.wait(lock, [] { return g_fiber.turn == ScriptFiber::Host; });
}
PyEval_RestoreThread(state);
}
void join_fiber()
{
if (!g_fiber.alive)
return;
PyThreadState* state = PyEval_SaveThread();
pthread_join(g_fiber.thread, nullptr);
PyEval_RestoreThread(state);
g_fiber.alive = false;
}
bool run_main_script_body(const char* lpCmdLine, std::string* error);
void* fiber_main(void*)
{
{
std::unique_lock<std::mutex> lock(g_fiber.mutex);
g_fiber.cv.wait(lock, [] { return g_fiber.turn == ScriptFiber::Script; });
}
t_on_fiber = true;
PyGILState_STATE gil = PyGILState_Ensure();
std::string error;
const bool ok = run_main_script_body(g_fiber.command_line.c_str(), &error);
PyGILState_Release(gil);
std::lock_guard<std::mutex> lock(g_fiber.mutex);
g_fiber.ok = ok;
g_fiber.error = error;
g_fiber.finished = true;
g_fiber.looping = false;
g_fiber.turn = ScriptFiber::Host;
g_fiber.cv.notify_all();
return nullptr;
}
}
void init_2v0_gameplay_stubs();
@@ -63,20 +141,32 @@ void UIMouseButton(int button, bool pressed, int x, int y)
}
}
// 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) g_window_manager->RunKeyDown(key);
else g_window_manager->RunKeyUp(key);
if (pressed)
{
if (DIK_ESCAPE == key)
g_window_manager->RunPressEscapeKey();
g_window_manager->RunKeyDown(key);
}
else
g_window_manager->RunKeyUp(key);
}
void UIUpdate()
{
// While system.py is inside app.Loop(), one host frame is one CPythonApplication::Process(),
// which updates and renders the window tree itself.
if (IsAppLooping()) { AppFrame(); return; }
if (g_window_manager) g_window_manager->Update();
}
void UIRender()
{
if (IsAppLooping()) return;
UIRenderBeginFrame();
if (g_window_manager) g_window_manager->Render();
}
@@ -94,6 +184,8 @@ bool Start(const char* stdlib_path, std::string* error)
PythonHost::Configure();
g_launcher = std::make_unique<CPythonLauncher>(); // Py_Initialize
// The script fiber (RunMainScript) is a second OS thread; 2.7 creates the GIL lazily.
PyEval_InitThreads();
if (!g_launcher->Create())
{
Stop();
@@ -106,17 +198,19 @@ bool Start(const char* stdlib_path, std::string* 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>();
return true;
}
// 40250: RunMainScript
bool RunMainScript(const char* lpCmdLine, std::string* error)
{
if (!g_launcher)
return fail(error, "PythonBoot::Start has not run");
}
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.
@@ -189,6 +283,81 @@ bool RunMainScript(const char* lpCmdLine, std::string* error)
NANOEND
return true;
}
}
namespace PythonBoot
{
bool RunMainScript(const char* lpCmdLine, std::string* error)
{
if (!g_launcher)
return fail(error, "PythonBoot::Start has not run");
if (g_fiber.alive)
return fail(error, "RunMainScript is already running");
g_fiber.turn = ScriptFiber::Host;
g_fiber.finished = g_fiber.looping = g_fiber.quit = g_fiber.ok = false;
g_fiber.error.clear();
g_fiber.command_line = lpCmdLine ? lpCmdLine : "";
// 40250's main thread has the default 1 MB Windows stack; Python frames are deep during
// uiScriptLocale/ui imports, so give the script thread more than a secondary thread's default.
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 16u * 1024u * 1024u);
const int rc = pthread_create(&g_fiber.thread, &attr, fiber_main, nullptr);
pthread_attr_destroy(&attr);
if (rc != 0)
return fail(error, "cannot start the script thread");
g_fiber.alive = true;
hand_to_script(); // until system.py parks in app.Loop() or returns
if (g_fiber.finished)
{
join_fiber();
if (!g_fiber.ok)
return fail(error, g_fiber.error);
}
return true;
}
bool IsAppLooping()
{
return g_fiber.alive && g_fiber.looping && !g_fiber.finished;
}
bool AppFrame()
{
if (!IsAppLooping())
return false;
hand_to_script();
if (g_fiber.finished)
join_fiber();
return IsAppLooping();
}
bool AppLoopWait()
{
if (!t_on_fiber)
return false;
g_fiber.looping = true;
if (g_fiber.quit)
return false;
PyThreadState* state = PyEval_SaveThread();
{
std::unique_lock<std::mutex> lock(g_fiber.mutex);
g_fiber.turn = ScriptFiber::Host;
g_fiber.cv.notify_all();
g_fiber.cv.wait(lock, [] { return g_fiber.turn == ScriptFiber::Script; });
}
PyEval_RestoreThread(state);
return !g_fiber.quit;
}
void AppRequestQuit()
{
g_fiber.quit = true;
}
bool RunLine(const char* source, std::string* error)
{
@@ -205,9 +374,21 @@ void Stop()
{
if (!g_launcher)
return;
// In 40250, closing the window ends Loop(); RunApp() then runs mainStream.Destroy() and returns.
if (g_fiber.alive)
{
AppRequestQuit();
while (g_fiber.alive && !g_fiber.finished)
hand_to_script();
join_fiber();
}
g_fiber.finished = false;
// 40250 Main(): pyLauncher.Clear() runs before app->Destroy()/delete app, so the window manager
// (a CPythonApplication member) is still alive while Py_Finalize runs the windows' __del__.
g_launcher->Clear();
g_window_manager.reset();
g_resource.reset();
g_launcher->Clear(); // Main() calls Clear() explicitly before the launcher leaves scope
g_timer.reset();
g_launcher.reset();
}
@@ -29,7 +29,18 @@ bool IsRunning();
// __COMMAND_LINE__ and RunFile("system.py"). 2V0-e registers temporary observable gameplay modules;
// they are replaced by the real modules in later 2V slices.
// False leaves *error set to what the launcher reported through LogBox (platform/EterBase/LogBox.h).
//
// PORT: system.py runs on a script thread. RunMainScript returns once prototype.RunApp() has reached
// app.Loop() (IsAppLooping) or the script has ended; from then on every AppFrame() runs exactly one
// CPythonApplication::Process() while the host thread waits, so the two never run concurrently.
bool RunMainScript(const char* lpCmdLine, std::string* error);
bool IsAppLooping();
bool AppFrame();
// Used by the platform CPythonApplication: Loop() calls AppLoopWait() before each Process() and stops
// when it returns false; Exit()/Abort() call AppRequestQuit() (40250: PostQuitMessage).
bool AppLoopWait();
void AppRequestQuit();
// CPythonLauncher::RunLine, with the reported text handed back rather than only shown. The host's own
// check uses it to prove the interpreter and the standard library work in-process on a device that has
@@ -42,6 +53,7 @@ void SetUISize(int width, int height);
void UIMouseMove(int x, int y);
void UIMouseButton(int button, bool pressed, int x, int y);
void UIKey(int key, bool pressed);
// While IsAppLooping(), UIUpdate is AppFrame() and UIRender keeps the commands that Process() drew.
void UIUpdate();
void UIRender();
@@ -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; }