// 批次 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> // // 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 // DIK_ESCAPE #include #include #include #include #include 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:@"). 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); } 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; }