368 lines
15 KiB
C++
368 lines
15 KiB
C++
// 批次 2V0-f: the real 40250 system.py driven through the host lifecycle PythonBoot gives Godot.
|
|
//
|
|
// prototype.RunApp() runs app.Create, mainStream.SetLogoPhase() and then blocks in app.Loop() until
|
|
// the window closes (40250 PythonApplication.cpp:980). Here the script runs on PythonBoot's script
|
|
// thread and every PythonBoot::AppFrame() is one CPythonApplication::Process(); this test is what the
|
|
// Godot surface does each frame, minus Godot:
|
|
//
|
|
// 1. RunMainScript returns with the script parked in app.Loop(), the logo phase open;
|
|
// 2. frames advance introLogo.LogoWindow.OnUpdate through both logo videos into the login phase
|
|
// (40250's appLogoOpen/Update are themselves commented out: open succeeds, update reports done);
|
|
// 3. the popup that networkModule.MainStream created opens, draws, and closes from an ESC key
|
|
// (PopupDialog.OnPressEscapeKey) and from a click on its accept button;
|
|
// 4. Stop() ends Loop() and RunApp() runs mainStream.Destroy() before the thread is joined.
|
|
//
|
|
// port_app_loop_test <40250 Client dir> <python27.zip>
|
|
//
|
|
// Exit 77 (ctest SKIP) when the client is missing, unless MT_ASSETS_STRICT=1, which fails instead.
|
|
#include "ScriptLib/StdAfx.h"
|
|
#include "../src/platform/ScriptLib/PythonBoot.h"
|
|
#include "../src/platform/ScriptLib/PythonHost.h"
|
|
#include "../src/platform/UserInterface/UserInterface.h"
|
|
#include "../src/platform/EterLib/UIRenderCommands.h"
|
|
#include "EterLib/GrpScreen.h"
|
|
#include "EterPythonLib/PythonGraphic.h"
|
|
#include <dinput.h> // DIK_ESCAPE
|
|
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
|
|
#include <unistd.h>
|
|
|
|
static int g_failures = 0;
|
|
#define CHECK(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
|
|
++g_failures; \
|
|
} \
|
|
} while (0)
|
|
|
|
static bool py(const char* source)
|
|
{
|
|
std::string error;
|
|
if (PythonBoot::RunLine(source, &error))
|
|
return true;
|
|
std::fprintf(stderr, "python: %s\n%s\n", source, error.c_str());
|
|
return false;
|
|
}
|
|
|
|
// Evaluates a Python expression on the host thread without raising (a raise would log every frame).
|
|
static bool py_true(const char* expression)
|
|
{
|
|
PyObject* globals = PyModule_GetDict(PyImport_AddModule((char*) "__main__"));
|
|
PyObject* result = PyRun_String(expression, Py_eval_input, globals, globals);
|
|
if (!result)
|
|
{
|
|
PyErr_Clear();
|
|
return false;
|
|
}
|
|
const bool truth = PyObject_IsTrue(result) == 1;
|
|
Py_DECREF(result);
|
|
return truth;
|
|
}
|
|
|
|
// CGraphicTextInstance draws one quad per glyph from a CGraphicFontTexture page ("mem:<id>@<rev>").
|
|
static int glyph_quads()
|
|
{
|
|
int count = 0;
|
|
for (const auto& command : UIRenderCommands())
|
|
count += command.kind == UIRenderCommand::Image && command.text.rfind("mem:", 0) == 0;
|
|
return count;
|
|
}
|
|
|
|
// Some glyph page holds inked pixels (A4R4G4B4 coverage in the alpha channel).
|
|
static bool glyph_page_has_ink()
|
|
{
|
|
for (const auto& command : UIRenderCommands())
|
|
{
|
|
UIMemoryTexture page;
|
|
if (command.kind != UIRenderCommand::Image || command.text.rfind("mem:", 0) != 0 ||
|
|
!UIRenderMemoryTexture(command.text, &page))
|
|
continue;
|
|
for (std::uint32_t argb : page.argb)
|
|
if ((argb >> 24) != 0)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool near(float a, float b)
|
|
{
|
|
return std::fabs(a - b) < 0.01f;
|
|
}
|
|
|
|
static const UIRenderCommand* image_command(const char* suffix)
|
|
{
|
|
const std::string tail = suffix;
|
|
for (const auto& command : UIRenderCommands())
|
|
if (command.kind == UIRenderCommand::Image && command.quad && command.text.size() >= tail.size() &&
|
|
command.text.compare(command.text.size() - tail.size(), tail.size(), tail) == 0)
|
|
return &command;
|
|
return nullptr;
|
|
}
|
|
|
|
static void frames(int count)
|
|
{
|
|
for (int i = 0; i < count && PythonBoot::IsAppLooping(); ++i)
|
|
PythonBoot::UIUpdate();
|
|
}
|
|
|
|
static void drawing_primitives()
|
|
{
|
|
UIRenderSetSize(800, 600);
|
|
UIRenderBeginFrame();
|
|
CScreen screen;
|
|
screen.SetDiffuseColor(0xFF123456u);
|
|
screen.RenderBox2d(10, 20, 30, 40);
|
|
screen.RenderGradationBar2d(50, 60, 70, 80, 0xFF102030u, 0xFF405060u);
|
|
const auto& commands = UIRenderCommands();
|
|
CHECK(commands.size() == 5);
|
|
if (commands.size() == 5)
|
|
{
|
|
CHECK(commands[0].kind == UIRenderCommand::Line && near(commands[0].x1, 10) && near(commands[0].y2, 20));
|
|
CHECK(commands[3].kind == UIRenderCommand::Line && near(commands[3].x2, 31));
|
|
CHECK(commands[4].kind == UIRenderCommand::GradientBar && commands[4].argb == 0xFF102030u &&
|
|
commands[4].end_argb == 0xFF405060u);
|
|
}
|
|
|
|
// 40250 CPythonGraphic::RenderCoolTimeBox: fTime 0.25 leaves 6 whole fan triangles from the top
|
|
// centre anticlockwise round to (+1, 0); fTime 0.3 has 5 whole ones and a sixth 60% of the way
|
|
// from (+1, +1) to (+1, 0).
|
|
UIRenderBeginFrame();
|
|
{
|
|
CPythonGraphic graphic;
|
|
graphic.RenderCoolTimeBox(100, 100, 10, 0.25f);
|
|
CHECK(commands.size() == 6);
|
|
if (commands.size() == 6)
|
|
{
|
|
CHECK(commands[0].kind == UIRenderCommand::Bar && commands[0].quad && commands[0].argb == 0x80000000u);
|
|
CHECK(near(commands[0].qx[0], 100) && near(commands[0].qy[0], 100));
|
|
CHECK(near(commands[0].qx[1], 100) && near(commands[0].qy[1], 90));
|
|
CHECK(near(commands[0].qx[2], 90) && near(commands[0].qy[2], 90));
|
|
CHECK(near(commands[5].qx[2], 110) && near(commands[5].qy[2], 100));
|
|
}
|
|
UIRenderBeginFrame();
|
|
graphic.RenderCoolTimeBox(100, 100, 10, 0.3f);
|
|
CHECK(commands.size() == 6);
|
|
if (commands.size() == 6)
|
|
CHECK(near(commands[5].qx[1], 110) && near(commands[5].qy[1], 110) &&
|
|
near(commands[5].qx[2], 110) && near(commands[5].qy[2], 104));
|
|
UIRenderBeginFrame();
|
|
graphic.RenderCoolTimeBox(100, 100, 10, 1.0f);
|
|
CHECK(commands.empty());
|
|
|
|
// A cooldown triangle crossing the viewport must have clipped vertices, not only a clipped bbox.
|
|
UIRenderSetClip(95, 95, 10, 10);
|
|
graphic.RenderCoolTimeBox(100, 100, 10, 0.25f);
|
|
CHECK(!commands.empty());
|
|
bool touched_clip_edge = false;
|
|
for (const auto& command : commands)
|
|
{
|
|
CHECK(command.kind == UIRenderCommand::Bar && command.quad);
|
|
for (int vertex = 0; vertex < 3; ++vertex)
|
|
{
|
|
CHECK(command.qx[vertex] >= 95 && command.qx[vertex] <= 105);
|
|
CHECK(command.qy[vertex] >= 95 && command.qy[vertex] <= 105);
|
|
touched_clip_edge |= near(command.qx[vertex], 95) || near(command.qx[vertex], 105) ||
|
|
near(command.qy[vertex], 95) || near(command.qy[vertex], 105);
|
|
}
|
|
}
|
|
CHECK(touched_clip_edge);
|
|
UIRenderRestoreClip();
|
|
UIRenderBeginFrame();
|
|
|
|
// CPythonGraphic::SetViewport also clips the UI commands to the viewport until RestoreViewport.
|
|
graphic.SetViewport(10, 20, 30, 40);
|
|
screen.RenderBar2d(0, 0, 5, 5);
|
|
graphic.RestoreViewport();
|
|
screen.RenderBar2d(0, 0, 5, 5);
|
|
CHECK(commands.size() == 2);
|
|
if (commands.size() == 2)
|
|
{
|
|
CHECK(near(commands[0].clip_x1, 10) && near(commands[0].clip_y1, 20) &&
|
|
near(commands[0].clip_x2, 40) && near(commands[0].clip_y2, 60));
|
|
CHECK(near(commands[1].clip_x1, 0) && near(commands[1].clip_x2, 800) && near(commands[1].clip_y2, 600));
|
|
}
|
|
}
|
|
UIRenderSetSize(0, 0);
|
|
UIRenderBeginFrame();
|
|
}
|
|
|
|
|
|
int main(int argc, char** argv)
|
|
{
|
|
drawing_primitives();
|
|
const char* strict = std::getenv("MT_ASSETS_STRICT");
|
|
const bool is_strict = strict && std::string(strict) == "1";
|
|
const std::string client = argc > 1 ? argv[1] : "";
|
|
const std::string stdlib = argc > 2 ? argv[2] : PythonHost::DefaultStdLibPath();
|
|
if (client.empty() || chdir(client.c_str()) != 0 || access("pack/Index", 0) != 0)
|
|
{
|
|
std::fprintf(stderr, "port_app_loop_test: no 40250 Client/pack at '%s'\n", client.c_str());
|
|
return is_strict ? 1 : 77;
|
|
}
|
|
|
|
PackSingletons();
|
|
CHECK(PackInitialize("pack"));
|
|
|
|
std::string error;
|
|
CHECK(PythonBoot::Start(stdlib.c_str(), &error));
|
|
if (!error.empty())
|
|
std::fprintf(stderr, "Start: %s\n", error.c_str());
|
|
|
|
// app.Create has no device without a canvas, and RunApp reports it the 40250 way.
|
|
CHECK(!PythonBoot::RunMainScript("", &error));
|
|
CHECK(error.find("Sorry, Your system does not support 3D graphics") != std::string::npos);
|
|
CHECK(!PythonBoot::IsAppLooping());
|
|
PythonBoot::Stop();
|
|
|
|
CHECK(PythonBoot::Start(stdlib.c_str(), &error));
|
|
PythonBoot::SetUISize(800, 600);
|
|
error.clear();
|
|
const bool ran = PythonBoot::RunMainScript("", &error);
|
|
if (!ran)
|
|
std::fprintf(stderr, "RunMainScript: %s\n", error.c_str());
|
|
CHECK(ran);
|
|
CHECK(PythonBoot::IsAppLooping());
|
|
|
|
// The script thread is parked in app.Loop(); the host thread can run Python in between.
|
|
CHECK(py("import __main__, networkModule\n"
|
|
"__main__._stream = [o for o in __import__('gc').get_objects() if isinstance(o, networkModule.MainStream)][0]\n"
|
|
"import introLogo\n"
|
|
"assert isinstance(_stream.curPhaseWindow, introLogo.LogoWindow), _stream.curPhaseWindow"));
|
|
|
|
// The phase switch waits for the curtain fade; give it a bounded number of frames.
|
|
for (int i = 0; i < 600 && PythonBoot::IsAppLooping(); ++i)
|
|
{
|
|
PythonBoot::UIUpdate();
|
|
if (py_true("__import__('introLogin').LoginWindow.__instancecheck__(_stream.curPhaseWindow)"))
|
|
break;
|
|
}
|
|
CHECK(PythonBoot::IsAppLooping());
|
|
CHECK(py("import introLogin\n"
|
|
"assert isinstance(_stream.curPhaseWindow, introLogin.LoginWindow), _stream.curPhaseWindow"));
|
|
|
|
// ThinBoard.BOARD_COLOR is grp.GenerateColor(0, 0, 0, 0.51): the server list board's Bar draws it.
|
|
CHECK(py("import grp\nassert grp.GenerateColor(0.0, 0.0, 0.0, 0.51) & 0xffffffff == 0x82000000"));
|
|
frames(1);
|
|
bool has_board = false;
|
|
for (const auto& command : UIRenderCommands())
|
|
has_board |= command.kind == UIRenderCommand::Bar && command.argb == 0x82000000u;
|
|
CHECK(has_board);
|
|
|
|
// PopupDialog: open with a message, render, close with ESC through the window manager's key path.
|
|
// The message is 11 glyphs of the default font (app.SetDefaultFontName, a Tahoma cell here).
|
|
const int glyphs_before = glyph_quads();
|
|
CHECK(py("_stream.popupWindow.Open('POPUP_PROBE')\nassert _stream.popupWindow.IsShow()"));
|
|
frames(1);
|
|
CHECK(glyph_quads() >= glyphs_before + 11);
|
|
CHECK(glyph_page_has_ink());
|
|
CHECK(py("assert _stream.popupWindow.GetChild('message').GetTextSize()[0] > 0"));
|
|
PythonBoot::UIKey(DIK_ESCAPE, true);
|
|
PythonBoot::UIKey(DIK_ESCAPE, false);
|
|
frames(1);
|
|
CHECK(py("assert not _stream.popupWindow.IsShow()"));
|
|
CHECK(glyph_quads() == glyphs_before);
|
|
|
|
// ... and with a click on its accept button, which runs the close event. Off-centre: the button's
|
|
// caption is a 0x0 TextLine at its exact centre and CWindow::IsIn is inclusive, so in 40250 too
|
|
// that one pixel picks the caption instead of the button.
|
|
CHECK(py("_popup_closed = []\n"
|
|
"_stream.popupWindow.Open('POPUP_PROBE', lambda: _popup_closed.append(1))\n"
|
|
"_accept = _stream.popupWindow.GetChild('accept')\n"
|
|
"_ax, _ay = _accept.GetGlobalPosition()\n"
|
|
"__main__._accept_point = (_ax + 4, _ay + _accept.GetHeight() // 2)"));
|
|
frames(1);
|
|
PyObject* point = PyDict_GetItemString(PyModule_GetDict(PyImport_AddModule((char*) "__main__")), "_accept_point");
|
|
CHECK(point && PyTuple_Check(point));
|
|
if (point && PyTuple_Check(point))
|
|
{
|
|
const int x = int(PyInt_AsLong(PyTuple_GetItem(point, 0)));
|
|
const int y = int(PyInt_AsLong(PyTuple_GetItem(point, 1)));
|
|
PythonBoot::UIMouseMove(x, y);
|
|
PythonBoot::UIMouseButton(1, true, x, y);
|
|
PythonBoot::UIMouseButton(1, false, x, y);
|
|
}
|
|
frames(1);
|
|
CHECK(py("assert not _stream.popupWindow.IsShow() and _popup_closed == [1]"));
|
|
|
|
// ExpandedImageBox (CGraphicExpandedImageInstance::OnRender): scale + rendering rect, then rotation.
|
|
CHECK(py("import ui\n"
|
|
"_ex = ui.ExpandedImageBox()\n"
|
|
"_ex.LoadImage('d:/ymir work/ui/pattern/board_base.tga')\n"
|
|
"_ex.SetPosition(100, 50)\n"
|
|
"_ex.SetScale(2.0, 1.0)\n"
|
|
"_ex.SetRenderingRect(0.0, 0.0, -0.25, 0.0)\n"
|
|
"_ex.Show()"));
|
|
frames(1);
|
|
const UIRenderCommand* ex = image_command("board_base.tga");
|
|
CHECK(ex);
|
|
if (ex)
|
|
{
|
|
// Unscaled image size from the texture coordinates: eu = (w + right) / w, right = long(w * -0.25).
|
|
const float w = ex->qx[1] - ex->qx[0] + 0.0f; // 2w + right
|
|
const float h = ex->qy[2] - ex->qy[0];
|
|
const float image_w = std::round(w / (1.0f + ex->eu)); // 2w + w*(eu-1) = w*(1+eu)
|
|
CHECK(near(ex->qx[0], 100.0f) && near(ex->qy[0], 50.0f));
|
|
CHECK(near(ex->qx[2], 100.0f) && near(ex->qy[1], 50.0f));
|
|
CHECK(image_w > 0 && near(w, 2 * image_w + long(image_w * -0.25f)));
|
|
CHECK(near(ex->su, 0) && near(ex->sv, 0) && near(ex->ev, 1));
|
|
CHECK(near(ex->eu, (image_w + long(image_w * -0.25f)) / image_w));
|
|
CHECK(h > 0 && near(ex->qy[3], 50.0f + h));
|
|
}
|
|
CHECK(py("_ex.SetRenderingRect(0.0, 0.0, 0.0, 0.0)\n_ex.SetRotation(90.0)"));
|
|
frames(1);
|
|
ex = image_command("board_base.tga");
|
|
CHECK(ex);
|
|
if (ex)
|
|
{
|
|
// Rotated about the origin (the image centre, set on load): the top edge now points down.
|
|
const float cx = (ex->qx[0] + ex->qx[3]) / 2, cy = (ex->qy[0] + ex->qy[3]) / 2;
|
|
const float top_x = ex->qx[1] - ex->qx[0], top_y = ex->qy[1] - ex->qy[0];
|
|
CHECK(near(top_x, 0) && top_y > 0);
|
|
CHECK(near(ex->eu, 1) && near(ex->ev, 1));
|
|
CHECK(py("__main__._ex_origin = (100.0 + _ex.GetWidth() / 2.0 / 2.0, 50.0 + _ex.GetHeight() / 2.0)"));
|
|
PyObject* origin = PyDict_GetItemString(PyModule_GetDict(PyImport_AddModule((char*) "__main__")), "_ex_origin");
|
|
CHECK(origin && near(cx, float(PyFloat_AsDouble(PyTuple_GetItem(origin, 0)))) &&
|
|
near(cy, float(PyFloat_AsDouble(PyTuple_GetItem(origin, 1)))));
|
|
}
|
|
CHECK(py("_ex.Hide()\n_ex = None"));
|
|
|
|
// A .sub draws its parent texture through the sub rect (40250 CGraphicSubImage shares the D3D texture).
|
|
CHECK(py("_sub = ui.ImageBox()\n"
|
|
"_sub.LoadImage('d:/ymir work/ui/public/large_button_01.sub')\n"
|
|
"_sub.SetPosition(10, 10)\n"
|
|
"_sub.Show()"));
|
|
frames(1);
|
|
const UIRenderCommand* sub = nullptr;
|
|
for (const auto& command : UIRenderCommands())
|
|
if (command.kind == UIRenderCommand::Image && near(command.qx[0], 10) && near(command.qy[0], 10))
|
|
sub = &command;
|
|
CHECK(sub);
|
|
if (sub)
|
|
{
|
|
CHECK(sub->text.find(".sub") == std::string::npos && !sub->text.empty());
|
|
CHECK(sub->su >= 0 && sub->eu <= 1 && sub->sv >= 0 && sub->ev <= 1);
|
|
CHECK(sub->eu - sub->su < 1 || sub->ev - sub->sv < 1);
|
|
}
|
|
CHECK(py("_sub.Hide()\n_sub = None"));
|
|
|
|
// app.Exit() is 40250's PostQuitMessage: the next frame leaves Loop() and RunApp() finishes.
|
|
CHECK(py("import app\napp.Exit()"));
|
|
frames(1);
|
|
CHECK(!PythonBoot::IsAppLooping());
|
|
CHECK(py("assert _stream.curPhaseWindow == 0 and _stream.popupWindow == 0"));
|
|
|
|
PythonBoot::Stop();
|
|
CHECK(!PythonBoot::IsRunning());
|
|
|
|
if (g_failures)
|
|
std::fprintf(stderr, "%d check(s) failed\n", g_failures);
|
|
else
|
|
std::printf("port_app_loop_test: ok\n");
|
|
return g_failures ? 1 : 0;
|
|
}
|