Adapt Android mobile controls and native client UI
This commit is contained in:
+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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user