feat: complete native client rendering parity updates

This commit is contained in:
shen
2026-09-27 19:44:41 -07:00
parent 9a9dc6d224
commit 25dd65f2ce
99 changed files with 21121 additions and 921 deletions
+84
View File
@@ -0,0 +1,84 @@
find_package(Vulkan REQUIRED)
find_package(SDL3 CONFIG REQUIRED)
find_program(GLSLC glslc REQUIRED)
set(MT_NATIVE_VERT_SPV "${CMAKE_CURRENT_BINARY_DIR}/native.vert.spv")
set(MT_NATIVE_FRAG_SPV "${CMAKE_CURRENT_BINARY_DIR}/native.frag.spv")
add_custom_command(OUTPUT "${MT_NATIVE_VERT_SPV}"
COMMAND "${GLSLC}" -fshader-stage=vert "${CMAKE_CURRENT_SOURCE_DIR}/native.vert" -o "${MT_NATIVE_VERT_SPV}"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/native.vert")
add_custom_command(OUTPUT "${MT_NATIVE_FRAG_SPV}"
COMMAND "${GLSLC}" -fshader-stage=frag "${CMAKE_CURRENT_SOURCE_DIR}/native.frag" -o "${MT_NATIVE_FRAG_SPV}"
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/native.frag")
add_custom_target(mt_native_shaders DEPENDS "${MT_NATIVE_VERT_SPV}" "${MT_NATIVE_FRAG_SPV}")
set(MT_NATIVE_RENDER_SOURCES
main.cpp
stb_image_impl.cpp
"${PROJECT_SOURCE_DIR}/extension/src/dxt.cpp"
)
if(TARGET port_platform AND TARGET mtpython)
list(APPEND MT_NATIVE_RENDER_SOURCES
"${PROJECT_SOURCE_DIR}/extension/tests/port_login_flow_server.cpp"
"${PROJECT_SOURCE_DIR}/extension/src/net/classic/classic_cipher.cpp"
)
endif()
if(ANDROID)
# SDLActivity loads libmain.so; Android does not launch a native executable.
set(MT_NATIVE_TARGET main)
add_library(${MT_NATIVE_TARGET} SHARED ${MT_NATIVE_RENDER_SOURCES})
else()
set(MT_NATIVE_TARGET mt_native_render)
add_executable(${MT_NATIVE_TARGET} ${MT_NATIVE_RENDER_SOURCES})
endif()
if(APPLE)
set_target_properties(${MT_NATIVE_TARGET} PROPERTIES
OSX_ARCHITECTURES "${CMAKE_HOST_SYSTEM_PROCESSOR}"
)
endif()
add_dependencies(${MT_NATIVE_TARGET} mt_native_shaders)
target_include_directories(${MT_NATIVE_TARGET} PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/third_party"
"${PROJECT_SOURCE_DIR}/extension/src"
"${PROJECT_SOURCE_DIR}/extension/src/platform/EterLib")
if(NOT ANDROID)
add_custom_command(TARGET ${MT_NATIVE_TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${MT_NATIVE_VERT_SPV}" "$<TARGET_FILE_DIR:${MT_NATIVE_TARGET}>/native.vert.spv"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${MT_NATIVE_FRAG_SPV}" "$<TARGET_FILE_DIR:${MT_NATIVE_TARGET}>/native.frag.spv")
endif()
target_link_libraries(${MT_NATIVE_TARGET} PRIVATE Vulkan::Vulkan SDL3::SDL3)
if(APPLE)
target_link_libraries(${MT_NATIVE_TARGET} PRIVATE
"-framework AudioToolbox")
endif()
if(TARGET port_platform AND TARGET mtpython)
target_compile_definitions(${MT_NATIVE_TARGET} PRIVATE MT_NATIVE_HAS_LIVE_CLIENT=1)
target_link_libraries(${MT_NATIVE_TARGET} PRIVATE port_platform)
if(TARGET mtpython_stdlib)
add_dependencies(${MT_NATIVE_TARGET} mtpython_stdlib)
if(NOT ANDROID)
add_custom_command(TARGET ${MT_NATIVE_TARGET} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${MT_PYTHON_STDLIB_ZIP}" "$<TARGET_FILE_DIR:${MT_NATIVE_TARGET}>/python27.zip")
endif()
endif()
endif()
if(ANDROID)
# Add this directory to the SDL Android app's assets.srcDirs.
set(MT_NATIVE_ANDROID_ASSETS "${CMAKE_CURRENT_BINARY_DIR}/android-assets")
add_custom_target(mt_native_android_assets ALL
COMMAND ${CMAKE_COMMAND} -E make_directory "${MT_NATIVE_ANDROID_ASSETS}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${MT_NATIVE_VERT_SPV}" "${MT_NATIVE_ANDROID_ASSETS}/native.vert.spv"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${MT_NATIVE_FRAG_SPV}" "${MT_NATIVE_ANDROID_ASSETS}/native.frag.spv"
DEPENDS mt_native_shaders)
if(TARGET mtpython_stdlib)
add_dependencies(mt_native_android_assets mtpython_stdlib)
add_custom_command(TARGET mt_native_android_assets POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${MT_PYTHON_STDLIB_ZIP}" "${MT_NATIVE_ANDROID_ASSETS}/python27.zip")
endif()
endif()
+153
View File
@@ -0,0 +1,153 @@
# Native Vulkan renderer prototype
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.
## Build and run on macOS
Install Vulkan headers/loader, MoltenVK, SDL3 and `glslc` (shaderc). With Homebrew:
```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
cmake --build build-release --target mt_native_render -j8
```
On macOS, `mt_native_render` automatically detects `/opt/homebrew/etc/vulkan/icd.d/MoltenVK_icd.json` (or `/usr/local/etc/vulkan/icd.d/MoltenVK_icd.json`) when `VK_ICD_FILENAMES` is not set in the environment.
The build copies `native.vert.spv`, `native.frag.spv`, and, for the live client,
`python27.zip` beside the executable. Keep these files together when moving the
binary. In a macOS `.app`, put them in `Contents/Resources` (the directory
returned by SDL's `SDL_GetBasePath`). `MT_PYTHON_STDLIB` can override the zip path.
## Android integration
With an Android toolchain and SDL3 Android AAR/Prefab available to CMake, the
same option builds `libmain.so` for SDLActivity. The `mt_native_android_assets`
target prepares `native.vert.spv`, `native.frag.spv`, and `python27.zip` under
`<build>/native_render/android-assets` (`python27.zip` is included when the
embedded Python target is enabled); include that directory in the SDL app's
`assets.srcDirs`. The app must allow network access for login. The 40250 `Client`
directory must be placed at `SDL_GetPrefPath("mtgodot", "native-render")/Client`,
with `pack/Index` present, before live mode starts. The Godot APK is a separate
application and does not launch this SDL renderer.
The standalone `arm64-v8a` renderer cross-builds at Android API 24. The full
live-client cross-build currently stops in `extension/src/port/common/Win32Crt.cpp`:
the NDK exposes `<iconv.h>` at API 24 but does not declare `iconv` until API 28.
This is a port-runtime prerequisite for an Android live-client APK; raising the
minimum API level is not assumed here.
The Vulkan portability enumeration extension is selected only when advertised
by the loader. DDS, TGA, and memory textures retain their existing decoders;
JPEG, PNG, and BMP use the same portable decoder on macOS and Android. The
vendored `stb_image.h` is upstream v2.30 (SHA-256
`594c2fe35d49488b4382dbfaec8f98366defca819d916ac95becf3e75f4200b3`).
### Interactive Playable Modes
Run the full 40250 client interactively (infinite frame loop until window close, resizable SDL3 window with automatic Vulkan swapchain recreation and `PythonBoot::SetUISize` sync, full keyboard/IME text input, SDL hardware cursor built from the original cursor images, and SDL3 + `AudioToolbox` `.wav`/`.mp3` audio):
```sh
# Interactive outdoor map session (auto-login via loopback FakeLoginServer)
./build-release/native_render/mt_native_render \
--live-client "/path/to/40250/Server Client TMP4/Client" \
--interactive --fake-mobs 24 --width 1280 --height 800
# Interactive login screen (stops at introLogin.LoginWindow for manual typing/login)
./build-release/native_render/mt_native_render \
--live-client "/path/to/40250/Server Client TMP4/Client" \
--login-screen --width 1024 --height 768
# Connect to an external 40250 Auth + Game server
./build-release/native_render/mt_native_render \
--live-client "/path/to/40250/Server Client TMP4/Client" \
--live-server 127.0.0.1:11002:13000 --login-screen
```
### Synthetic benchmark
```sh
./build-release/native_render/mt_native_render --frames 60 --draws 64 --triangles-per-draw 333 --no-vsync
```
Timing metrics printed by `mt_native_render`:
- `p95_frame_ms`, `p99_frame_ms`, `max_frame_ms`: wall-clock time for each measured update/render iteration, excluding startup and final GPU drain. These include vsync wait when enabled; use them alongside `gpu_ms` and the CPU breakdown.
- `game_update_ms`: mean CPU time spent in `PythonBoot::UIUpdate()`, `PythonBoot::UIRender()`, and audio command draining per frame in `--live-client` mode.
- `prepare_ms`: mean CPU draw-preparation time across all frames (including frame 0 cold-start geometry/texture uploads and pipeline creation).
- `steady_prepare_ms`: mean CPU draw-preparation time on frames after frame 0 (bone palette copy, UI quad batching, command recording).
- `submit_ms`: host CPU time around `vkQueueSubmit` (in `FIFO` mode this includes swapchain backpressure; pass `--no-vsync` to switch to `IMMEDIATE`/`MAILBOX`).
- `gpu_ms`: true hardware GPU execution time between top-of-pipe `vkCmdBeginRenderPass` and bottom-of-pipe `vkCmdEndRenderPass` measured via `VK_QUERY_TYPE_TIMESTAMP`.
### macOS real-server acceptance
Build the Release live-client target above, then start the evidence runner from the repository root:
```sh
# First verify the runner and renderer using the local fake server.
node script/native_mac_acceptance.mjs --fake --frames 180
# Use the real server's shared host, auth port, and game channel port.
node script/native_mac_acceptance.mjs --server HOST:AUTH_PORT:GAME_PORT
```
Real-server mode checks both ports before starting, opens the native login screen,
and collects a redacted client log, one-second process RSS samples, and a JSON
report under `build/native-acceptance/`. Enter credentials in the app, then
exercise login, character selection, movement, combat, map changes, inventory,
chat, window resize/focus, and visual comparison with the current client. Close
the window after at least 30 minutes. The report records whether that minimum
was met, but remains `NEEDS_MANUAL_REVIEW` until those actions and visual results
are checked by a person. It does not assert that RSS alone proves no GPU leak.
`--live-server` currently accepts one shared host for the auth and game ports.
If those endpoints use different hosts, update the native connection setup
before claiming a real-server pass. Credentials are entered in the client UI;
the runner never puts them in arguments or the report.
## Run the real 40250 client benchmark in native Vulkan (`--live-client`)
When built with `port_platform`, `mt_native_render` boots `system.py`, logs in via loopback `FakeLoginServer`, enters the outdoor map with 1..64 monsters, renders the complete 3D scene (40250 hardware-transform terrain splats, animated water patches, gradient skybox & scrolling clouds, SpeedTree forest bark/leaf geometry, GPU-skinned characters with stage-1 specular sphere-maps, and 2D UI/minimap/text-tails/software cursor), and optionally writes a Version 5 `.mtdr` capture:
```sh
./build-release/native_render/mt_native_render \
--live-client "/path/to/40250/Server Client TMP4/Client" \
--fake-mobs 64 --frames 180 --gpu-skinning --no-vsync \
--capture-out /tmp/mt_full_64mobs.mtdr
```
Compare against CPU skinning (`GrannyDeformVertices` on CPU + per-frame vertex buffer re-uploads) with `--no-gpu-skinning`:
```sh
./build-release/native_render/mt_native_render \
--live-client "/path/to/40250/Server Client TMP4/Client" \
--fake-mobs 64 --frames 180 --no-gpu-skinning --no-vsync
```
## Replay a captured frame (`.mtdr` v1 / v2 / v3 / v4 / v5)
```sh
./build-release/native_render/mt_native_render \
--capture /tmp/mt_full_64mobs.mtdr --frames 180 --animate-bones --no-vsync
```
- Pass `--animate-bones` to animate the bone palette SSBO each frame without re-uploading any vertex buffers (`uploads` stays equal to unique static geometries uploaded on frame 0).
- Pass `--animate-first-draw` to increment the first draw's `geometry_revision` each frame after frame 0 and verify incremental GPU buffer re-uploads.
Capture format Version 5 (backward-compatible with Versions 1–4) stores:
1. 3D draws (`Render3DDraw`): `geometry_key`, `geometry_revision`, matrices, positions, normals, UVs, diffuse colors, indices, D3D8 fixed-function states, `texture0` / `texture1` names, and GPU skinning data (`bone_indices`, `bone_weights`, `bone_matrices`).
2. Self-contained textures: `.dds`, `.tga`, `.jpg`, `.png`, and `.bmp` pack bytes plus `"MTRA"` raw RGBA memory textures (`mem:<id>@<revision>` font glyph pages).
3. 2D UI stream (`UIRenderCommand`): canvas size (`ui_width`, `ui_height`) and all `Bar`, `GradientBar`, `Line`, and `Image` commands (including `behind_3d`, clip rects, minimap mask UV coordinates, and software mouse cursor quads).
4. Per-draw fog color, vertex/table mode, range flag, start/end distance and density. Older captures omit these fields and replay without fog.
The native shader now evaluates recorded D3D8 stage 0/1 color and alpha operations, including the original cloud operation (`D3DTOP_MODULATEINVALPHA_ADDCOLOR = 20`), and applies the captured linear or exponential fog. Expanded UI image modes use the 40250 blend factors for screen/color-dodge and modulate. These state fixes do not by themselves establish pixel parity with a Windows 40250 screenshot; compare the same map, time, camera and UI state before treating a color difference as resolved.
+378
View File
@@ -0,0 +1,378 @@
#pragma once
#include "../extension/src/platform/EterLib/RenderCommands3D.h"
#include "../extension/src/platform/EterLib/UIRenderCommands.h"
#include <cstdint>
#include <cstring>
#include <fstream>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
// Diagnostic, little-endian arm64 format:
// - Version 1: matrices, positions, diffuse, indices.
// - Version 2: adds stable geometry_key and geometry_revision.
// - Version 3: adds normals, UVs, D3D8 fixed-function states, and embedded pack texture bytes.
// - Version 4: adds GPU skeletal skinning streams (bone_indices, bone_weights, bone_matrices)
// and 2D UIRenderCommand stream + memory textures ("MTRA").
// - Version 5: adds per-draw fog color, mode, range and distance/density state.
// - Version 6: adds texture address and filtering state for both stages.
// - Version 7: adds two fixed-function lights and material color sources.
// Capture is opt-in and never runs in the normal game path.
namespace native_draw_capture {
struct Capture {
std::uint32_t version = 7;
std::vector<Render3DDraw> draws;
std::unordered_map<std::string, std::vector<std::uint8_t>> textures;
std::uint32_t ui_width = 960;
std::uint32_t ui_height = 640;
std::vector<UIRenderCommand> ui_commands;
};
inline std::vector<std::uint8_t> encode_raw_argb_as_mtra(
std::uint32_t width,
std::uint32_t height,
const std::uint32_t* argb) {
std::vector<std::uint8_t> out;
if (!width || !height || !argb)
return out;
const std::size_t pixel_count = std::size_t(width) * std::size_t(height);
out.resize(12 + pixel_count * 4);
out[0] = 'M'; out[1] = 'T'; out[2] = 'R'; out[3] = 'A';
std::memcpy(out.data() + 4, &width, 4);
std::memcpy(out.data() + 8, &height, 4);
std::uint8_t* dst = out.data() + 12;
for (std::size_t i = 0; i < pixel_count; ++i) {
const std::uint32_t c = argb[i];
dst[i * 4 + 0] = static_cast<std::uint8_t>((c >> 16) & 0xffu);
dst[i * 4 + 1] = static_cast<std::uint8_t>((c >> 8) & 0xffu);
dst[i * 4 + 2] = static_cast<std::uint8_t>(c & 0xffu);
dst[i * 4 + 3] = static_cast<std::uint8_t>((c >> 24) & 0xffu);
}
return out;
}
template <typename T> void write_scalar(std::ofstream& file, const T& value) {
file.write(reinterpret_cast<const char*>(&value), sizeof(value));
}
template <typename T> void read_scalar(std::ifstream& file, T& value) {
file.read(reinterpret_cast<char*>(&value), sizeof(value));
if (!file) throw std::runtime_error("truncated native draw capture");
}
template <typename T>
void write_vector(std::ofstream& file, const std::vector<T>& values, std::size_t max_count = 4'000'000) {
if (values.size() > max_count) throw std::runtime_error("native draw capture array too large");
const auto count = static_cast<std::uint32_t>(values.size());
write_scalar(file, count);
if (count) file.write(reinterpret_cast<const char*>(values.data()), count * sizeof(T));
}
template <typename T>
void read_vector(std::ifstream& file, std::vector<T>& values, std::size_t max_count = 4'000'000) {
std::uint32_t count = 0;
read_scalar(file, count);
if (count > max_count) throw std::runtime_error("native draw capture array too large");
values.resize(count);
if (count) file.read(reinterpret_cast<char*>(values.data()), count * sizeof(T));
if (!file) throw std::runtime_error("truncated native draw capture array");
}
inline void write_string(std::ofstream& file, const std::string& value) {
if (value.size() > 4096) throw std::runtime_error("native draw capture string too large");
const auto size = static_cast<std::uint32_t>(value.size());
write_scalar(file, size);
if (size) file.write(value.data(), size);
}
inline void read_string(std::ifstream& file, std::string& value) {
std::uint32_t size = 0;
read_scalar(file, size);
if (size > 4096) throw std::runtime_error("native draw capture string too large");
value.resize(size);
if (size) file.read(value.data(), size);
if (!file) throw std::runtime_error("truncated native draw capture string");
}
inline void write(
const std::string& path,
const std::vector<Render3DDraw>& draws,
const std::unordered_map<std::string, std::vector<std::uint8_t>>& textures = {},
std::uint32_t ui_width = 960,
std::uint32_t ui_height = 640,
const std::vector<UIRenderCommand>& ui_commands = {}) {
if (draws.size() > 10'000 || textures.size() > 8'192 || ui_commands.size() > 100'000)
throw std::runtime_error("native draw capture has too many draws, textures, or UI commands");
std::ofstream file(path, std::ios::binary | std::ios::trunc);
if (!file) throw std::runtime_error("cannot create native draw capture: " + path);
const std::uint32_t magic = 0x4d544452; // MTDR
const std::uint32_t version = 7;
const auto count = static_cast<std::uint32_t>(draws.size());
write_scalar(file, magic); write_scalar(file, version); write_scalar(file, count);
for (const auto& draw : draws) {
file.write(reinterpret_cast<const char*>(draw.world), sizeof(draw.world));
file.write(reinterpret_cast<const char*>(draw.view), sizeof(draw.view));
file.write(reinterpret_cast<const char*>(draw.proj), sizeof(draw.proj));
write_scalar(file, draw.geometry_key);
write_scalar(file, draw.geometry_revision);
const std::uint32_t flags =
(draw.lines ? 1u : 0u) | (draw.pretransformed ? 2u : 0u) | (draw.light0 ? 4u : 0u);
write_scalar(file, flags);
write_vector(file, draw.positions);
write_vector(file, draw.diffuse);
write_vector(file, draw.indices);
file.write(reinterpret_cast<const char*>(draw.viewport), sizeof(draw.viewport));
write_string(file, draw.texture0);
write_string(file, draw.texture1);
write_vector(file, draw.rhw);
write_vector(file, draw.normals);
write_vector(file, draw.uv0);
write_vector(file, draw.uv1);
write_scalar(file, draw.alpha_blend);
write_scalar(file, draw.src_blend);
write_scalar(file, draw.dest_blend);
write_scalar(file, draw.alpha_test);
write_scalar(file, draw.alpha_ref);
write_scalar(file, draw.alpha_func);
write_scalar(file, draw.cull_mode);
write_scalar(file, draw.z_enable);
write_scalar(file, draw.z_write);
write_scalar(file, draw.z_func);
write_scalar(file, draw.lighting);
write_scalar(file, draw.texture_factor);
write_scalar(file, draw.fog_enable);
file.write(reinterpret_cast<const char*>(draw.color_op), sizeof(draw.color_op));
file.write(reinterpret_cast<const char*>(draw.color_arg1), sizeof(draw.color_arg1));
file.write(reinterpret_cast<const char*>(draw.color_arg2), sizeof(draw.color_arg2));
file.write(reinterpret_cast<const char*>(draw.alpha_op), sizeof(draw.alpha_op));
file.write(reinterpret_cast<const char*>(draw.alpha_arg1), sizeof(draw.alpha_arg1));
file.write(reinterpret_cast<const char*>(draw.alpha_arg2), sizeof(draw.alpha_arg2));
file.write(reinterpret_cast<const char*>(draw.material_diffuse), sizeof(draw.material_diffuse));
file.write(reinterpret_cast<const char*>(draw.material_ambient), sizeof(draw.material_ambient));
file.write(reinterpret_cast<const char*>(draw.material_emissive), sizeof(draw.material_emissive));
file.write(reinterpret_cast<const char*>(draw.light0_direction), sizeof(draw.light0_direction));
file.write(reinterpret_cast<const char*>(draw.light0_diffuse), sizeof(draw.light0_diffuse));
file.write(reinterpret_cast<const char*>(draw.light0_ambient), sizeof(draw.light0_ambient));
write_scalar(file, draw.ambient);
write_scalar(file, draw.fog_color);
write_scalar(file, draw.fog_vertex_mode);
write_scalar(file, draw.fog_table_mode);
write_scalar(file, draw.fog_range_enable);
write_scalar(file, draw.fog_start);
write_scalar(file, draw.fog_end);
write_scalar(file, draw.fog_density);
file.write(reinterpret_cast<const char*>(draw.address_u), sizeof(draw.address_u));
file.write(reinterpret_cast<const char*>(draw.address_v), sizeof(draw.address_v));
file.write(reinterpret_cast<const char*>(draw.min_filter), sizeof(draw.min_filter));
file.write(reinterpret_cast<const char*>(draw.mag_filter), sizeof(draw.mag_filter));
file.write(reinterpret_cast<const char*>(draw.mip_filter), sizeof(draw.mip_filter));
file.write(reinterpret_cast<const char*>(draw.lights), sizeof(draw.lights));
write_scalar(file, draw.diffuse_material_source);
write_scalar(file, draw.ambient_material_source);
write_scalar(file, draw.color_vertex);
write_vector(file, draw.bone_indices);
write_vector(file, draw.bone_weights);
write_vector(file, draw.bone_matrices);
}
const auto texture_count = static_cast<std::uint32_t>(textures.size());
write_scalar(file, texture_count);
for (const auto& [name, bytes] : textures) {
write_string(file, name);
write_vector(file, bytes, 16'777'216);
}
write_scalar(file, ui_width);
write_scalar(file, ui_height);
const auto ui_count = static_cast<std::uint32_t>(ui_commands.size());
write_scalar(file, ui_count);
for (const auto& cmd : ui_commands) {
const auto kind = static_cast<std::uint32_t>(cmd.kind);
write_scalar(file, kind);
write_scalar(file, cmd.x1);
write_scalar(file, cmd.y1);
write_scalar(file, cmd.x2);
write_scalar(file, cmd.y2);
write_scalar(file, cmd.argb);
write_scalar(file, cmd.end_argb);
write_scalar(file, cmd.clip_x1);
write_scalar(file, cmd.clip_y1);
write_scalar(file, cmd.clip_x2);
write_scalar(file, cmd.clip_y2);
write_string(file, cmd.text);
const std::uint32_t uiflags = (cmd.quad ? 1u : 0u) | (cmd.behind_3d ? 2u : 0u);
write_scalar(file, uiflags);
file.write(reinterpret_cast<const char*>(cmd.qx), sizeof(cmd.qx));
file.write(reinterpret_cast<const char*>(cmd.qy), sizeof(cmd.qy));
write_scalar(file, cmd.su);
write_scalar(file, cmd.sv);
write_scalar(file, cmd.eu);
write_scalar(file, cmd.ev);
write_scalar(file, cmd.blend);
write_string(file, cmd.mask);
file.write(reinterpret_cast<const char*>(cmd.mu), sizeof(cmd.mu));
file.write(reinterpret_cast<const char*>(cmd.mv), sizeof(cmd.mv));
}
if (!file) throw std::runtime_error("failed to write native draw capture: " + path);
}
inline Capture read_capture(const std::string& path) {
std::ifstream file(path, std::ios::binary);
if (!file) throw std::runtime_error("cannot open native draw capture: " + path);
std::uint32_t magic = 0, version = 0, count = 0;
read_scalar(file, magic); read_scalar(file, version); read_scalar(file, count);
if (magic != 0x4d544452 || (version < 1 || version > 7) || count > 10'000)
throw std::runtime_error("unsupported native draw capture format");
Capture capture;
capture.version = version;
capture.draws.resize(count);
for (auto& draw : capture.draws) {
file.read(reinterpret_cast<char*>(draw.world), sizeof(draw.world));
file.read(reinterpret_cast<char*>(draw.view), sizeof(draw.view));
file.read(reinterpret_cast<char*>(draw.proj), sizeof(draw.proj));
if (!file) throw std::runtime_error("truncated native draw capture matrices");
if (version >= 2) {
read_scalar(file, draw.geometry_key);
read_scalar(file, draw.geometry_revision);
}
std::uint32_t flags = 0;
read_scalar(file, flags);
draw.lines = (flags & 1u) != 0;
draw.pretransformed = (flags & 2u) != 0;
draw.light0 = (flags & 4u) != 0;
read_vector(file, draw.positions);
read_vector(file, draw.diffuse);
read_vector(file, draw.indices);
if (version >= 3) {
file.read(reinterpret_cast<char*>(draw.viewport), sizeof(draw.viewport));
read_string(file, draw.texture0);
read_string(file, draw.texture1);
read_vector(file, draw.rhw);
read_vector(file, draw.normals);
read_vector(file, draw.uv0);
read_vector(file, draw.uv1);
read_scalar(file, draw.alpha_blend);
read_scalar(file, draw.src_blend);
read_scalar(file, draw.dest_blend);
read_scalar(file, draw.alpha_test);
read_scalar(file, draw.alpha_ref);
read_scalar(file, draw.alpha_func);
read_scalar(file, draw.cull_mode);
read_scalar(file, draw.z_enable);
read_scalar(file, draw.z_write);
read_scalar(file, draw.z_func);
read_scalar(file, draw.lighting);
read_scalar(file, draw.texture_factor);
read_scalar(file, draw.fog_enable);
file.read(reinterpret_cast<char*>(draw.color_op), sizeof(draw.color_op));
file.read(reinterpret_cast<char*>(draw.color_arg1), sizeof(draw.color_arg1));
file.read(reinterpret_cast<char*>(draw.color_arg2), sizeof(draw.color_arg2));
file.read(reinterpret_cast<char*>(draw.alpha_op), sizeof(draw.alpha_op));
file.read(reinterpret_cast<char*>(draw.alpha_arg1), sizeof(draw.alpha_arg1));
file.read(reinterpret_cast<char*>(draw.alpha_arg2), sizeof(draw.alpha_arg2));
file.read(reinterpret_cast<char*>(draw.material_diffuse), sizeof(draw.material_diffuse));
file.read(reinterpret_cast<char*>(draw.material_ambient), sizeof(draw.material_ambient));
file.read(reinterpret_cast<char*>(draw.material_emissive), sizeof(draw.material_emissive));
file.read(reinterpret_cast<char*>(draw.light0_direction), sizeof(draw.light0_direction));
file.read(reinterpret_cast<char*>(draw.light0_diffuse), sizeof(draw.light0_diffuse));
file.read(reinterpret_cast<char*>(draw.light0_ambient), sizeof(draw.light0_ambient));
read_scalar(file, draw.ambient);
if (version >= 5) {
read_scalar(file, draw.fog_color);
read_scalar(file, draw.fog_vertex_mode);
read_scalar(file, draw.fog_table_mode);
read_scalar(file, draw.fog_range_enable);
read_scalar(file, draw.fog_start);
read_scalar(file, draw.fog_end);
read_scalar(file, draw.fog_density);
}
if (version >= 6) {
file.read(reinterpret_cast<char*>(draw.address_u), sizeof(draw.address_u));
file.read(reinterpret_cast<char*>(draw.address_v), sizeof(draw.address_v));
file.read(reinterpret_cast<char*>(draw.min_filter), sizeof(draw.min_filter));
file.read(reinterpret_cast<char*>(draw.mag_filter), sizeof(draw.mag_filter));
file.read(reinterpret_cast<char*>(draw.mip_filter), sizeof(draw.mip_filter));
}
if (version >= 7) {
file.read(reinterpret_cast<char*>(draw.lights), sizeof(draw.lights));
read_scalar(file, draw.diffuse_material_source);
read_scalar(file, draw.ambient_material_source);
read_scalar(file, draw.color_vertex);
} else if (draw.light0) {
auto& light = draw.lights[0];
light.type = 3;
std::memcpy(light.direction, draw.light0_direction, sizeof(light.direction));
std::memcpy(light.diffuse, draw.light0_diffuse, sizeof(light.diffuse));
std::memcpy(light.ambient, draw.light0_ambient, sizeof(light.ambient));
}
if (!file) throw std::runtime_error("truncated native draw capture state");
} else {
draw.z_enable = 1;
draw.z_write = 1;
}
if (version >= 4) {
read_vector(file, draw.bone_indices);
read_vector(file, draw.bone_weights);
read_vector(file, draw.bone_matrices);
}
}
if (version >= 3) {
std::uint32_t texture_count = 0;
read_scalar(file, texture_count);
if (texture_count > 8'192) throw std::runtime_error("native draw capture has too many textures");
for (std::uint32_t i = 0; i < texture_count; ++i) {
std::string name;
std::vector<std::uint8_t> bytes;
read_string(file, name);
read_vector(file, bytes, 16'777'216);
capture.textures.emplace(std::move(name), std::move(bytes));
}
}
if (version >= 4) {
read_scalar(file, capture.ui_width);
read_scalar(file, capture.ui_height);
std::uint32_t ui_count = 0;
read_scalar(file, ui_count);
if (ui_count > 100'000) throw std::runtime_error("native draw capture has too many UI commands");
capture.ui_commands.resize(ui_count);
for (auto& cmd : capture.ui_commands) {
std::uint32_t kind = 0, uiflags = 0;
read_scalar(file, kind);
cmd.kind = static_cast<UIRenderCommand::Kind>(kind);
read_scalar(file, cmd.x1);
read_scalar(file, cmd.y1);
read_scalar(file, cmd.x2);
read_scalar(file, cmd.y2);
read_scalar(file, cmd.argb);
read_scalar(file, cmd.end_argb);
read_scalar(file, cmd.clip_x1);
read_scalar(file, cmd.clip_y1);
read_scalar(file, cmd.clip_x2);
read_scalar(file, cmd.clip_y2);
read_string(file, cmd.text);
read_scalar(file, uiflags);
cmd.quad = (uiflags & 1u) != 0;
cmd.behind_3d = (uiflags & 2u) != 0;
file.read(reinterpret_cast<char*>(cmd.qx), sizeof(cmd.qx));
file.read(reinterpret_cast<char*>(cmd.qy), sizeof(cmd.qy));
read_scalar(file, cmd.su);
read_scalar(file, cmd.sv);
read_scalar(file, cmd.eu);
read_scalar(file, cmd.ev);
read_scalar(file, cmd.blend);
read_string(file, cmd.mask);
file.read(reinterpret_cast<char*>(cmd.mu), sizeof(cmd.mu));
file.read(reinterpret_cast<char*>(cmd.mv), sizeof(cmd.mv));
if (!file) throw std::runtime_error("truncated native draw capture UI command");
}
}
return capture;
}
inline std::vector<Render3DDraw> read(const std::string& path) {
return read_capture(path).draws;
}
} // namespace native_draw_capture
File diff suppressed because it is too large Load Diff
+139
View File
@@ -0,0 +1,139 @@
#version 450
layout(location = 0) in vec4 in_color;
layout(location = 1) in vec2 in_uv;
layout(location = 2) in vec2 in_mask_uv;
layout(location = 3) in float in_fog;
layout(location = 0) out vec4 out_color;
layout(set = 0, binding = 0) uniform sampler2D tex_sampler;
layout(set = 0, binding = 1) uniform sampler2D mask_sampler;
layout(push_constant) uniform DrawConstants {
mat4 mvp;
vec4 tint_color;
vec4 ambient_emissive;
vec4 light_dir;
vec4 light_diffuse;
} draw;
layout(set = 1, binding = 1, std430) readonly buffer FixedFunctionState {
uvec4 stage0_color;
uvec4 stage0_alpha;
uvec4 stage1_color;
uvec4 stage1_alpha;
vec4 fog_color;
vec4 fog_params;
vec4 texture_factor;
mat4 world_view;
uvec4 flags;
} fixed_state;
vec4 stage_arg(uint selector, vec4 diffuse, vec4 current, vec4 texel) {
uint source = selector & 15u;
vec4 value = source == 0u ? diffuse :
source == 1u ? current :
source == 2u ? texel :
source == 3u ? fixed_state.texture_factor : vec4(1.0);
if ((selector & 16u) != 0u) value = vec4(1.0) - value;
if ((selector & 32u) != 0u) value.rgb = vec3(value.a);
return value;
}
vec4 apply_op(uint op, vec4 a, vec4 b, vec4 current, vec4 diffuse, vec4 texel) {
if (op == 2u) return a; // SELECTARG1
if (op == 3u) return b; // SELECTARG2
if (op == 4u) return a * b; // MODULATE
if (op == 5u) return 2.0 * a * b; // MODULATE2X
if (op == 6u) return 4.0 * a * b; // MODULATE4X
if (op == 7u) return a + b; // ADD
if (op == 8u) return a + b - vec4(0.5); // ADDSIGNED
if (op == 9u) return 2.0 * (a + b - vec4(0.5)); // ADDSIGNED2X
if (op == 10u) return a - b; // SUBTRACT
if (op == 11u) return a + b - a * b; // ADDSMOOTH
if (op == 12u) return mix(b, a, diffuse.a);
if (op == 13u) return mix(b, a, texel.a);
if (op == 14u) return mix(b, a, fixed_state.texture_factor.a);
if (op == 15u) return a + b * (1.0 - texel.a);
if (op == 16u) return mix(b, a, current.a);
if (op == 18u) return vec4(a.rgb + a.a * b.rgb, a.a);
if (op == 19u) return vec4(a.rgb * b.rgb + vec3(a.a), a.a);
if (op == 20u) return vec4((1.0 - a.a) * b.rgb + a.rgb, a.a);
if (op == 21u) return vec4((vec3(1.0) - a.rgb) * b.rgb + vec3(a.a), a.a);
return current;
}
vec4 apply_stage(uvec4 color_state, uvec4 alpha_state,
vec4 diffuse, vec4 current, vec4 texel) {
if (color_state.x <= 1u) return current;
vec4 color_arg1 = stage_arg(color_state.y, diffuse, current, texel);
vec4 color_arg2 = stage_arg(color_state.z, diffuse, current, texel);
vec4 result = apply_op(color_state.x, color_arg1, color_arg2, current, diffuse, texel);
if (alpha_state.x > 1u) {
vec4 alpha_arg1 = stage_arg(alpha_state.y, diffuse, current, texel);
vec4 alpha_arg2 = stage_arg(alpha_state.z, diffuse, current, texel);
result.a = apply_op(alpha_state.x, alpha_arg1, alpha_arg2, current, diffuse, texel).a;
} else if (alpha_state.x == 1u) {
result.a = current.a;
}
return clamp(result, 0.0, 1.0);
}
void main() {
vec4 tex0 = texture(tex_sampler, in_uv);
if (fixed_state.flags.x == 2u) {
vec3 shadow = tex0.rgb;
if (fixed_state.flags.z != 0u && fixed_state.stage1_color.x == 4u)
shadow *= texture(mask_sampler, in_mask_uv).rgb;
shadow = mix(fixed_state.fog_color.rgb, shadow, in_fog);
if (all(greaterThanEqual(shadow, vec3(0.997)))) discard;
out_color = vec4(shadow, 1.0);
return;
}
if (fixed_state.flags.x != 0u) {
vec4 diffuse = in_color;
vec4 color = apply_stage(fixed_state.stage0_color, fixed_state.stage0_alpha,
diffuse, diffuse, tex0);
if (fixed_state.flags.z != 0u && fixed_state.stage1_color.x > 1u) {
vec4 tex1 = texture(mask_sampler, in_mask_uv);
color = apply_stage(fixed_state.stage1_color, fixed_state.stage1_alpha,
diffuse, color, tex1);
}
if (fixed_state.stage1_alpha.w != 0u) {
float ref = draw.ambient_emissive.a;
uint func = fixed_state.stage0_alpha.w;
bool passes = func == 1u ? false :
func == 2u ? color.a < ref :
func == 3u ? abs(color.a - ref) < (0.5 / 255.0) :
func == 4u ? color.a <= ref :
func == 5u ? color.a > ref :
func == 6u ? abs(color.a - ref) >= (0.5 / 255.0) :
func == 7u ? color.a >= ref : true;
if (!passes) discard;
}
color.rgb = mix(fixed_state.fog_color.rgb, color.rgb, in_fog);
out_color = color;
return;
}
vec4 color = in_color * tex0;
if (draw.light_dir.w < -0.4) {
vec4 mask_val = texture(mask_sampler, in_mask_uv);
if (draw.light_dir.w < -0.8) {
if (in_mask_uv.x < 0.0 || in_mask_uv.x > 1.0 || in_mask_uv.y < 0.0 || in_mask_uv.y > 1.0) {
discard;
}
color.rgb *= mask_val.rgb;
color.a = mask_val.a * in_color.a;
} else {
color.a = mask_val.a * in_color.a;
if (color.a <= 0.003) discard;
}
} else if (draw.light_dir.w > 1.001) {
// Specular sphere-map stage 1 (D3DTOP_MODULATEALPHA_ADDCOLOR)
float spec_power = draw.light_dir.w - 1.0;
vec4 spec_map = texture(mask_sampler, in_mask_uv);
color.rgb = min(color.rgb + (tex0.a * spec_power) * spec_map.rgb, vec3(1.5));
color.a = 1.0;
}
if (color.a <= draw.ambient_emissive.a) discard;
out_color = color;
}
+151
View File
@@ -0,0 +1,151 @@
#version 450
layout(location = 0) in vec3 in_position;
layout(location = 1) in vec3 in_normal;
layout(location = 2) in vec2 in_uv;
layout(location = 3) in vec4 in_color;
layout(location = 4) in uvec4 in_joints;
layout(location = 5) in vec4 in_weights;
layout(location = 6) in vec2 in_mask_uv;
layout(location = 7) in float in_rhw;
layout(location = 0) out vec4 out_color;
layout(location = 1) out vec2 out_uv;
layout(location = 2) out vec2 out_mask_uv;
layout(location = 3) out float out_fog;
layout(set = 1, binding = 0, std430) readonly buffer BonePalette {
mat4 bones[];
} bone_palette;
layout(push_constant) uniform DrawConstants {
mat4 mvp;
vec4 tint_color;
vec4 ambient_emissive;
vec4 light_dir;
vec4 light_diffuse;
} draw;
layout(set = 1, binding = 1, std430) readonly buffer FixedFunctionState {
uvec4 stage0_color;
uvec4 stage0_alpha;
uvec4 stage1_color;
uvec4 stage1_alpha;
vec4 fog_color;
vec4 fog_params;
vec4 texture_factor;
mat4 world_view;
uvec4 flags;
mat4 world;
uvec4 lighting_flags;
vec4 material_ambient;
vec4 material_emissive;
vec4 global_ambient;
vec4 light_position_type[2];
vec4 light_direction_range[2];
vec4 light_diffuse[2];
vec4 light_ambient[2];
vec4 light_attenuation[2];
vec4 light_spot[2];
} fixed_state;
void main() {
vec3 pos = in_position;
vec3 nrm = in_normal;
if (draw.light_diffuse.w > 0.5) {
uint bone_base = uint(draw.light_diffuse.w - 0.5);
vec3 skinned_pos = vec3(0.0);
vec3 skinned_nrm = vec3(0.0);
float total_w = 0.0;
for (int k = 0; k < 4; ++k) {
float w = in_weights[k];
if (w > 0.0) {
mat4 B = bone_palette.bones[bone_base + in_joints[k]];
skinned_pos += w * (B * vec4(pos, 1.0)).xyz;
skinned_nrm += w * (mat3(B) * nrm);
total_w += w;
}
}
if (total_w > 0.0) {
pos = skinned_pos;
nrm = skinned_nrm;
}
}
// D3D row-vector matrices are uploaded row-major. GLSL reads the bytes as their transpose.
gl_Position = draw.mvp * vec4(pos, 1.0);
if (draw.light_diffuse.w < -0.5) {
float clip_w = in_rhw > 0.0 ? 1.0 / in_rhw : 1.0;
gl_Position.xyz *= clip_w;
gl_Position.w = clip_w;
}
gl_Position.y = -gl_Position.y;
vec3 n = length(nrm) > 1e-4 ? normalize(nrm) : vec3(0.0, 0.0, 1.0);
out_color = in_color;
if (fixed_state.lighting_flags.x != 0u) {
vec3 world_pos = (fixed_state.world * vec4(pos, 1.0)).xyz;
vec3 world_normal = normalize(mat3(fixed_state.world) * n);
vec3 mat_diffuse = (fixed_state.lighting_flags.y != 0u && fixed_state.lighting_flags.w == 1u)
? in_color.rgb : draw.tint_color.rgb;
vec3 mat_ambient = (fixed_state.lighting_flags.y != 0u && fixed_state.lighting_flags.z == 1u)
? in_color.rgb : fixed_state.material_ambient.rgb;
vec3 ambient_sum = fixed_state.global_ambient.rgb;
vec3 diffuse_sum = vec3(0.0);
for (int i = 0; i < 2; ++i) {
int type = int(fixed_state.light_position_type[i].w + 0.5);
if (type == 0) continue;
vec3 L;
float strength = 1.0;
if (type == 3) {
L = normalize(-fixed_state.light_direction_range[i].xyz);
} else {
vec3 to_light = fixed_state.light_position_type[i].xyz - world_pos;
float distance_to_light = length(to_light);
if (distance_to_light > fixed_state.light_direction_range[i].w || distance_to_light < 0.0001)
continue;
L = to_light / distance_to_light;
vec4 attenuation = fixed_state.light_attenuation[i];
float denominator = attenuation.x + attenuation.y * distance_to_light +
attenuation.z * distance_to_light * distance_to_light;
strength = denominator > 0.0001 ? min(1.0 / denominator, 1.0) : 1.0;
if (type == 2) {
vec3 spot_dir = normalize(fixed_state.light_direction_range[i].xyz);
float cosine = dot(-L, spot_dir);
float inner = cos(fixed_state.light_spot[i].x * 0.5);
float outer = cos(fixed_state.light_spot[i].y * 0.5);
float cone = clamp((cosine - outer) / max(inner - outer, 0.0001), 0.0, 1.0);
strength *= pow(cone, max(attenuation.w, 0.0001));
}
}
ambient_sum += fixed_state.light_ambient[i].rgb * strength;
diffuse_sum += fixed_state.light_diffuse[i].rgb * max(dot(world_normal, L), 0.0) * strength;
}
vec3 lit = fixed_state.material_emissive.rgb + mat_ambient * ambient_sum + mat_diffuse * diffuse_sum;
float alpha = (fixed_state.lighting_flags.y != 0u && fixed_state.lighting_flags.w == 1u)
? in_color.a : draw.tint_color.a;
out_color = vec4(clamp(lit, 0.0, 1.0), alpha);
}
out_uv = in_uv;
if (draw.light_dir.w > 1.001) {
vec3 view_normal = normalize(mat3(fixed_state.world_view) * n);
vec3 view_pos = (fixed_state.world_view * vec4(pos, 1.0)).xyz;
vec3 view_dir = normalize(-view_pos);
vec3 reflected = reflect(-view_dir, view_normal);
out_mask_uv = reflected.xy * vec2(0.5, -0.5) + vec2(0.5);
} else {
out_mask_uv = in_mask_uv;
}
out_fog = 1.0;
if (fixed_state.flags.x != 0u && fixed_state.flags.w != 0u && draw.light_diffuse.w >= -0.5) {
vec3 eye = (fixed_state.world_view * vec4(pos, 1.0)).xyz;
float distance_to_eye = fixed_state.fog_params.w > 0.5 ? length(eye) : abs(eye.z);
if (fixed_state.flags.w == 3u) {
float span = fixed_state.fog_params.y - fixed_state.fog_params.x;
if (span > 0.0001)
out_fog = clamp((fixed_state.fog_params.y - distance_to_eye) / span, 0.0, 1.0);
} else if (fixed_state.flags.w == 1u) {
out_fog = clamp(exp(-fixed_state.fog_params.z * distance_to_eye), 0.0, 1.0);
} else if (fixed_state.flags.w == 2u) {
float fog_distance = fixed_state.fog_params.z * distance_to_eye;
out_fog = clamp(exp(-fog_distance * fog_distance), 0.0, 1.0);
}
}
}
+7
View File
@@ -0,0 +1,7 @@
#define STBI_NO_STDIO
#define STBI_ONLY_JPEG
#define STBI_ONLY_PNG
#define STBI_ONLY_BMP
#define STBI_MAX_DIMENSIONS 4096
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
File diff suppressed because it is too large Load Diff
+732
View File
@@ -0,0 +1,732 @@
#pragma once
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <string>
#include <vector>
#include "UIRenderCommands.h"
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
#include "platform/ScriptLib/PythonBoot.h"
#endif
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 dik;
float x, y, radius;
const char* label;
uint32_t color_idle;
uint32_t color_pressed;
bool pressed = false;
int64_t finger_id = -1;
};
TouchController() {
init_buttons();
}
void set_enabled(bool enabled) { enabled_ = enabled; }
bool is_enabled() const { return enabled_; }
void update_screen_size(int width, int height) {
if (width <= 0 || height <= 0) return;
screen_w_ = width;
screen_h_ = height;
// Position joystick on lower-left
joystick_base_x_ = std::max(90.0f, float(width) * 0.12f);
joystick_base_y_ = float(height) - std::max(90.0f, float(height) * 0.22f);
if (!joystick_active_) {
joystick_knob_x_ = joystick_base_x_;
joystick_knob_y_ = joystick_base_y_;
}
layout_buttons();
}
// Called once per frame to maintain continuous analog movement
void update() {
if (!enabled_) return;
if (joystick_active_) {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::SetMoveDirection(move_angle_, true);
#endif
}
}
// Handles finger touch down (norm_x, norm_y in 0.0 .. 1.0)
bool on_finger_down(int64_t finger_id, float norm_x, float norm_y) {
if (!enabled_) return false;
const float px = norm_x * float(screen_w_);
const float py = norm_y * float(screen_h_);
// 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();
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
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (PythonBoot::IsPointInsideActiveUI(int(px), int(py))) {
ui_touch_finger_id_ = finger_id;
PythonBoot::UIMouseMove(int(px), int(py));
PythonBoot::UIMouseButton(1, true, int(px), int(py));
return true;
}
#endif
// 3. Left side: virtual joystick (lower-left quadrant)
if (norm_x < 0.40f && norm_y > 0.35f && !joystick_active_) {
joystick_active_ = true;
joystick_finger_id_ = finger_id;
joystick_base_x_ = px;
joystick_base_y_ = py;
joystick_knob_x_ = px;
joystick_knob_y_ = py;
update_joystick_motion(px, py);
return true;
}
// 4. 3D Game World: camera rotation, pinch zoom, or tap to target
if (camera_finger_id_ < 0) {
camera_finger_id_ = finger_id;
camera_last_x_ = px;
camera_last_y_ = py;
camera_start_x_ = px;
camera_start_y_ = py;
camera_dragged_ = false;
camera_down_time_ = get_time_sec();
return true;
} else if (pinch_finger2_ < 0) {
pinch_finger1_ = camera_finger_id_;
pinch_finger2_ = finger_id;
pinch_last_dist_ = std::hypot(px - camera_last_x_, py - camera_last_y_);
return true;
}
return false;
}
// Handles finger motion
bool on_finger_motion(int64_t finger_id, float norm_x, float norm_y) {
if (!enabled_) return false;
const float px = norm_x * float(screen_w_);
const float py = norm_y * float(screen_h_);
// 1. UI Touch dragging (e.g. dragging item in inventory or scrollbar)
if (ui_touch_finger_id_ == finger_id) {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::UIMouseMove(int(px), int(py));
#endif
return true;
}
// 2. Virtual Joystick finger
if (joystick_active_ && finger_id == joystick_finger_id_) {
update_joystick_motion(px, py);
return true;
}
// 3. Button drag tracking (check if finger slid off)
for (auto& btn : buttons_) {
if (btn.finger_id == finger_id) {
const float dist = std::hypot(px - btn.x, py - btn.y);
if (dist > btn.radius * 1.5f && btn.pressed) {
btn.pressed = false;
trigger_button(btn.dik, false);
} else if (dist <= btn.radius * 1.5f && !btn.pressed) {
btn.pressed = true;
trigger_button(btn.dik, true);
}
return true;
}
}
// 4. Two-finger pinch zoom
if (pinch_finger1_ >= 0 && pinch_finger2_ >= 0 &&
(finger_id == pinch_finger1_ || finger_id == pinch_finger2_)) {
const float cur_dist = std::hypot(px - camera_last_x_, py - camera_last_y_);
if (pinch_last_dist_ > 1.0f && cur_dist > 1.0f) {
const float delta_d = cur_dist - pinch_last_dist_;
if (std::abs(delta_d) > 2.0f) {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::UIMouseWheel(int(delta_d * 8.0f));
#endif
pinch_last_dist_ = cur_dist;
}
}
return true;
}
// 5. Single-finger camera drag
if (finger_id == camera_finger_id_) {
const float total_dist = std::hypot(px - camera_start_x_, py - camera_start_y_);
if (total_dist > 6.0f) {
if (!camera_dragged_) {
camera_dragged_ = true;
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::CameraBeginDrag(int(camera_start_x_), int(camera_start_y_));
#endif
}
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::CameraDrag(int(px), int(py));
#endif
}
camera_last_x_ = px;
camera_last_y_ = py;
return true;
}
return false;
}
// Handles finger touch up
bool on_finger_up(int64_t finger_id, float norm_x, float norm_y) {
if (!enabled_) return false;
const float px = norm_x * float(screen_w_);
const float py = norm_y * float(screen_h_);
// 1. UI Touch release (e.g. dropped item in inventory or clicked button)
if (ui_touch_finger_id_ == finger_id) {
ui_touch_finger_id_ = -1;
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::UIMouseButton(1, false, int(px), int(py));
#endif
return true;
}
// 2. Joystick release
if (joystick_active_ && finger_id == joystick_finger_id_) {
joystick_active_ = false;
joystick_finger_id_ = -1;
joystick_knob_x_ = joystick_base_x_;
joystick_knob_y_ = joystick_base_y_;
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::SetMoveDirection(0.0f, false);
#endif
return true;
}
// 3. Button release
for (auto& btn : buttons_) {
if (btn.finger_id == finger_id) {
if (btn.pressed) {
btn.pressed = false;
trigger_button(btn.dik, false);
}
btn.finger_id = -1;
return true;
}
}
// 4. Pinch end
if (finger_id == pinch_finger1_ || finger_id == pinch_finger2_) {
pinch_finger1_ = -1;
pinch_finger2_ = -1;
pinch_last_dist_ = 0.0f;
if (finger_id == camera_finger_id_) camera_finger_id_ = -1;
return true;
}
// 5. Camera finger release
if (finger_id == camera_finger_id_) {
camera_finger_id_ = -1;
if (camera_dragged_) {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::CameraEndDrag();
#endif
} else if ((get_time_sec() - camera_down_time_) < 0.35) {
// Short tap on 3D world: select target (mob, NPC, ground)
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::UIMouseMove(int(px), int(py));
PythonBoot::UIMouseButton(1, true, int(px), int(py));
PythonBoot::UIMouseButton(1, false, int(px), int(py));
#endif
}
return true;
}
return false;
}
// Render virtual joystick, buttons, drawer menu, and player status bar
void append_ui_commands(std::vector<UIRenderCommand>& commands) const {
if (!enabled_) 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) ---
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);
const uint32_t icon_col = btn.pressed ? 0xFFFFFFFF : 0xDDFFFFFF;
const float r = btn.radius;
switch (btn.id) {
case 1: { // ATK: crossed swords
const float s = r * 0.35f;
draw_line(commands, btn.x - s, btn.y - s, btn.x + s, btn.y + s, icon_col);
draw_line(commands, btn.x + s, btn.y - s, btn.x - s, btn.y + s, icon_col);
const float g = s * 0.35f;
draw_line(commands, btn.x - s*0.4f - g, btn.y - s*0.4f + g, btn.x - s*0.4f + g, btn.y - s*0.4f - g, icon_col);
draw_line(commands, btn.x + s*0.4f - g, btn.y - s*0.4f - g, btn.x + s*0.4f + g, btn.y - s*0.4f + g, icon_col);
break;
}
case 2: { // S1: I
const float h = r * 0.35f;
draw_line(commands, btn.x, btn.y - h, btn.x, btn.y + h, icon_col);
draw_line(commands, btn.x - 4.0f, btn.y - h, btn.x + 4.0f, btn.y - h, icon_col);
draw_line(commands, btn.x - 4.0f, btn.y + h, btn.x + 4.0f, btn.y + h, icon_col);
break;
}
case 3: { // S2: II
const float h = r * 0.35f;
draw_line(commands, btn.x - 4.0f, btn.y - h, btn.x - 4.0f, btn.y + h, icon_col);
draw_line(commands, btn.x + 4.0f, btn.y - h, btn.x + 4.0f, btn.y + h, icon_col);
break;
}
case 4: { // S3: III
const float h = r * 0.35f;
draw_line(commands, btn.x - 6.0f, btn.y - h, btn.x - 6.0f, btn.y + h, icon_col);
draw_line(commands, btn.x, btn.y - h, btn.x, btn.y + h, icon_col);
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);
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);
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);
break;
}
}
}
// --- 4. Virtual 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_,
joystick_base_x_ - joystick_radius_ + 8.0f, joystick_base_y_, 0x90FFFFFF);
draw_line(commands, joystick_base_x_ + joystick_radius_ - 8.0f, joystick_base_y_,
joystick_base_x_ + joystick_radius_, joystick_base_y_, 0x90FFFFFF);
draw_line(commands, joystick_base_x_, joystick_base_y_ - joystick_radius_,
joystick_base_x_, joystick_base_y_ - joystick_radius_ + 8.0f, 0x90FFFFFF);
draw_line(commands, joystick_base_x_, joystick_base_y_ + joystick_radius_ - 8.0f,
joystick_base_x_, joystick_base_y_ + joystick_radius_, 0x90FFFFFF);
if (joystick_active_) {
draw_line(commands, joystick_base_x_, joystick_base_y_, joystick_knob_x_, joystick_knob_y_, 0xB000FFFF);
}
const uint32_t knob_color = joystick_active_ ? 0xB040A0FF : 0x6040A0FF;
draw_filled_disc(commands, joystick_knob_x_, joystick_knob_y_, joystick_knob_radius_, knob_color);
draw_circle(commands, joystick_knob_x_, joystick_knob_y_, joystick_knob_radius_ * 0.5f, 0x80FFFFFF, 12);
}
// Desktop mouse testing simulation
bool on_mouse_button(int button, bool pressed, int x, int y) {
if (!enabled_) return false;
const float norm_x = float(x) / float(screen_w_);
const float norm_y = float(y) / float(screen_h_);
if (pressed) {
if (button == 1) {
return on_finger_down(101, norm_x, norm_y);
}
return false;
} else {
if (button == 1) {
return on_finger_up(101, norm_x, norm_y);
}
return false;
}
}
bool on_mouse_motion(int x, int y) {
if (!enabled_) return false;
const float norm_x = float(x) / float(screen_w_);
const float norm_y = float(y) / float(screen_h_);
bool handled = false;
if (ui_touch_finger_id_ == 101) {
handled |= on_finger_motion(101, norm_x, norm_y);
}
if (joystick_active_ && joystick_finger_id_ == 101) {
handled |= on_finger_motion(101, norm_x, norm_y);
}
if (camera_finger_id_ == 101) {
handled |= on_finger_motion(101, norm_x, norm_y);
}
return handled;
}
private:
void init_buttons() {
buttons_.clear();
// Combat Action Wheel (Lower-Right)
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
}
void layout_buttons() {
const float W = float(screen_w_);
const float H = float(screen_h_);
for (auto& btn : buttons_) {
switch (btn.id) {
case 1:
btn.x = W - 85.0f;
btn.y = H - 85.0f;
btn.radius = std::min(46.0f, H * 0.12f);
break;
case 2:
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;
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;
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;
}
}
}
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);
}
}
return -1;
}
void trigger_button(int dik, bool pressed) {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
if (dik == 0x39) {
PythonBoot::SetAttackKey(pressed);
}
if (dik != 0) {
PythonBoot::UIKey(dik, pressed);
}
#endif
}
void update_joystick_motion(float px, float py) {
const float dx = px - joystick_base_x_;
const float dy = py - joystick_base_y_;
const float dist = std::hypot(dx, dy);
if (dist <= joystick_radius_) {
joystick_knob_x_ = px;
joystick_knob_y_ = py;
} else if (dist > 0.0f) {
joystick_knob_x_ = joystick_base_x_ + (dx / dist) * joystick_radius_;
joystick_knob_y_ = joystick_base_y_ + (dy / dist) * joystick_radius_;
}
if (dist > 8.0f) {
const float rad = std::atan2(-dx, -dy);
move_angle_ = rad * 180.0f / 3.14159265358979323846f;
if (move_angle_ < 0.0f) move_angle_ += 360.0f;
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::SetMoveDirection(move_angle_, true);
#endif
} else {
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
PythonBoot::SetMoveDirection(0.0f, false);
#endif
}
}
static void draw_line(std::vector<UIRenderCommand>& commands,
float x1, float y1, float x2, float y2, uint32_t argb) {
UIRenderCommand cmd{};
cmd.kind = UIRenderCommand::Line;
cmd.x1 = x1; cmd.y1 = y1;
cmd.x2 = x2; cmd.y2 = y2;
cmd.argb = argb;
commands.push_back(cmd);
}
static void draw_rect_bar(std::vector<UIRenderCommand>& commands,
float x1, float y1, float x2, float y2, uint32_t argb) {
UIRenderCommand bar{};
bar.kind = UIRenderCommand::Bar;
bar.x1 = x1; bar.y1 = y1;
bar.x2 = x2; bar.y2 = y2;
bar.argb = argb;
commands.push_back(bar);
}
static void draw_rect_lines(std::vector<UIRenderCommand>& commands,
float x1, float y1, float x2, float y2, uint32_t argb) {
draw_line(commands, x1, y1, x2, y1, argb);
draw_line(commands, x2, y1, x2, y2, argb);
draw_line(commands, x2, y2, x1, y2, argb);
draw_line(commands, x1, y2, x1, y1, argb);
}
static void draw_circle(std::vector<UIRenderCommand>& commands,
float cx, float cy, float radius, uint32_t argb, int segments = 16) {
const float step = 2.0f * 3.14159265f / float(segments);
for (int i = 0; i < segments; ++i) {
const float a1 = float(i) * step;
const float a2 = float(i + 1) * step;
draw_line(commands,
cx + std::cos(a1) * radius, cy + std::sin(a1) * radius,
cx + std::cos(a2) * radius, cy + std::sin(a2) * radius,
argb);
}
}
static void draw_filled_disc(std::vector<UIRenderCommand>& commands,
float cx, float cy, float radius, uint32_t argb) {
// Base rectangular fill
draw_rect_bar(commands, cx - radius * 0.65f, cy - radius * 0.65f, cx + radius * 0.65f, cy + radius * 0.65f, (argb & 0x00FFFFFF) | 0x55000000);
// Cross fills for roundness
draw_rect_bar(commands, cx - radius * 0.85f, cy - radius * 0.35f, cx + radius * 0.85f, cy + radius * 0.35f, (argb & 0x00FFFFFF) | 0x55000000);
draw_rect_bar(commands, cx - radius * 0.35f, cy - radius * 0.85f, cx + radius * 0.35f, cy + radius * 0.85f, (argb & 0x00FFFFFF) | 0x55000000);
// Border rings
draw_circle(commands, cx, cy, radius, argb, 20);
draw_circle(commands, cx, cy, radius - 1.0f, (argb & 0x00FFFFFF) | 0x40000000, 20);
}
static double get_time_sec() {
using namespace std::chrono;
return duration_cast<duration<double>>(steady_clock::now().time_since_epoch()).count();
}
bool enabled_ = false;
int screen_w_ = 1280;
int screen_h_ = 720;
bool drawer_open_ = false;
int64_t ui_touch_finger_id_ = -1;
bool joystick_active_ = false;
int64_t joystick_finger_id_ = -1;
float joystick_base_x_ = 140.0f;
float joystick_base_y_ = 580.0f;
float joystick_knob_x_ = 140.0f;
float joystick_knob_y_ = 580.0f;
float joystick_radius_ = 65.0f;
float joystick_knob_radius_ = 28.0f;
float move_angle_ = 0.0f;
int64_t camera_finger_id_ = -1;
float camera_start_x_ = 0.0f;
float camera_start_y_ = 0.0f;
float camera_last_x_ = 0.0f;
float camera_last_y_ = 0.0f;
double camera_down_time_ = 0.0;
bool camera_dragged_ = false;
int64_t pinch_finger1_ = -1;
int64_t pinch_finger2_ = -1;
float pinch_last_dist_ = 0.0f;
std::vector<ButtonDef> buttons_;
};