// SPDX-License-Identifier: GPL-3.0-or-later #include "mouse_camera.h" #include "input.h" #include "mouse_turn.h" #include "../engine/engine_hooks.h" #include "../engine/online_fixes.h" #include #include #include #include #include #include #ifdef _WIN32 #define LANTERN_STUB_SYMBOL #else #define LANTERN_STUB_SYMBOL __attribute__((visibility("hidden"))) #endif extern "C" { LANTERN_STUB_SYMBOL std::uint64_t lantern_free_camera_resume = 0; LANTERN_STUB_SYMBOL std::uint8_t lantern_free_camera_ran = 0; LANTERN_STUB_SYMBOL __attribute__((sysv_abi)) void lantern_free_camera_turn(std::uint8_t* camera, std::uint8_t* had_input, float stick_pitch, float stick_yaw); void lantern_free_camera_site(); void lantern_fast_turn_exit(); } asm(R"( .text .p2align 4 .globl lantern_free_camera_site lantern_free_camera_site: vmovss %xmm0, 0x140(%r13) vextractps $1, %xmm1, 0x144(%r13) push %rbx push %rax push %rcx push %rdx push %rsi push %rdi push %r8 push %r9 push %r10 push %r11 mov %rsp, %rbx and $-32, %rsp sub $512, %rsp vmovdqu %ymm0, 0x000(%rsp) vmovdqu %ymm1, 0x020(%rsp) vmovdqu %ymm2, 0x040(%rsp) vmovdqu %ymm3, 0x060(%rsp) vmovdqu %ymm4, 0x080(%rsp) vmovdqu %ymm5, 0x0a0(%rsp) vmovdqu %ymm6, 0x0c0(%rsp) vmovdqu %ymm7, 0x0e0(%rsp) vmovdqu %ymm8, 0x100(%rsp) vmovdqu %ymm9, 0x120(%rsp) vmovdqu %ymm10, 0x140(%rsp) vmovdqu %ymm11, 0x160(%rsp) vmovdqu %ymm12, 0x180(%rsp) vmovdqu %ymm13, 0x1a0(%rsp) vmovdqu %ymm14, 0x1c0(%rsp) vmovdqu %ymm15, 0x1e0(%rsp) mov %r13, %rdi lea -0x3c4(%rbp), %rsi vmovaps %xmm10, %xmm0 vmovshdup %xmm10, %xmm1 vzeroupper call lantern_free_camera_turn vmovdqu 0x000(%rsp), %ymm0 vmovdqu 0x020(%rsp), %ymm1 vmovdqu 0x040(%rsp), %ymm2 vmovdqu 0x060(%rsp), %ymm3 vmovdqu 0x080(%rsp), %ymm4 vmovdqu 0x0a0(%rsp), %ymm5 vmovdqu 0x0c0(%rsp), %ymm6 vmovdqu 0x0e0(%rsp), %ymm7 vmovdqu 0x100(%rsp), %ymm8 vmovdqu 0x120(%rsp), %ymm9 vmovdqu 0x140(%rsp), %ymm10 vmovdqu 0x160(%rsp), %ymm11 vmovdqu 0x180(%rsp), %ymm12 vmovdqu 0x1a0(%rsp), %ymm13 vmovdqu 0x1c0(%rsp), %ymm14 vmovdqu 0x1e0(%rsp), %ymm15 mov %rbx, %rsp pop %r11 pop %r10 pop %r9 pop %r8 pop %rdi pop %rsi pop %rdx pop %rcx pop %rax pop %rbx vmovss 0x140(%r13), %xmm0 jmp *lantern_free_camera_resume(%rip) .p2align 4 .globl lantern_fast_turn_exit lantern_fast_turn_exit: vmovss %xmm0, 0x140(%r13) movb $1, lantern_free_camera_ran(%rip) jmp *lantern_free_camera_resume(%rip) )"); namespace MouseCamera { namespace { using Clock = std::chrono::steady_clock; constexpr std::size_t kUpdate = 0x143ac60; constexpr std::uint8_t kUpdateEntry[] = {0x55, 0x48, 0x89, 0xe5, 0x41, 0x57, 0x41, 0x56, 0x41, 0x55, 0x41, 0x54, 0x53, 0x48, 0x81, 0xec, 0xd8, 0x03, 0x00, 0x00}; constexpr std::size_t kFreeStore = 0x143ce54, kAfterStore = 0x143ce67; constexpr std::uint8_t kFreeStoreCode[] = {0xc4, 0xc1, 0x7a, 0x11, 0x85, 0x40, 0x01, 0x00, 0x00, 0xc4, 0xc3, 0x79, 0x17, 0x8d, 0x44, 0x01, 0x00, 0x00, 0x01}; struct FastTurnExit { std::size_t at; std::uint8_t code[14]; }; constexpr FastTurnExit kFastTurnExits[] = { {0x143f9bc, {0xc4, 0xc1, 0x7a, 0x11, 0x85, 0x40, 0x01, 0x00, 0x00, 0xe9, 0x9d, 0xd4, 0xff, 0xff}}, {0x143fabf, {0xc4, 0xc1, 0x7a, 0x11, 0x85, 0x40, 0x01, 0x00, 0x00, 0xe9, 0x9a, 0xd3, 0xff, 0xff}}, }; constexpr std::size_t kPitch = 0x140, kYaw = 0x144, kHighest = 0x1ec, kLowest = 0x1f0; struct AutoRotationStore { std::size_t at; std::uint8_t code[9]; bool held_by_mouse; }; constexpr AutoRotationStore kAutoRotation[] = { {0x143c6e8, {0xc4, 0xc1, 0x7a, 0x11, 0x85, 0x44, 0x01, 0x00, 0x00}, true}, {0x143c870, {0xc4, 0xc1, 0x7a, 0x11, 0xad, 0x44, 0x01, 0x00, 0x00}, false}, {0x143c984, {0xc4, 0xc1, 0x7a, 0x11, 0x8d, 0x30, 0x01, 0x00, 0x00}, true}, {0x143dde6, {0xc4, 0xc1, 0x7a, 0x11, 0x85, 0x94, 0x02, 0x00, 0x00}, true}, }; constexpr std::uint8_t kNops[9] = {0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90}; constexpr float kFlickRate = 3000.0f, kFlickRearmRate = 1500.0f, kFlickCooldown = 0.5f; constexpr auto kFlickShown = std::chrono::milliseconds(80); std::uint8_t* g_image = nullptr; bool g_installed = false; std::mutex g_counts_lock; float g_count_x = 0, g_count_y = 0; std::atomic g_degrees_per_count{MouseTurn::DegreesPerCount(5)}; std::atomic g_invert_x{false}, g_invert_y{false}, g_hold{false}; float g_update_x = 0, g_update_y = 0; bool g_auto_rotation_nops = false; bool g_patched_out = false; bool g_store_is_games[std::size(kAutoRotation)] = {}; bool g_free_last_update = true; std::mutex g_flick_lock; float g_flick_x = 0, g_flick_y = 0; unsigned g_flick_updates = 0; Clock::time_point g_last_read{}; float g_cooldown = 0; float g_shown_x = 0, g_shown_y = 0; Clock::time_point g_shown_until{}; std::atomic g_turns{0}, g_flicks{0}, g_free_changes{0}; std::uint8_t* At(std::size_t offset) { return g_image + offset; } void HoldAutoRotation(bool hold) { if (g_patched_out || hold == g_auto_rotation_nops) return; for (std::size_t i = 0; i < std::size(kAutoRotation); ++i) { const AutoRotationStore& s = kAutoRotation[i]; if (s.held_by_mouse && g_store_is_games[i]) BbEngine::WriteCode(At(s.at), hold ? kNops : s.code, sizeof s.code); } g_auto_rotation_nops = hold; } std::int64_t __attribute__((sysv_abi)) OnCameraUpdate(std::uint64_t, const std::uint64_t*) { { std::scoped_lock lock(g_counts_lock); g_update_x = g_count_x; g_update_y = g_count_y; g_count_x = g_count_y = 0; } HoldAutoRotation(g_hold.load(std::memory_order_relaxed)); const bool free = __atomic_exchange_n(&lantern_free_camera_ran, 0, __ATOMIC_RELAXED) != 0; if (!free) { std::scoped_lock lock(g_flick_lock); g_flick_x += g_update_x; g_flick_y += g_update_y; ++g_flick_updates; } static unsigned updates = 0; if (++updates > 2 && free != g_free_last_update && g_free_changes.fetch_add(1, std::memory_order_relaxed) < 8) host_log("mouse camera: the free camera %s", free ? "is back" : "stopped (locked on, or the game turns it)"); g_free_last_update = free; return 0; } bool JumpFrom(std::uint8_t* site, std::size_t size, void (*to)()) { std::uint8_t code[32]; std::memset(code, 0xcc, sizeof code); const auto target = reinterpret_cast(to); code[0] = 0xff; code[1] = 0x25; std::memset(code + 2, 0, 4); std::memcpy(code + 6, &target, 8); return size >= 14 && size <= sizeof code && BbEngine::WriteCode(site, code, size); } } bool Install(std::uint8_t* image, std::size_t size) { g_image = image; const auto matches = [&](std::size_t at, const std::uint8_t* code, std::size_t n) { return at + n <= size && std::memcmp(image + at, code, n) == 0; }; if (!matches(kFreeStore, kFreeStoreCode, sizeof kFreeStoreCode)) { host_log("mouse camera: off, the follow camera's store is not 1.09's"); return false; } for (const FastTurnExit& exit : kFastTurnExits) { if (!matches(exit.at, exit.code, sizeof exit.code)) { host_log("mouse camera: off, the follow camera's fast turn is not 1.09's"); return false; } } bool patched = false; for (std::size_t i = 0; i < std::size(kAutoRotation); ++i) { const AutoRotationStore& s = kAutoRotation[i]; g_store_is_games[i] = matches(s.at, s.code, sizeof s.code); patched = patched || matches(s.at, kNops, sizeof kNops); } BbEngine::Hook entry; if (!BbEngine::Prepare(entry, image, size, kUpdate, kUpdateEntry, OnCameraUpdate)) { host_log("mouse camera: off, the follow camera's update is not 1.09's"); return false; } g_patched_out = patched; lantern_free_camera_resume = reinterpret_cast(At(kAfterStore)); bool placed = JumpFrom(At(kFreeStore), sizeof kFreeStoreCode, lantern_free_camera_site); for (const FastTurnExit& exit : kFastTurnExits) placed = placed && JumpFrom(At(exit.at), sizeof exit.code, lantern_fast_turn_exit); if (!placed) { BbEngine::WriteCode(At(kFreeStore), kFreeStoreCode, sizeof kFreeStoreCode); for (const FastTurnExit& exit : kFastTurnExits) BbEngine::WriteCode(At(exit.at), exit.code, sizeof exit.code); BbEngine::Discard(entry); host_log("mouse camera: off, the follow camera's code could not be written"); return false; } BbEngine::Commit(std::span(&entry, 1)); g_installed = true; host_log("mouse camera: %.3f degrees a count at sensitivity 5, after the stick and only without it; a flick " "switches the lock-on target%s", MouseTurn::DegreesPerCount(5), g_patched_out ? "; a patch holds the camera's auto-rotation already" : ""); return true; } bool Installed() { return g_installed; } void AddCounts(float dx, float dy) { std::scoped_lock lock(g_counts_lock); g_count_x += dx; g_count_y += dy; } void Reset() { { std::scoped_lock lock(g_counts_lock); g_count_x = g_count_y = 0; } std::scoped_lock lock(g_flick_lock); g_flick_x = g_flick_y = 0; g_flick_updates = 0; } void Publish(int sensitivity, bool invert_x, bool invert_y, bool hold_auto_rotation) { g_degrees_per_count.store(MouseTurn::DegreesPerCount(sensitivity), std::memory_order_relaxed); g_invert_x.store(invert_x, std::memory_order_relaxed); g_invert_y.store(invert_y, std::memory_order_relaxed); static unsigned changes = 0; if (g_hold.exchange(hold_auto_rotation, std::memory_order_relaxed) != hold_auto_rotation && changes++ < 6) host_log("mouse camera: auto-rotation %s", hold_auto_rotation ? "held (the keyboard and mouse in use)" : "the game's own"); } bool Flick(float& x, float& y) { const Clock::time_point now = Clock::now(); float dt = g_last_read == Clock::time_point{} ? 0.0f : std::chrono::duration(now - g_last_read).count(); dt = std::min(dt, 0.25f); g_last_read = now; float dx, dy; unsigned updates; { std::scoped_lock lock(g_flick_lock); dx = g_flick_x; dy = g_flick_y; updates = g_flick_updates; g_flick_x = g_flick_y = 0; g_flick_updates = 0; } if (updates && dt > 0) { const float distance = std::hypot(dx, dy); const float rate = distance / dt; g_cooldown = rate < kFlickRearmRate ? 0.0f : std::max(0.0f, g_cooldown - dt); if (g_cooldown <= 0 && rate > kFlickRate) { g_shown_x = dx / distance; g_shown_y = dy / distance; g_shown_until = now + kFlickShown; g_cooldown = kFlickCooldown; if (g_flicks.fetch_add(1, std::memory_order_relaxed) < 4) host_log("mouse camera: a flick of %.0f counts (%.0f a second) switches the lock-on target toward " "%+.2f,%+.2f", distance, rate, g_shown_x, g_shown_y); } } if (now >= g_shown_until) return false; x = g_shown_x; y = g_shown_y; return true; } } extern "C" __attribute__((sysv_abi)) void lantern_free_camera_turn(std::uint8_t* camera, std::uint8_t* had_input, float stick_pitch, float stick_yaw) { using namespace MouseCamera; __atomic_store_n(&lantern_free_camera_ran, 1, __ATOMIC_RELAXED); { float seen; std::memcpy(&seen, camera + kYaw, 4); BbEngine::OnlineFixes::SetCameraYaw(seen); } if (g_update_x == 0 && g_update_y == 0) return; if (stick_pitch != 0 || stick_yaw != 0) return; float pitch, yaw, highest, lowest; std::memcpy(&pitch, camera + kPitch, 4); std::memcpy(&yaw, camera + kYaw, 4); std::memcpy(&highest, camera + kHighest, 4); std::memcpy(&lowest, camera + kLowest, 4); const float was_pitch = pitch, was_yaw = yaw; const MouseTurn::Settings s{g_degrees_per_count.load(std::memory_order_relaxed), g_invert_x.load(std::memory_order_relaxed), g_invert_y.load(std::memory_order_relaxed)}; if (!MouseTurn::Turn(g_update_x, g_update_y, s, pitch, yaw, lowest, highest)) return; std::memcpy(camera + kPitch, &pitch, 4); std::memcpy(camera + kYaw, &yaw, 4); *had_input = 1; if (g_turns.fetch_add(1, std::memory_order_relaxed) == 0) host_log("mouse camera: first turn, %+.0f,%+.0f counts: pitch %.4f -> %.4f, yaw %.4f -> %.4f", g_update_x, g_update_y, was_pitch, pitch, was_yaw, yaw); }