diff --git a/docs/PORT-PLAN.md b/docs/PORT-PLAN.md index ffdcff4b..3235098c 100644 --- a/docs/PORT-PLAN.md +++ b/docs/PORT-PLAN.md @@ -116,8 +116,11 @@ SDL3 宿主主循环(src/host/live_client.cpp run_live_client) PORT 扩展(40250 没有对应物,只为移动端/平台而加;不能改 40250 脚本;玩法语义只在用户明确要求时扩展,如 5 页背包): - 由 `PythonBoot::RunMainScript` 注入的 Python 模块:`mt_autohunt`(自动狩猎,`AutoHuntScript.inc`)、`mt_register` - (客户端内账号注册)、`mt_framerate`(系统选项里的"画面帧率"行)、`mt_inventory`(5 页背包:改写 `InventoryWindow.py` 的装备格起点和页签, + (客户端内账号注册)、`mt_framerate`(系统选项里的"画面帧率"行)、`mt_uiscale`(系统选项里的"界面大小"行)、`mt_inventory`(5 页背包:改写 `InventoryWindow.py` 的装备格起点和页签, 泛化翻页、自动药水高亮和选宝石窗口的格子扫描)。它们包装 40250 窗口类的方法,不修改脚本文件。 +- 界面大小(`platform/EterBase/UIScaleMode.h` + `src/host/ui_scale.cpp`):Android 上 40250 界面按 600 / 700 / 800 + 逻辑像素高排版(默认 600,最低就是 600:背包窗口 565 高),宽度随屏幕比例;选择存在 `ui_height.cfg`,下次启动生效。 + 桌面按窗口大小排版,`MT_UI_HEIGHT=600` 可在桌面上测试这一设置。 - 触控控制(`src/host/touch_controller.h`):摇杆、攻击/技能按钮,转成 40250 的输入接口。 - 帧率与功耗(`platform/EterBase/FrameRateMode.h` + `src/host/frame_policy.cpp` 的 `FrameRatePolicy`):30 / 60(默认)/ 显示器最高三档, 无输入一段时间降到 30 fps(自动狩猎时除外),Android 上配合 `setFrameRate` 和 ADPF。 diff --git a/src/host/CMakeLists.txt b/src/host/CMakeLists.txt index 6d8a91bc..9f285dbc 100644 --- a/src/host/CMakeLists.txt +++ b/src/host/CMakeLists.txt @@ -25,6 +25,7 @@ set(MT_NATIVE_RENDER_SOURCES vulkan_window.cpp sdl_window.cpp ui_batches.cpp + ui_scale.cpp vk_device.cpp vk_frame.cpp vk_pipelines.cpp diff --git a/src/host/live_client.cpp b/src/host/live_client.cpp index 749ac9a8..2d287430 100644 --- a/src/host/live_client.cpp +++ b/src/host/live_client.cpp @@ -8,6 +8,7 @@ #include "native_audio.h" #include "perf_log.h" #include "texture_decode.h" +#include "ui_scale.h" #include "vulkan_window.h" #include "platform/MilesLib/AudioCommands.h" @@ -452,6 +453,7 @@ int run_live_client( }; while ((frames == 0 || completed < frames) && renderer.poll(true) && PythonBoot::IsAppLooping()) { apply_policy(); + save_ui_height_if_changed(); double pace_ms = 0.0; if (cap_period.count()) { // Fixed cadence: a late frame starts the next one at once, a frame more than one period diff --git a/src/host/sdl_window.cpp b/src/host/sdl_window.cpp index 468146ea..f978bbaa 100644 --- a/src/host/sdl_window.cpp +++ b/src/host/sdl_window.cpp @@ -3,6 +3,7 @@ #include "input_keymap.h" #include "texture_decode.h" +#include "ui_scale.h" #ifdef MT_NATIVE_HAS_LIVE_CLIENT #include "platform/ScriptLib/PythonBoot.h" @@ -141,20 +142,17 @@ void VulkanWindow::request_screenshot(std::string path, std::uint64_t frame) { std::uint32_t VulkanWindow::logical_width() const { int w = 0, h = 0; if (window_) SDL_GetWindowSize(window_, &w, &h); -#ifdef __ANDROID__ - // Keep the 40250 UI at the same height as the macOS 1280x800 window, - // while retaining the device aspect ratio on wider phone displays. - if (w > 0 && h > 0) return std::max(1, int(std::lround(double(w) * 800.0 / h))); -#endif + // The UI size setting (ui_scale.h; always on Android, MT_UI_HEIGHT on the desktop) fixes the logical + // height and keeps the device aspect ratio on wider phone displays. + if (const int ui_h = mt_host::ui_height(); ui_h > 0 && w > 0 && h > 0) + return std::max(1, int(std::lround(double(w) * ui_h / h))); return w > 0 ? static_cast(w) : extent_.width; } std::uint32_t VulkanWindow::logical_height() const { int w = 0, h = 0; if (window_) SDL_GetWindowSize(window_, &w, &h); -#ifdef __ANDROID__ - if (w > 0 && h > 0) return 800; -#endif + if (const int ui_h = mt_host::ui_height(); ui_h > 0 && w > 0 && h > 0) return std::uint32_t(ui_h); return h > 0 ? static_cast(h) : extent_.height; } diff --git a/src/host/ui_scale.cpp b/src/host/ui_scale.cpp new file mode 100644 index 00000000..d1bed9ed --- /dev/null +++ b/src/host/ui_scale.cpp @@ -0,0 +1,58 @@ +#include "ui_scale.h" + +#include "platform/EterBase/UIScaleMode.h" + +#include + +#include +#include +#include + +namespace mt_host { +namespace { + +std::string settings_path() { + std::string dir; + if (char* pref = SDL_GetPrefPath("metin2port", "client")) { + dir = pref; + SDL_free(pref); + } + return dir + "ui_height.cfg"; +} + +int g_saved = 0; + +} // namespace + +int ui_height() { + static const int height = [] { + int saved = 0; +#ifdef __ANDROID__ + saved = MtUIScale::c_iDefaultHeight; + if (std::ifstream in{settings_path()}; in) { + int value = 0; + if (in >> value && MtUIScale::IsValid(value)) saved = value; + } +#endif + int applied = saved; + if (const char* env = std::getenv("MT_UI_HEIGHT"); env && *env) { + const int value = std::atoi(env); + if (MtUIScale::IsValid(value)) applied = saved = value; + } + g_saved = saved; + MtUIScale::Applied().store(applied); + MtUIScale::Chosen().store(saved); + return applied; + }(); + return height; +} + +void save_ui_height_if_changed() { + const int chosen = MtUIScale::Chosen().load(std::memory_order_relaxed); + if (!chosen || chosen == g_saved) return; + g_saved = chosen; + std::ofstream out{settings_path(), std::ios::trunc}; + out << chosen << '\n'; +} + +} // namespace mt_host diff --git a/src/host/ui_scale.h b/src/host/ui_scale.h new file mode 100644 index 00000000..570b392d --- /dev/null +++ b/src/host/ui_scale.h @@ -0,0 +1,13 @@ +#pragma once + +namespace mt_host { + +// The logical UI height (platform/EterBase/UIScaleMode.h): the saved setting on Android, MT_UI_HEIGHT on +// the desktop (a test override that also shows the option), 0 when the window's own size is the UI size. +// Loaded on first use, which needs SDL initialised. +int ui_height(); + +// Writes the player's choice from the system option dialog when it differs from the saved one. +void save_ui_height_if_changed(); + +} // namespace mt_host diff --git a/src/platform/EterBase/UIScaleMode.h b/src/platform/EterBase/UIScaleMode.h new file mode 100644 index 00000000..fbc9fe1f --- /dev/null +++ b/src/platform/EterBase/UIScaleMode.h @@ -0,0 +1,45 @@ +#pragma once +// PORT: the player's UI size setting (system option dialog, app.Get/SetUIHeight), shared between the +// script thread and the host (src/host/ui_scale.cpp), which lays the 40250 UI out that many logical +// pixels high and scales it onto the screen, and saves the choice. 40250 has no counterpart: the PC +// client's UI pixels were screen pixels. A lower height draws every window and text larger. + +#include + +namespace MtUIScale { + +// The choices, lowest (largest UI) first. 600 is the 40250 minimum resolution's height: the inventory +// and character windows need about that much. +inline constexpr int c_aiHeights[] = {600, 700, 800}; +inline constexpr int c_iDefaultHeight = 600; + +inline bool IsValid(int height) +{ + for (int choice : c_aiHeights) + if (choice == height) + return true; + return false; +} + +// The height the running client lays out at (0 = the setting is unavailable: a desktop window lays +// out at its own size). Set once by the host at startup; a new choice applies at the next start. +inline std::atomic& Applied() +{ + static std::atomic height{0}; + return height; +} + +// The player's choice, saved by the host when it changes. +inline std::atomic& Chosen() +{ + static std::atomic height{0}; + return height; +} + +inline void Set(int height) +{ + if (IsValid(height) && Applied().load(std::memory_order_relaxed)) + Chosen().store(height, std::memory_order_relaxed); +} + +} // namespace MtUIScale diff --git a/src/platform/ScriptLib/PythonBoot.cpp b/src/platform/ScriptLib/PythonBoot.cpp index 7aad295d..16a70b2f 100644 --- a/src/platform/ScriptLib/PythonBoot.cpp +++ b/src/platform/ScriptLib/PythonBoot.cpp @@ -877,6 +877,7 @@ namespace PythonBoot #include "AutoHuntScript.inc" #include "RegisterScript.inc" #include "FrameRateOptionScript.inc" +#include "UIScaleOptionScript.inc" #include "InventoryPagesScript.inc" static void install_script_module(const char* c_szModule, const char* c_szScript) @@ -934,6 +935,7 @@ bool RunMainScript(const char* lpCmdLine, std::string* error) install_script_module("mt_autohunt", c_szAutoHuntScript); install_script_module("mt_register", c_szRegisterScript); install_script_module("mt_framerate", c_szFrameRateOptionScript); + install_script_module("mt_uiscale", c_szUIScaleOptionScript); install_script_module("mt_inventory", c_szInventoryPagesScript); } return true; diff --git a/src/platform/ScriptLib/UIScaleOptionScript.inc b/src/platform/ScriptLib/UIScaleOptionScript.inc new file mode 100644 index 00000000..6a3ef4a3 --- /dev/null +++ b/src/platform/ScriptLib/UIScaleOptionScript.inc @@ -0,0 +1,92 @@ +// The UI-size row of the system option dialog (PORT; 40250 has no counterpart, its UI pixels were screen +// pixels), run by PythonBoot::RunMainScript as module mt_uiscale after mt_framerate. It wraps +// uiSystemOption.OptionDialog's __Load, grows the dialog by one row below the frame-rate row and fills it +// in the same style. The value is app.Get/SetUIHeight (platform/EterBase/UIScaleMode.h): the logical +// height the host lays the UI out at, saved by the host and applied at the next start. The row is only +// added where the setting exists (Android, or MT_UI_HEIGHT on the desktop). +// Strings are UTF-8 here and go through player.AutoHuntLocalize into the locale's code page. +static const char c_szUIScaleOptionScript[] = R"PY(# -*- coding: utf-8 -*- +import app +import chat +import player +import ui +import uiSystemOption + +L = player.AutoHuntLocalize + +SMALL_BUTTON = "d:/ymir work/ui/public/small_button_%02d.sub" +ROW_Y = 235 +ROW_H = 25 +HEIGHTS = (600, 700, 800) +HEIGHT_TEXTS = ("大", "中", "小") + + +def _Refresh(buttons): + height = app.GetUIHeight() + for i in xrange(len(buttons)): + if HEIGHTS[i] == height: + buttons[i].Down() + else: + buttons[i].SetUp() + + +def _Select(buttons, height): + changed = height != app.GetUIHeight() + app.SetUIHeight(height) + _Refresh(buttons) + if changed: + if height == app.GetAppliedUIHeight(): + chat.AppendChat(chat.CHAT_TYPE_INFO, L("界面大小已恢复为当前设置")) + else: + chat.AppendChat(chat.CHAT_TYPE_INFO, L("界面大小将在重新启动游戏后生效")) + + +def _Attach(dialog): + if not app.GetAppliedUIHeight(): + return + board = dialog.GetChild("board") + width = dialog.GetWidth() + height = dialog.GetHeight() + ROW_H + dialog.SetSize(width, height) + board.SetSize(width, height) + dialog.SetCenterPosition() + + label = ui.TextLine() + label.SetParent(board) + label.SetPosition(30, ROW_Y + 2) + label.SetText(L("界面大小")) + label.Show() + + buttons = [] + for i in xrange(len(HEIGHTS)): + button = ui.RadioButton() + button.SetParent(board) + button.SetUpVisual(SMALL_BUTTON % 1) + button.SetOverVisual(SMALL_BUTTON % 2) + button.SetDownVisual(SMALL_BUTTON % 3) + button.SetPosition(110 + 50 * i, ROW_Y) + button.SetText(L(HEIGHT_TEXTS[i])) + button.SetEvent(lambda height=HEIGHTS[i]: _Select(buttons, height)) + button.Show() + buttons.append(button) + _Refresh(buttons) + + dialog.mtUIScaleLabel = label + dialog.mtUIScaleButtons = buttons + + +def __Install(): + load = uiSystemOption.OptionDialog._OptionDialog__Load + + def Load(self): + load(self) + try: + _Attach(self) + except: + import dbg + dbg.TraceError("mt_uiscale: cannot add the UI size row") + + uiSystemOption.OptionDialog._OptionDialog__Load = Load + +__Install() +)PY"; diff --git a/src/port/UserInterface/PythonApplicationModule.cpp b/src/port/UserInterface/PythonApplicationModule.cpp index 6564744b..7f24f5b0 100644 --- a/src/port/UserInterface/PythonApplicationModule.cpp +++ b/src/port/UserInterface/PythonApplicationModule.cpp @@ -4,6 +4,7 @@ #include "PythonApplication.h" #include "../EterLib/Camera.h" #include "../../platform/EterBase/FrameRateMode.h" // PORT: frame-rate setting +#include "../../platform/EterBase/UIScaleMode.h" // PORT: UI size setting extern bool PERF_CHECKER_RENDER_GAME; extern D3DXCOLOR g_fSpecularColor; @@ -1207,6 +1208,27 @@ PyObject* appSetFrameRateMode(PyObject* poSelf, PyObject* poArgs) return Py_BuildNone(); } +// PORT: the UI size setting of the system option dialog (platform/EterBase/UIScaleMode.h). GetUIHeight is +// the player's choice (0 = unavailable), GetAppliedUIHeight the height this run lays out at. +PyObject* appGetUIHeight(PyObject* poSelf, PyObject* poArgs) +{ + return Py_BuildValue("i", MtUIScale::Chosen().load()); +} + +PyObject* appGetAppliedUIHeight(PyObject* poSelf, PyObject* poArgs) +{ + return Py_BuildValue("i", MtUIScale::Applied().load()); +} + +PyObject* appSetUIHeight(PyObject* poSelf, PyObject* poArgs) +{ + int iHeight; + if (!PyTuple_GetInteger(poArgs, 0, &iHeight)) + return Py_BuildException(); + MtUIScale::Set(iHeight); + return Py_BuildNone(); +} + void initapp() { static PyMethodDef s_methods[] = @@ -1275,6 +1297,9 @@ void initapp() { "GetRandom", appGetRandom, METH_VARARGS }, { "GetFrameRateMode", appGetFrameRateMode, METH_VARARGS }, // PORT { "SetFrameRateMode", appSetFrameRateMode, METH_VARARGS }, // PORT + { "GetUIHeight", appGetUIHeight, METH_VARARGS }, // PORT + { "GetAppliedUIHeight", appGetAppliedUIHeight, METH_VARARGS }, // PORT + { "SetUIHeight", appSetUIHeight, METH_VARARGS }, // PORT { "RunPythonFile", appRunPythonFile, METH_VARARGS }, { "IsWebPageMode", appIsWebPageMode, METH_VARARGS }, { "ShowWebPage", appShowWebPage, METH_VARARGS },