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:
co-authored by
Claude Opus 5.5
parent
5590714b16
commit
141206cb69
@@ -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"); }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user