Adapt Android mobile controls and native client UI
This commit is contained in:
+5
-14
@@ -1,6 +1,6 @@
|
||||
# Native Vulkan renderer prototype
|
||||
# Native Vulkan client
|
||||
|
||||
This is the standalone renderer path for the 40250 `Render3DDraw` and `UIRenderCommand` command streams, plus direct `--live-client` execution of the ported 40250 client (`PythonBoot`). SDL3 owns the window and input forwarding; Vulkan owns the swapchain (`FIFO` or `IMMEDIATE`/`MAILBOX`), `VK_FORMAT_D32_SFLOAT` depth buffer, `VK_QUERY_TYPE_TIMESTAMP` hardware GPU timer, key-indexed persistent vertex/index buffers, GPU skeletal skinning bone palette SSBO (`set = 1, binding = 0`), DDS/TGA/`mem:` glyph-page texture sampler descriptors, fixed-function 3D + 2D UI state pipelines, and draw submission. On macOS, the Vulkan loader uses MoltenVK over Metal. The existing Godot client remains the default playable path.
|
||||
This is the standalone renderer path for the 40250 `Render3DDraw` and `UIRenderCommand` command streams, plus direct `--live-client` execution of the ported 40250 client (`PythonBoot`). SDL3 owns the window and input forwarding; Vulkan owns the swapchain (`FIFO` or `IMMEDIATE`/`MAILBOX`), `VK_FORMAT_D32_SFLOAT` depth buffer, `VK_QUERY_TYPE_TIMESTAMP` hardware GPU timer, key-indexed persistent vertex/index buffers, GPU skeletal skinning bone palette SSBO (`set = 1, binding = 0`), DDS/TGA/`mem:` glyph-page texture sampler descriptors, fixed-function 3D + 2D UI state pipelines, and draw submission. On macOS, the Vulkan loader uses MoltenVK over Metal. This is the active macOS and Android client path.
|
||||
|
||||
## Build and run on macOS
|
||||
|
||||
@@ -9,18 +9,9 @@ Install Vulkan headers/loader, MoltenVK, SDL3 and `glslc` (shaderc). With Homebr
|
||||
```sh
|
||||
brew install vulkan-headers vulkan-loader molten-vk sdl3 shaderc
|
||||
|
||||
# 1. Standalone replay build (without port_platform)
|
||||
cmake -S . -B build-native-render -DMTGODOT_BUILD_EXTENSION=OFF \
|
||||
-DMT_BUILD_NATIVE_RENDER_PROTOTYPE=ON -DCMAKE_PREFIX_PATH=/opt/homebrew
|
||||
cmake --build build-native-render --target mt_native_render -j8
|
||||
|
||||
# 2. Full live-client build (links port_platform + mtpython + FakeLoginServer)
|
||||
cmake -S . -B build -DMT_BUILD_NATIVE_RENDER_PROTOTYPE=ON -DCMAKE_PREFIX_PATH=/opt/homebrew
|
||||
cmake --build build --target mtgodot mt_native_render port_fake_login_server -j8
|
||||
|
||||
# 3. Optimized Release (-O3) live-client build
|
||||
cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release -DMT_BUILD_NATIVE_RENDER_PROTOTYPE=ON \
|
||||
-DMTGODOT_EMBED_PYTHON=ON -DCMAKE_PREFIX_PATH=/opt/homebrew
|
||||
# Optimized Release build (includes port_platform, embedded Python and FakeLoginServer)
|
||||
cmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release -DMT_BUILD_NATIVE_RENDER=ON \
|
||||
-DMT_EMBED_PYTHON=ON -DCMAKE_PREFIX_PATH=/opt/homebrew
|
||||
cmake --build build-release --target mt_native_render -j8
|
||||
```
|
||||
|
||||
|
||||
+147
-25
@@ -9,6 +9,7 @@
|
||||
#include "platform/PackBackend.h"
|
||||
#include "platform/ScriptLib/PythonBoot.h"
|
||||
#include "platform/UserInterface/ServerClock.h"
|
||||
#include "platform/EterBase/TraceErrorObserver.h"
|
||||
#include "../tests/port_login_flow_server.h"
|
||||
#include <thread>
|
||||
#endif
|
||||
@@ -23,6 +24,7 @@
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <AudioToolbox/AudioToolbox.h>
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
@@ -230,8 +232,8 @@ std::uint64_t geometry_hash(const Render3DDraw& draw) {
|
||||
return hash_bytes(hash, draw.indices.data(), draw.indices.size() * sizeof(std::uint32_t));
|
||||
}
|
||||
|
||||
mtgodot::Image decode_tga(const std::uint8_t* data, std::size_t size) {
|
||||
mtgodot::Image out;
|
||||
mtimage::Image decode_tga(const std::uint8_t* data, std::size_t size) {
|
||||
mtimage::Image out;
|
||||
if (size < 18) return out;
|
||||
const std::uint8_t id_len = data[0];
|
||||
const std::uint8_t cmap_type = data[1];
|
||||
@@ -306,7 +308,7 @@ mtgodot::Image decode_tga(const std::uint8_t* data, std::size_t size) {
|
||||
return out;
|
||||
}
|
||||
|
||||
mtgodot::Image decode_texture_bytes(const std::uint8_t* data, std::size_t size) {
|
||||
mtimage::Image decode_texture_bytes(const std::uint8_t* data, std::size_t size) {
|
||||
if (!data || size < 12) return {};
|
||||
if (data[0] == 'M' && data[1] == 'T' && data[2] == 'R' && data[3] == 'A') {
|
||||
std::uint32_t w = 0, h = 0;
|
||||
@@ -314,7 +316,7 @@ mtgodot::Image decode_texture_bytes(const std::uint8_t* data, std::size_t size)
|
||||
std::memcpy(&h, data + 8, 4);
|
||||
const std::size_t bytes = std::size_t(w) * std::size_t(h) * 4;
|
||||
if (w > 0 && h > 0 && w <= 4096 && h <= 4096 && size == 12 + bytes) {
|
||||
mtgodot::Image out;
|
||||
mtimage::Image out;
|
||||
out.w = static_cast<std::uint16_t>(w);
|
||||
out.h = static_cast<std::uint16_t>(h);
|
||||
out.rgba.assign(data + 12, data + 12 + bytes);
|
||||
@@ -323,14 +325,14 @@ mtgodot::Image decode_texture_bytes(const std::uint8_t* data, std::size_t size)
|
||||
return {};
|
||||
}
|
||||
if (data[0] == 'D' && data[1] == 'D' && data[2] == 'S' && data[3] == ' ')
|
||||
return mtgodot::load_dds(data, size);
|
||||
return mtimage::load_dds(data, size);
|
||||
auto tga = decode_tga(data, size);
|
||||
if (tga.ok()) return tga;
|
||||
if (size > static_cast<std::size_t>(INT_MAX)) return {};
|
||||
int w = 0, h = 0, channels = 0;
|
||||
stbi_uc* pixels = stbi_load_from_memory(data, static_cast<int>(size), &w, &h, &channels, 4);
|
||||
if (pixels && w > 0 && h > 0 && w <= 4096 && h <= 4096) {
|
||||
mtgodot::Image out;
|
||||
mtimage::Image out;
|
||||
out.w = static_cast<std::uint16_t>(w);
|
||||
out.h = static_cast<std::uint16_t>(h);
|
||||
out.rgba.assign(pixels, pixels + std::size_t(w) * std::size_t(h) * 4);
|
||||
@@ -847,6 +849,7 @@ public:
|
||||
}
|
||||
#endif
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) throw std::runtime_error(SDL_GetError());
|
||||
SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight");
|
||||
window_ = SDL_CreateWindow(
|
||||
"Metin2 Native Vulkan Client",
|
||||
std::max(init_width, 320),
|
||||
@@ -979,6 +982,7 @@ public:
|
||||
screenshot_path_ = std::move(path);
|
||||
screenshot_frame_ = frame;
|
||||
}
|
||||
std::uint64_t frame_number() const { return frame_number_; }
|
||||
|
||||
int msaa_samples() const { return int(samples_); }
|
||||
std::uint32_t width() const { return extent_.width; }
|
||||
@@ -986,11 +990,19 @@ public:
|
||||
std::uint32_t 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
|
||||
return w > 0 ? static_cast<std::uint32_t>(w) : extent_.width;
|
||||
}
|
||||
std::uint32_t 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
|
||||
return h > 0 ? static_cast<std::uint32_t>(h) : extent_.height;
|
||||
}
|
||||
|
||||
@@ -1192,10 +1204,10 @@ public:
|
||||
if (event.type == SDL_EVENT_WINDOW_RESIZED || event.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) {
|
||||
int ew = 0, eh = 0;
|
||||
SDL_GetWindowSize(window_, &ew, &eh);
|
||||
touch_controller.update_screen_size(ew, eh);
|
||||
touch_controller.update_screen_size(int(logical_width()), int(logical_height()));
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (forward_to_live_client && ew > 0 && eh > 0) {
|
||||
PythonBoot::SetUISize(ew, eh);
|
||||
PythonBoot::SetUISize(int(logical_width()), int(logical_height()));
|
||||
}
|
||||
#endif
|
||||
recreate_swapchain();
|
||||
@@ -1658,9 +1670,12 @@ public:
|
||||
present.waitSemaphoreCount = 1; present.pWaitSemaphores = &rendered_;
|
||||
present.swapchainCount = 1; present.pSwapchains = &swapchain_; present.pImageIndices = &image_index;
|
||||
const auto presented = vkQueuePresentKHR(queue_, &present);
|
||||
if (presented == VK_ERROR_OUT_OF_DATE_KHR || presented == VK_SUBOPTIMAL_KHR) {
|
||||
// An Android surface can continuously report SUBOPTIMAL while the app deliberately
|
||||
// uses the identity transform. The image is still presentable; rebuilding every frame
|
||||
// stalls rendering without changing that status.
|
||||
if (presented == VK_ERROR_OUT_OF_DATE_KHR) {
|
||||
recreate_swapchain();
|
||||
} else if (presented != VK_SUCCESS) {
|
||||
} else if (presented != VK_SUCCESS && presented != VK_SUBOPTIMAL_KHR) {
|
||||
check(presented, "vkQueuePresentKHR");
|
||||
}
|
||||
const auto presented_at = std::chrono::steady_clock::now();
|
||||
@@ -2027,7 +2042,17 @@ private:
|
||||
info.imageFormat = format.format; info.imageColorSpace = format.colorSpace; info.imageExtent = extent_;
|
||||
info.imageArrayLayers = 1; info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT |
|
||||
(capabilities.supportedUsageFlags & VK_IMAGE_USAGE_TRANSFER_SRC_BIT);
|
||||
info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; info.preTransform = capabilities.currentTransform;
|
||||
info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
|
||||
// Android surfaces can report a quarter-turn transform even when SDL's window is
|
||||
// already landscape. Rendering in SDL window coordinates and requesting that transform
|
||||
// again rotates the entire game image on presentation.
|
||||
info.preTransform = (capabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR)
|
||||
? VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR : capabilities.currentTransform;
|
||||
#ifdef __ANDROID__
|
||||
SDL_Log("swapchain: window=%dx%d extent=%ux%u currentTransform=%u preTransform=%u",
|
||||
width, height, extent_.width, extent_.height,
|
||||
unsigned(capabilities.currentTransform), unsigned(info.preTransform));
|
||||
#endif
|
||||
info.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; info.presentMode = present_mode_;
|
||||
info.clipped = VK_TRUE;
|
||||
check(vkCreateSwapchainKHR(device_, &info, nullptr, &swapchain_), "vkCreateSwapchainKHR");
|
||||
@@ -2617,7 +2642,7 @@ private:
|
||||
}
|
||||
it->second.last_decode_attempt_frame = frame_number_;
|
||||
|
||||
mtgodot::Image decoded;
|
||||
mtimage::Image decoded;
|
||||
if (auto raw = capture_textures.find(texture_name); raw != capture_textures.end() && !raw->second.empty()) {
|
||||
decoded = decode_texture_bytes(raw->second.data(), raw->second.size());
|
||||
}
|
||||
@@ -3187,6 +3212,32 @@ bool py_eval_true(const char* expr) {
|
||||
return PythonBoot::Evaluate(expr, &res, &err) && (res == "True" || res == "1");
|
||||
}
|
||||
|
||||
#ifdef __ANDROID__
|
||||
bool hide_mobile_taskbar_quickslots() {
|
||||
return py_exec(
|
||||
"_mobile_taskbar = _stream.curPhaseWindow.interface.wndTaskBar\n"
|
||||
"for _mobile_quickslot in _mobile_taskbar.quickslot:\n"
|
||||
" _mobile_quickslot.Hide()\n"
|
||||
"_mobile_taskbar.GetChild('QuickSlotBoard').Hide()\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) && TARGET_OS_OSX
|
||||
bool configure_macos_numeric_quickslots() {
|
||||
return py_exec(
|
||||
"_mac_game = _stream.curPhaseWindow\n"
|
||||
"_mac_game.onPressKeyDict[app.DIK_5] = lambda: _mac_game._GameWindow__PressQuickSlot(4)\n"
|
||||
"_mac_game.onPressKeyDict[app.DIK_6] = lambda: _mac_game._GameWindow__PressQuickSlot(5)\n"
|
||||
"_mac_game.onPressKeyDict[app.DIK_7] = lambda: _mac_game._GameWindow__PressQuickSlot(6)\n"
|
||||
"_mac_game.onPressKeyDict[app.DIK_8] = lambda: _mac_game._GameWindow__PressQuickSlot(7)\n"
|
||||
"for _mac_key in (app.DIK_F1, app.DIK_F2, app.DIK_F3, app.DIK_F4):\n"
|
||||
" del _mac_game.onPressKeyDict[_mac_key]\n"
|
||||
"_mac_taskbar = _mac_game.interface.wndTaskBar\n"
|
||||
"for _mac_slot_number in range(5, 9):\n"
|
||||
" _mac_taskbar.GetChild('slot_%d' % _mac_slot_number).LoadImage('d:/ymir work/ui/game/taskbar/%d.sub' % _mac_slot_number)\n");
|
||||
}
|
||||
#endif
|
||||
|
||||
bool parse_live_server_spec(const std::string& spec, std::string& host, int& auth_port, int& game_port) {
|
||||
const auto p1 = spec.find(':');
|
||||
if (p1 == std::string::npos) return false;
|
||||
@@ -3206,15 +3257,25 @@ int run_live_client(
|
||||
bool gpu_skinning,
|
||||
bool native_terrain,
|
||||
bool login_screen,
|
||||
bool selection_screen,
|
||||
const std::string& live_server_spec,
|
||||
const std::string& capture_out) {
|
||||
const std::string& capture_out,
|
||||
const std::string& screenshot_out) {
|
||||
if (fake_mobs > 0) {
|
||||
const std::string mobs_str = std::to_string(fake_mobs);
|
||||
setenv("MT_FAKE_MOB_COUNT", mobs_str.c_str(), 1);
|
||||
}
|
||||
#ifdef __ANDROID__
|
||||
SetTraceErrorObserver([](const char* line) {
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "40250 SYSERR: %s", line);
|
||||
});
|
||||
#endif
|
||||
char resolved_client_dir[4096] = {};
|
||||
const std::string abs_client_dir = realpath(client_dir.c_str(), resolved_client_dir) ? std::string(resolved_client_dir) : client_dir;
|
||||
setenv("MT_40250_CLIENT", abs_client_dir.c_str(), 1);
|
||||
#ifdef __ANDROID__
|
||||
SDL_Log("live stage: pack init %s", abs_client_dir.c_str());
|
||||
#endif
|
||||
if (chdir(abs_client_dir.c_str()) != 0 || !mtpack40250::initialize(".")) {
|
||||
throw std::runtime_error("cannot initialize 40250 pack at: " + abs_client_dir);
|
||||
}
|
||||
@@ -3226,6 +3287,9 @@ int run_live_client(
|
||||
SetNativeTerrainRenderEnabled(native_terrain);
|
||||
SetGpuSkinningEnabled(gpu_skinning);
|
||||
NativeAudioEngine audio;
|
||||
#ifdef __ANDROID__
|
||||
SDL_Log("live stage: audio initialized");
|
||||
#endif
|
||||
|
||||
std::string server_host = "127.0.0.1";
|
||||
int auth_port = 0;
|
||||
@@ -3271,11 +3335,17 @@ int run_live_client(
|
||||
#endif
|
||||
if (!PythonBoot::Start(stdlib.c_str(), &error))
|
||||
throw std::runtime_error("PythonBoot::Start: " + error);
|
||||
#ifdef __ANDROID__
|
||||
SDL_Log("live stage: PythonBoot started");
|
||||
#endif
|
||||
|
||||
SetPlatformServerTime(123456789);
|
||||
PythonBoot::SetUISize(static_cast<int>(renderer.logical_width()), static_cast<int>(renderer.logical_height()));
|
||||
if (!PythonBoot::RunMainScript("", &error) || !PythonBoot::IsAppLooping())
|
||||
throw std::runtime_error("PythonBoot::RunMainScript: " + error);
|
||||
#ifdef __ANDROID__
|
||||
SDL_Log("live stage: main script started");
|
||||
#endif
|
||||
|
||||
const std::unordered_map<std::string, std::vector<std::uint8_t>> empty_textures;
|
||||
auto pump_until = [&](double max_seconds, int sleep_ms, const auto& cond) -> bool {
|
||||
@@ -3301,6 +3371,9 @@ int run_live_client(
|
||||
|
||||
if (!pump_until(20.0, 5, [] { return py_eval_true("isinstance(_stream.curPhaseWindow, introLogin.LoginWindow)"); }))
|
||||
throw std::runtime_error("timed out waiting for LoginWindow");
|
||||
#ifdef __ANDROID__
|
||||
SDL_Log("live stage: LoginWindow open");
|
||||
#endif
|
||||
|
||||
if (login_screen) {
|
||||
char conn_cmd[512];
|
||||
@@ -3338,18 +3411,28 @@ int run_live_client(
|
||||
auth_port,
|
||||
FakeLoginServer::kLogin,
|
||||
FakeLoginServer::kPassword);
|
||||
#ifdef __ANDROID__
|
||||
SDL_Log("live stage: submitting fake login");
|
||||
#endif
|
||||
if (!py_exec(login_cmd))
|
||||
throw std::runtime_error("failed to submit login credentials");
|
||||
#ifdef __ANDROID__
|
||||
SDL_Log("live stage: fake login submitted");
|
||||
#endif
|
||||
|
||||
if (!pump_until(20.0, 5, [&] {
|
||||
return (use_external_server || server.Has("game1:login2")) &&
|
||||
py_eval_true("isinstance(_stream.curPhaseWindow, introSelect.SelectCharacterWindow)");
|
||||
}))
|
||||
throw std::runtime_error("timed out waiting for SelectCharacterWindow");
|
||||
#ifdef __ANDROID__
|
||||
SDL_Log("live stage: SelectCharacterWindow open");
|
||||
#endif
|
||||
|
||||
if (!py_exec("_stream.curPhaseWindow.SelectSlot(0)\n"
|
||||
"_stream.curPhaseWindow.StartGame()"))
|
||||
throw std::runtime_error("failed to start game from SelectCharacterWindow");
|
||||
if (!selection_screen) {
|
||||
if (!py_exec("_stream.curPhaseWindow.SelectSlot(0)\n"
|
||||
"_stream.curPhaseWindow.StartGame()"))
|
||||
throw std::runtime_error("failed to start game from SelectCharacterWindow");
|
||||
|
||||
if (!use_external_server) {
|
||||
if (!pump_until(30.0, 5, [&] { return server.Has("game2:client_version"); }))
|
||||
@@ -3362,12 +3445,22 @@ int run_live_client(
|
||||
}))
|
||||
throw std::runtime_error("timed out waiting for GameWindow (server error: " + server.Error() + ")");
|
||||
|
||||
const int warmup_frames = std::max(10, fake_mobs / 2 + 10);
|
||||
for (int i = 0; i < warmup_frames; ++i) {
|
||||
if (!renderer.poll(true) || !PythonBoot::IsAppLooping()) break;
|
||||
PythonBoot::UIUpdate();
|
||||
renderer.sync_game_cursor();
|
||||
audio.pump();
|
||||
#ifdef __ANDROID__
|
||||
if (!hide_mobile_taskbar_quickslots())
|
||||
throw std::runtime_error("failed to hide the desktop quick-slot bar on Android");
|
||||
#endif
|
||||
#if defined(__APPLE__) && TARGET_OS_OSX
|
||||
if (!configure_macos_numeric_quickslots())
|
||||
throw std::runtime_error("failed to configure numeric quick slots on macOS");
|
||||
#endif
|
||||
|
||||
const int warmup_frames = std::max(10, fake_mobs / 2 + 10);
|
||||
for (int i = 0; i < warmup_frames; ++i) {
|
||||
if (!renderer.poll(true) || !PythonBoot::IsAppLooping()) break;
|
||||
PythonBoot::UIUpdate();
|
||||
renderer.sync_game_cursor();
|
||||
audio.pump();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3417,6 +3510,10 @@ int run_live_client(
|
||||
native_draw_capture::write(capture_out, Render3DDraws(), cap_textures, ui_w, ui_h, cap_ui);
|
||||
}
|
||||
|
||||
if (!screenshot_out.empty() && frames > 0) {
|
||||
renderer.request_screenshot(screenshot_out, renderer.frame_number() + static_cast<std::uint64_t>(frames));
|
||||
}
|
||||
|
||||
renderer.reset_timings();
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
double update_ms = 0.0;
|
||||
@@ -3442,10 +3539,16 @@ int run_live_client(
|
||||
++completed;
|
||||
}
|
||||
renderer.finish_gpu_timings();
|
||||
#ifdef __ANDROID__
|
||||
SDL_Log("live stage: render loop complete (%d frames)", completed);
|
||||
#endif
|
||||
const auto wall_ms = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start).count();
|
||||
print_summary(renderer, completed, wall_ms, update_ms, std::move(frame_ms));
|
||||
|
||||
PythonBoot::Stop();
|
||||
#ifdef __ANDROID__
|
||||
SDL_Log("live stage: PythonBoot stopped");
|
||||
#endif
|
||||
if (!use_external_server)
|
||||
server.Stop();
|
||||
return (frames == 0 || completed == frames) ? 0 : 2;
|
||||
@@ -3461,6 +3564,7 @@ int main(int argc, char** argv) {
|
||||
bool synthetic_specified = false;
|
||||
bool interactive = false;
|
||||
bool login_screen = false;
|
||||
bool selection_screen = false;
|
||||
int init_width = 1280;
|
||||
int init_height = 800;
|
||||
std::size_t draw_count = 64;
|
||||
@@ -3489,6 +3593,9 @@ int main(int argc, char** argv) {
|
||||
else if (arg == "--login-screen") {
|
||||
login_screen = true;
|
||||
interactive = true;
|
||||
} else if (arg == "--selection-screen") {
|
||||
selection_screen = true;
|
||||
interactive = true;
|
||||
} else if (arg == "--live-server" && i+1 < argc) {
|
||||
live_server_spec = argv[++i];
|
||||
interactive = true;
|
||||
@@ -3525,7 +3632,7 @@ int main(int argc, char** argv) {
|
||||
else if (arg == "--no-terrain") native_terrain = false;
|
||||
else if (arg == "--mobile") mobile_mode = true;
|
||||
else throw std::runtime_error(
|
||||
"usage: mt_native_render [--frames N] [--interactive] [--login-screen] "
|
||||
"usage: mt_native_render [--frames N] [--interactive] [--login-screen|--selection-screen] "
|
||||
"[--live-server HOST:AUTH_PORT:GAME_PORT] [--width W] [--height H] "
|
||||
"[--draws N] [--triangles-per-draw N] [--capture FILE] [--animate-first-draw] "
|
||||
"[--animate-bones] [--no-vsync] [--live-client DIR] [--fake-mobs N] "
|
||||
@@ -3558,6 +3665,12 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
}
|
||||
if (interactive && !frames_specified) frames = 0;
|
||||
#ifdef __ANDROID__
|
||||
SDL_Log("native startup: argc=%d live_client=%s selection=%d frames=%d", argc,
|
||||
live_client_dir.c_str(), selection_screen, frames);
|
||||
#endif
|
||||
if (login_screen && selection_screen)
|
||||
throw std::runtime_error("--login-screen and --selection-screen are mutually exclusive");
|
||||
if (frames < 0 || (!interactive && frames == 0) || draw_count == 0 || draw_count > 4096 ||
|
||||
triangles_per_draw == 0 || triangles_per_draw > 10000)
|
||||
throw std::runtime_error("invalid frame/draw/triangle count");
|
||||
@@ -3569,7 +3682,7 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
if (!live_client_dir.empty()) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
return run_live_client(
|
||||
const int result = run_live_client(
|
||||
renderer,
|
||||
live_client_dir,
|
||||
frames,
|
||||
@@ -3577,8 +3690,14 @@ int main(int argc, char** argv) {
|
||||
gpu_skinning,
|
||||
native_terrain,
|
||||
login_screen,
|
||||
selection_screen,
|
||||
live_server_spec,
|
||||
capture_out);
|
||||
capture_out,
|
||||
screenshot_out);
|
||||
#ifdef __ANDROID__
|
||||
SDL_Log("native live client exit: %d", result);
|
||||
#endif
|
||||
return result;
|
||||
#else
|
||||
throw std::runtime_error("mt_native_render was built without port_platform (--live-client unavailable)");
|
||||
#endif
|
||||
@@ -3620,6 +3739,9 @@ int main(int argc, char** argv) {
|
||||
return (frames == 0 || completed == frames) ? 0 : 2;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "native renderer: " << error.what() << '\n';
|
||||
#ifdef __ANDROID__
|
||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "native renderer: %s", error.what());
|
||||
#endif
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
+132
-211
@@ -17,9 +17,7 @@
|
||||
class TouchController {
|
||||
public:
|
||||
struct ButtonDef {
|
||||
int id; // 1 = ATK, 2 = S1, 3 = S2, 4 = S3, 5 = POT, 6 = PICK
|
||||
// 99 = MENU (toggle drawer)
|
||||
// 10 = BAG (DIK_I), 11 = CHAR (DIK_C), 12 = SKILL (DIK_V), 13 = QUEST (DIK_N), 14 = COMM (DIK_M), 15 = SET (DIK_ESCAPE)
|
||||
int id; // 1 = attack, 2..7 = quick slots 1..6
|
||||
int dik;
|
||||
float x, y, radius;
|
||||
const char* label;
|
||||
@@ -53,6 +51,14 @@ public:
|
||||
// Called once per frame to maintain continuous analog movement
|
||||
void update() {
|
||||
if (!enabled_) return;
|
||||
const bool controls_visible = gameplay_controls_visible();
|
||||
if (!controls_visible) {
|
||||
if (controls_were_visible_)
|
||||
release_gameplay_controls();
|
||||
controls_were_visible_ = false;
|
||||
return;
|
||||
}
|
||||
controls_were_visible_ = true;
|
||||
if (joystick_active_) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::SetMoveDirection(move_angle_, true);
|
||||
@@ -65,23 +71,25 @@ public:
|
||||
if (!enabled_) return false;
|
||||
const float px = norm_x * float(screen_w_);
|
||||
const float py = norm_y * float(screen_h_);
|
||||
const bool controls_visible = gameplay_controls_visible();
|
||||
|
||||
// 1. Check HUD buttons (Action cluster & Drawer menu)
|
||||
int btn_idx = find_button(px, py);
|
||||
if (btn_idx >= 0) {
|
||||
auto& btn = buttons_[btn_idx];
|
||||
if (btn.id == 99) { // MENU toggle button
|
||||
drawer_open_ = !drawer_open_;
|
||||
layout_buttons();
|
||||
// The mobile overlay owns only the movement and combat control regions.
|
||||
if (controls_visible) {
|
||||
const int btn_idx = find_button(px, py);
|
||||
if (btn_idx >= 0) {
|
||||
auto& btn = buttons_[btn_idx];
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (btn.id >= 2 && PythonBoot::TryAssignAttachedObjectToLocalQuickSlot(btn.id - 2))
|
||||
return true;
|
||||
#endif
|
||||
btn.pressed = true;
|
||||
btn.finger_id = finger_id;
|
||||
trigger_button(btn.dik, true);
|
||||
return true;
|
||||
}
|
||||
btn.pressed = true;
|
||||
btn.finger_id = finger_id;
|
||||
trigger_button(btn.dik, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Intelligent UI Hit-Testing: check if touch lands inside an open in-game UI window
|
||||
// Everything else uses the unchanged 40250 UI and its normal mouse path.
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (PythonBoot::IsPointInsideActiveUI(int(px), int(py))) {
|
||||
ui_touch_finger_id_ = finger_id;
|
||||
@@ -91,8 +99,8 @@ public:
|
||||
}
|
||||
#endif
|
||||
|
||||
// 3. Left side: virtual joystick (lower-left quadrant)
|
||||
if (norm_x < 0.40f && norm_y > 0.35f && !joystick_active_) {
|
||||
// Start movement only in the gameplay phase.
|
||||
if (controls_visible && norm_x < 0.40f && norm_y > 0.35f && !joystick_active_) {
|
||||
joystick_active_ = true;
|
||||
joystick_finger_id_ = finger_id;
|
||||
joystick_base_x_ = px;
|
||||
@@ -103,7 +111,10 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
// 4. 3D Game World: camera rotation, pinch zoom, or tap to target
|
||||
if (!controls_visible)
|
||||
return false;
|
||||
|
||||
// The remaining gameplay area controls camera and world selection.
|
||||
if (camera_finger_id_ < 0) {
|
||||
camera_finger_id_ = finger_id;
|
||||
camera_last_x_ = px;
|
||||
@@ -265,80 +276,12 @@ public:
|
||||
return false;
|
||||
}
|
||||
|
||||
// Render virtual joystick, buttons, drawer menu, and player status bar
|
||||
// Overlay only the controls that have no practical desktop-UI touch equivalent.
|
||||
void append_ui_commands(std::vector<UIRenderCommand>& commands) const {
|
||||
if (!enabled_) return;
|
||||
if (!enabled_ || !gameplay_controls_visible()) return;
|
||||
|
||||
const float W = float(screen_w_);
|
||||
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
// --- 1. Top-Left Player Status Bar (Mobile HUD) ---
|
||||
const auto status = PythonBoot::GetPlayerStatusInfo();
|
||||
if (status.max_hp > 0) {
|
||||
const float bar_x = 20.0f;
|
||||
const float bar_y = 16.0f;
|
||||
const float bar_w = 205.0f;
|
||||
const float bar_h = 44.0f;
|
||||
|
||||
// Background plate
|
||||
draw_rect_bar(commands, bar_x, bar_y, bar_x + bar_w, bar_y + bar_h, 0x90101824);
|
||||
draw_rect_lines(commands, bar_x, bar_y, bar_x + bar_w, bar_y + bar_h, 0x60405870);
|
||||
|
||||
// Avatar circle badge
|
||||
const float av_cx = bar_x + 22.0f;
|
||||
const float av_cy = bar_y + 22.0f;
|
||||
const float av_r = 15.0f;
|
||||
draw_filled_disc(commands, av_cx, av_cy, av_r, 0xB0203040);
|
||||
draw_circle(commands, av_cx, av_cy, av_r, 0xE0FFD700, 16);
|
||||
// "Lv" tick inside avatar
|
||||
draw_line(commands, av_cx - 4.0f, av_cy - 4.0f, av_cx - 4.0f, av_cy + 3.0f, 0xFFFFFFFF);
|
||||
draw_line(commands, av_cx - 4.0f, av_cy + 3.0f, av_cx - 1.0f, av_cy + 3.0f, 0xFFFFFFFF);
|
||||
draw_line(commands, av_cx + 1.0f, av_cy - 4.0f, av_cx + 3.0f, av_cy + 3.0f, 0xFFFFFFFF);
|
||||
draw_line(commands, av_cx + 5.0f, av_cy - 4.0f, av_cx + 3.0f, av_cy + 3.0f, 0xFFFFFFFF);
|
||||
|
||||
// Gauges
|
||||
const float gx1 = bar_x + 44.0f;
|
||||
const float gx2 = bar_x + bar_w - 10.0f;
|
||||
const float gw = gx2 - gx1;
|
||||
|
||||
// HP Gauge
|
||||
const float hp_y1 = bar_y + 10.0f;
|
||||
const float hp_y2 = hp_y1 + 10.0f;
|
||||
const float hp_ratio = std::clamp(float(status.hp) / float(status.max_hp), 0.0f, 1.0f);
|
||||
draw_rect_bar(commands, gx1, hp_y1, gx2, hp_y2, 0x70301010);
|
||||
draw_rect_bar(commands, gx1, hp_y1, gx1 + gw * hp_ratio, hp_y2, 0xE5D82424);
|
||||
draw_line(commands, gx1, hp_y1, gx1 + gw * hp_ratio, hp_y1, 0x60FFFFFF); // Gloss line
|
||||
draw_rect_lines(commands, gx1, hp_y1, gx2, hp_y2, 0x80502020);
|
||||
|
||||
// MP Gauge
|
||||
const float mp_y1 = bar_y + 24.0f;
|
||||
const float mp_y2 = mp_y1 + 8.0f;
|
||||
const float mp_ratio = status.max_sp > 0 ? std::clamp(float(status.sp) / float(status.max_sp), 0.0f, 1.0f) : 0.0f;
|
||||
draw_rect_bar(commands, gx1, mp_y1, gx2, mp_y2, 0x70102038);
|
||||
draw_rect_bar(commands, gx1, mp_y1, gx1 + gw * mp_ratio, mp_y2, 0xE52068E0);
|
||||
draw_line(commands, gx1, mp_y1, gx1 + gw * mp_ratio, mp_y1, 0x60FFFFFF); // Gloss line
|
||||
draw_rect_lines(commands, gx1, mp_y1, gx2, mp_y2, 0x80204060);
|
||||
|
||||
// EXP bar (bottom trim line)
|
||||
if (status.max_exp > 0) {
|
||||
const float exp_ratio = std::clamp(float(status.exp) / float(status.max_exp), 0.0f, 1.0f);
|
||||
draw_rect_bar(commands, bar_x, bar_y + bar_h - 2.0f, bar_x + bar_w * exp_ratio, bar_y + bar_h, 0xD0E0B020);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- 2. Top-Right Drawer Menu Bar ---
|
||||
if (drawer_open_) {
|
||||
// Background capsule tray behind drawer buttons
|
||||
draw_rect_bar(commands, W - 468.0f, 16.0f, W - 198.0f, 54.0f, 0x90101824);
|
||||
draw_rect_lines(commands, W - 468.0f, 16.0f, W - 198.0f, 54.0f, 0x60506880);
|
||||
}
|
||||
|
||||
// --- 3. Buttons Rendering (Combat wheel & Drawer items) ---
|
||||
// Right-side attack and quick-slot buttons.
|
||||
for (const auto& btn : buttons_) {
|
||||
if (btn.id >= 10 && btn.id <= 15 && !drawer_open_)
|
||||
continue;
|
||||
|
||||
const uint32_t col = btn.pressed ? btn.color_pressed : btn.color_idle;
|
||||
draw_filled_disc(commands, btn.x, btn.y, btn.radius, col);
|
||||
draw_circle(commands, btn.x, btn.y, btn.radius * 0.85f, btn.pressed ? 0xFFFFFFFF : 0x70FFFFFF, 16);
|
||||
@@ -346,6 +289,24 @@ public:
|
||||
const uint32_t icon_col = btn.pressed ? 0xFFFFFFFF : 0xDDFFFFFF;
|
||||
const float r = btn.radius;
|
||||
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (btn.id >= 2) {
|
||||
const std::string skill_icon = PythonBoot::LocalQuickSlotSkillIcon(btn.id - 2);
|
||||
if (!skill_icon.empty()) {
|
||||
UIRenderCommand icon{};
|
||||
icon.kind = UIRenderCommand::Image;
|
||||
icon.x1 = btn.x - r * 0.68f;
|
||||
icon.y1 = btn.y - r * 0.68f;
|
||||
icon.x2 = btn.x + r * 0.68f;
|
||||
icon.y2 = btn.y + r * 0.68f;
|
||||
icon.argb = icon_col;
|
||||
icon.text = skill_icon;
|
||||
commands.push_back(std::move(icon));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
switch (btn.id) {
|
||||
case 1: { // ATK: crossed swords
|
||||
const float s = r * 0.35f;
|
||||
@@ -376,69 +337,30 @@ public:
|
||||
draw_line(commands, btn.x + 6.0f, btn.y - h, btn.x + 6.0f, btn.y + h, icon_col);
|
||||
break;
|
||||
}
|
||||
case 5: { // POT: +
|
||||
const float p = r * 0.4f;
|
||||
draw_line(commands, btn.x - p, btn.y, btn.x + p, btn.y, icon_col);
|
||||
draw_line(commands, btn.x, btn.y - p, btn.x, btn.y + p, icon_col);
|
||||
case 5: { // S4: IV
|
||||
const float h = r * 0.35f;
|
||||
draw_line(commands, btn.x - 7.0f, btn.y - h, btn.x - 7.0f, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x, btn.y - h, btn.x + 7.0f, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x + 14.0f, btn.y - h, btn.x + 7.0f, btn.y + h, icon_col);
|
||||
break;
|
||||
}
|
||||
case 6: { // PICK: Downward arrow
|
||||
const float a = r * 0.35f;
|
||||
draw_line(commands, btn.x - a, btn.y - a * 0.3f, btn.x, btn.y + a * 0.6f, icon_col);
|
||||
draw_line(commands, btn.x + a, btn.y - a * 0.3f, btn.x, btn.y + a * 0.6f, icon_col);
|
||||
draw_line(commands, btn.x, btn.y - a * 0.7f, btn.x, btn.y + a * 0.6f, icon_col);
|
||||
case 6: { // S5: V
|
||||
const float h = r * 0.35f;
|
||||
draw_line(commands, btn.x - 8.0f, btn.y - h, btn.x, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x + 8.0f, btn.y - h, btn.x, btn.y + h, icon_col);
|
||||
break;
|
||||
}
|
||||
case 99: { // MENU: ☰ hamburger icon
|
||||
draw_line(commands, btn.x - 7.0f, btn.y - 5.0f, btn.x + 7.0f, btn.y - 5.0f, icon_col);
|
||||
draw_line(commands, btn.x - 7.0f, btn.y, btn.x + 7.0f, btn.y, icon_col);
|
||||
draw_line(commands, btn.x - 7.0f, btn.y + 5.0f, btn.x + 7.0f, btn.y + 5.0f, icon_col);
|
||||
break;
|
||||
}
|
||||
case 10: { // BAG: Backpack
|
||||
draw_rect_lines(commands, btn.x - 6.0f, btn.y - 4.0f, btn.x + 6.0f, btn.y + 6.0f, icon_col);
|
||||
draw_line(commands, btn.x - 3.0f, btn.y - 4.0f, btn.x, btn.y - 7.0f, icon_col);
|
||||
draw_line(commands, btn.x, btn.y - 7.0f, btn.x + 3.0f, btn.y - 4.0f, icon_col);
|
||||
draw_line(commands, btn.x - 6.0f, btn.y, btn.x + 6.0f, btn.y, icon_col);
|
||||
break;
|
||||
}
|
||||
case 11: { // CHAR: Head + Shoulders
|
||||
draw_circle(commands, btn.x, btn.y - 3.0f, 4.0f, icon_col, 12);
|
||||
draw_line(commands, btn.x - 6.0f, btn.y + 6.0f, btn.x - 3.0f, btn.y + 2.0f, icon_col);
|
||||
draw_line(commands, btn.x - 3.0f, btn.y + 2.0f, btn.x + 3.0f, btn.y + 2.0f, icon_col);
|
||||
draw_line(commands, btn.x + 3.0f, btn.y + 2.0f, btn.x + 6.0f, btn.y + 6.0f, icon_col);
|
||||
break;
|
||||
}
|
||||
case 12: { // SKILL: Lightning
|
||||
draw_line(commands, btn.x + 2.0f, btn.y - 7.0f, btn.x - 3.0f, btn.y - 1.0f, icon_col);
|
||||
draw_line(commands, btn.x - 3.0f, btn.y - 1.0f, btn.x + 1.0f, btn.y - 1.0f, icon_col);
|
||||
draw_line(commands, btn.x + 1.0f, btn.y - 1.0f, btn.x - 2.0f, btn.y + 7.0f, icon_col);
|
||||
break;
|
||||
}
|
||||
case 13: { // QUEST: Scroll
|
||||
draw_rect_lines(commands, btn.x - 5.0f, btn.y - 6.0f, btn.x + 5.0f, btn.y + 6.0f, icon_col);
|
||||
draw_line(commands, btn.x - 3.0f, btn.y - 2.0f, btn.x + 3.0f, btn.y - 2.0f, icon_col);
|
||||
draw_line(commands, btn.x - 3.0f, btn.y + 2.0f, btn.x + 1.0f, btn.y + 2.0f, icon_col);
|
||||
break;
|
||||
}
|
||||
case 14: { // COMM: Chat bubble
|
||||
draw_rect_lines(commands, btn.x - 6.0f, btn.y - 5.0f, btn.x + 6.0f, btn.y + 3.0f, icon_col);
|
||||
draw_line(commands, btn.x - 3.0f, btn.y + 3.0f, btn.x - 5.0f, btn.y + 6.0f, icon_col);
|
||||
draw_line(commands, btn.x - 5.0f, btn.y + 6.0f, btn.x, btn.y + 3.0f, icon_col);
|
||||
break;
|
||||
}
|
||||
case 15: { // SET: Gear / Close
|
||||
draw_circle(commands, btn.x, btn.y, 4.0f, icon_col, 10);
|
||||
draw_line(commands, btn.x - 7.0f, btn.y, btn.x + 7.0f, btn.y, icon_col);
|
||||
draw_line(commands, btn.x, btn.y - 7.0f, btn.x, btn.y + 7.0f, icon_col);
|
||||
draw_line(commands, btn.x - 5.0f, btn.y - 5.0f, btn.x + 5.0f, btn.y + 5.0f, icon_col);
|
||||
draw_line(commands, btn.x - 5.0f, btn.y + 5.0f, btn.x + 5.0f, btn.y - 5.0f, icon_col);
|
||||
case 7: { // S6: VI
|
||||
const float h = r * 0.35f;
|
||||
draw_line(commands, btn.x - 11.0f, btn.y - h, btn.x - 3.0f, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x + 5.0f, btn.y - h, btn.x - 3.0f, btn.y + h, icon_col);
|
||||
draw_line(commands, btn.x + 11.0f, btn.y - h, btn.x + 11.0f, btn.y + h, icon_col);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. Virtual Joystick ---
|
||||
// Left-side movement joystick.
|
||||
draw_circle(commands, joystick_base_x_, joystick_base_y_, joystick_radius_, 0x8080C0FF, 24);
|
||||
draw_circle(commands, joystick_base_x_, joystick_base_y_, joystick_radius_ * 0.45f, 0x4080C0FF, 16);
|
||||
draw_line(commands, joystick_base_x_ - joystick_radius_, joystick_base_y_,
|
||||
@@ -494,26 +416,52 @@ public:
|
||||
}
|
||||
|
||||
private:
|
||||
bool gameplay_controls_visible() const {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
return !PythonBoot::CurrentMapName().empty();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void release_gameplay_controls() {
|
||||
if (joystick_active_) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::SetMoveDirection(0.0f, false);
|
||||
#endif
|
||||
}
|
||||
joystick_active_ = false;
|
||||
joystick_finger_id_ = -1;
|
||||
joystick_knob_x_ = joystick_base_x_;
|
||||
joystick_knob_y_ = joystick_base_y_;
|
||||
for (auto& btn : buttons_) {
|
||||
if (btn.pressed)
|
||||
trigger_button(btn.dik, false);
|
||||
btn.pressed = false;
|
||||
btn.finger_id = -1;
|
||||
}
|
||||
if (camera_dragged_) {
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
PythonBoot::CameraEndDrag();
|
||||
#endif
|
||||
}
|
||||
camera_finger_id_ = -1;
|
||||
camera_dragged_ = false;
|
||||
pinch_finger1_ = -1;
|
||||
pinch_finger2_ = -1;
|
||||
pinch_last_dist_ = 0.0f;
|
||||
}
|
||||
|
||||
void init_buttons() {
|
||||
buttons_.clear();
|
||||
// Combat Action Wheel (Lower-Right)
|
||||
// Lower-right combat controls. All menus and status UI remain the original 40250 UI.
|
||||
buttons_.push_back({1, 0x39, 0, 0, 42.0f, "ATK", 0x80D48820, 0xD0FFB040});
|
||||
buttons_.push_back({2, 0x02, 0, 0, 26.0f, "S1", 0x803060C0, 0xD05080FF});
|
||||
buttons_.push_back({3, 0x03, 0, 0, 26.0f, "S2", 0x80903090, 0xD0D050D0});
|
||||
buttons_.push_back({4, 0x04, 0, 0, 26.0f, "S3", 0x80309060, 0xD050D080});
|
||||
buttons_.push_back({5, 0x05, 0, 0, 22.0f, "POT", 0x90A03030, 0xD0FF5050});
|
||||
buttons_.push_back({6, 0x2c, 0, 0, 22.0f, "PICK", 0x80208080, 0xD040B0B0});
|
||||
|
||||
// Top-Right Drawer Menu Toggle
|
||||
buttons_.push_back({99, 0, 0, 0, 20.0f, "MENU", 0x90283848, 0xD0FFB040});
|
||||
|
||||
// Drawer Menu Buttons
|
||||
buttons_.push_back({10, 0x17, 0, 0, 18.0f, "BAG", 0x90D09020, 0xD0FFB040}); // DIK_I
|
||||
buttons_.push_back({11, 0x2e, 0, 0, 18.0f, "CHAR", 0x903060B0, 0xD05080FF}); // DIK_C
|
||||
buttons_.push_back({12, 0x2f, 0, 0, 18.0f, "SKILL", 0x90803090, 0xD0D050D0}); // DIK_V
|
||||
buttons_.push_back({13, 0x31, 0, 0, 18.0f, "QUEST", 0x90308050, 0xD050D070}); // DIK_N
|
||||
buttons_.push_back({14, 0x32, 0, 0, 18.0f, "COMM", 0x90905020, 0xD0E07030}); // DIK_M
|
||||
buttons_.push_back({15, 0x01, 0, 0, 18.0f, "SET", 0x90506070, 0xD08090A0}); // DIK_ESCAPE
|
||||
buttons_.push_back({5, 0x05, 0, 0, 26.0f, "S4", 0x80906030, 0xD0D08050});
|
||||
buttons_.push_back({6, 0x06, 0, 0, 26.0f, "S5", 0x80603090, 0xD08050D0});
|
||||
buttons_.push_back({7, 0x07, 0, 0, 26.0f, "S6", 0x80308090, 0xD050C0D0});
|
||||
}
|
||||
|
||||
void layout_buttons() {
|
||||
@@ -522,70 +470,40 @@ private:
|
||||
for (auto& btn : buttons_) {
|
||||
switch (btn.id) {
|
||||
case 1:
|
||||
btn.x = W - 85.0f;
|
||||
btn.x = W - 75.0f;
|
||||
btn.y = H - 85.0f;
|
||||
btn.radius = std::min(46.0f, H * 0.12f);
|
||||
break;
|
||||
case 2:
|
||||
btn.x = W - 360.0f;
|
||||
btn.y = H - 75.0f;
|
||||
btn.radius = std::min(28.0f, H * 0.08f);
|
||||
break;
|
||||
case 3:
|
||||
btn.x = W - 295.0f;
|
||||
btn.y = H - 75.0f;
|
||||
btn.radius = std::min(28.0f, H * 0.08f);
|
||||
break;
|
||||
case 4:
|
||||
btn.x = W - 230.0f;
|
||||
btn.y = H - 75.0f;
|
||||
btn.radius = std::min(28.0f, H * 0.08f);
|
||||
break;
|
||||
case 5:
|
||||
btn.x = W - 165.0f;
|
||||
btn.y = H - 85.0f;
|
||||
btn.radius = std::min(28.0f, H * 0.08f);
|
||||
break;
|
||||
case 3:
|
||||
btn.x = W - 145.0f;
|
||||
case 6:
|
||||
btn.x = W - 125.0f;
|
||||
btn.y = H - 155.0f;
|
||||
btn.radius = std::min(28.0f, H * 0.08f);
|
||||
break;
|
||||
case 4:
|
||||
btn.x = W - 85.0f;
|
||||
btn.y = H - 175.0f;
|
||||
case 7:
|
||||
btn.x = W - 75.0f;
|
||||
btn.y = H - 225.0f;
|
||||
btn.radius = std::min(28.0f, H * 0.08f);
|
||||
break;
|
||||
case 5:
|
||||
btn.x = W - 225.0f;
|
||||
btn.y = H - 75.0f;
|
||||
btn.radius = std::min(24.0f, H * 0.065f);
|
||||
break;
|
||||
case 6:
|
||||
btn.x = W - 85.0f;
|
||||
btn.y = H - 235.0f;
|
||||
btn.radius = std::min(24.0f, H * 0.065f);
|
||||
break;
|
||||
case 99: // MENU toggle button (to the left of MiniMap)
|
||||
btn.x = W - 170.0f;
|
||||
btn.y = 35.0f;
|
||||
btn.radius = 20.0f;
|
||||
break;
|
||||
case 10: // BAG
|
||||
btn.x = W - 220.0f;
|
||||
btn.y = 35.0f;
|
||||
btn.radius = 18.0f;
|
||||
break;
|
||||
case 11: // CHAR
|
||||
btn.x = W - 265.0f;
|
||||
btn.y = 35.0f;
|
||||
btn.radius = 18.0f;
|
||||
break;
|
||||
case 12: // SKILL
|
||||
btn.x = W - 310.0f;
|
||||
btn.y = 35.0f;
|
||||
btn.radius = 18.0f;
|
||||
break;
|
||||
case 13: // QUEST
|
||||
btn.x = W - 355.0f;
|
||||
btn.y = 35.0f;
|
||||
btn.radius = 18.0f;
|
||||
break;
|
||||
case 14: // COMM
|
||||
btn.x = W - 400.0f;
|
||||
btn.y = 35.0f;
|
||||
btn.radius = 18.0f;
|
||||
break;
|
||||
case 15: // SET
|
||||
btn.x = W - 445.0f;
|
||||
btn.y = 35.0f;
|
||||
btn.radius = 18.0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -593,8 +511,6 @@ private:
|
||||
int find_button(float x, float y) {
|
||||
for (size_t i = 0; i < buttons_.size(); ++i) {
|
||||
const auto& btn = buttons_[i];
|
||||
if (btn.id >= 10 && btn.id <= 15 && !drawer_open_)
|
||||
continue;
|
||||
const float dist = std::hypot(x - btn.x, y - btn.y);
|
||||
if (dist <= btn.radius * 1.25f) {
|
||||
return int(i);
|
||||
@@ -608,6 +524,11 @@ private:
|
||||
if (dik == 0x39) {
|
||||
PythonBoot::SetAttackKey(pressed);
|
||||
}
|
||||
if (dik >= 0x02 && dik <= 0x07) {
|
||||
if (pressed)
|
||||
PythonBoot::UseLocalQuickSlot(dik - 0x02);
|
||||
return;
|
||||
}
|
||||
if (dik != 0) {
|
||||
PythonBoot::UIKey(dik, pressed);
|
||||
}
|
||||
@@ -703,7 +624,7 @@ private:
|
||||
int screen_w_ = 1280;
|
||||
int screen_h_ = 720;
|
||||
|
||||
bool drawer_open_ = false;
|
||||
bool controls_were_visible_ = false;
|
||||
int64_t ui_touch_finger_id_ = -1;
|
||||
|
||||
bool joystick_active_ = false;
|
||||
|
||||
Reference in New Issue
Block a user