From 2fa25dd4799a6cd05b6b84f3c0f567615e6a661c Mon Sep 17 00:00:00 2001 From: sqwishy Date: Wed, 2 Oct 2024 22:12:36 -0700 Subject: [PATCH] FEAT(positional-audio): Plugin for Grounded Positional audio support for the Steam release of Grounded. Tested on 1.4.3.4578, 1.4.4.4634, and 1.4.5.4679. --- plugins/CMakeLists.txt | 1 + plugins/HostWindows.cpp | 4 + plugins/HostWindows.h | 2 + plugins/ProcessBase.h | 1 + plugins/ProcessWindows.h | 1 + plugins/grounded/CMakeLists.txt | 17 +++ plugins/grounded/grounded.cpp | 263 ++++++++++++++++++++++++++++++++ 7 files changed, 289 insertions(+) create mode 100644 plugins/grounded/CMakeLists.txt create mode 100644 plugins/grounded/grounded.cpp diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 1faefad9828..c2a14fd3821 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -57,6 +57,7 @@ if(WIN32 OR (UNIX AND CMAKE_SYSTEM_NAME STREQUAL "Linux")) "ffxiv" "ffxiv_x64" "gmod" + "grounded" "gtaiv" "gtasa" "gtav" diff --git a/plugins/HostWindows.cpp b/plugins/HostWindows.cpp index 1bad08c2c92..ef1503cc836 100644 --- a/plugins/HostWindows.cpp +++ b/plugins/HostWindows.cpp @@ -18,6 +18,10 @@ HostWindows::~HostWindows() { } } +HostWindows::HostWindows(HostWindows &&other) noexcept + : m_pid(other.m_pid), m_handle(std::exchange(other.m_handle, nullptr)) { +} + bool HostWindows::peek(const procptr_t address, void *dst, const size_t size) const { SIZE_T read; const auto ok = ReadProcessMemory(m_handle, reinterpret_cast< void * >(address), dst, size, &read); diff --git a/plugins/HostWindows.h b/plugins/HostWindows.h index fbad687d00d..2ecd8959943 100644 --- a/plugins/HostWindows.h +++ b/plugins/HostWindows.h @@ -22,6 +22,8 @@ class HostWindows { Modules modules() const; HostWindows(const procid_t pid); + HostWindows(const HostWindows &) = delete; + HostWindows(HostWindows &&other) noexcept; virtual ~HostWindows(); }; diff --git a/plugins/ProcessBase.h b/plugins/ProcessBase.h index 29c2cdfea9c..732dd3e1bd1 100644 --- a/plugins/ProcessBase.h +++ b/plugins/ProcessBase.h @@ -108,6 +108,7 @@ class ProcessBase : public Host { procptr_t findPattern(const std::vector< uint8_t > &pattern, procptr_t address, const size_t size); ProcessBase(const procid_t id, const std::string &name); + ProcessBase(ProcessBase &&other) = default; virtual ~ProcessBase(); }; diff --git a/plugins/ProcessWindows.h b/plugins/ProcessWindows.h index a44b9ccd442..e6ccff06fb0 100644 --- a/plugins/ProcessWindows.h +++ b/plugins/ProcessWindows.h @@ -14,6 +14,7 @@ class ProcessWindows : public ProcessBase { procptr_t exportedSymbol(const std::string &symbol, const procptr_t module) const override; ProcessWindows(const procid_t id, const std::string &name); + ProcessWindows(ProcessWindows &&other) = default; virtual ~ProcessWindows(); }; diff --git a/plugins/grounded/CMakeLists.txt b/plugins/grounded/CMakeLists.txt new file mode 100644 index 00000000000..62254cff8f9 --- /dev/null +++ b/plugins/grounded/CMakeLists.txt @@ -0,0 +1,17 @@ +# Copyright The Mumble Developers. All rights reserved. +# Use of this source code is governed by a BSD-style license +# that can be found in the LICENSE file at the root of the +# Mumble source tree or at . + +add_library(grounded SHARED + "grounded.cpp" + + "../Module.cpp" + "../ProcessBase.cpp" + "../ProcessWindows.cpp") + +if(WIN32) + target_sources(grounded PRIVATE "../HostWindows.cpp") +else() + target_sources(grounded PRIVATE "../HostLinux.cpp") +endif() diff --git a/plugins/grounded/grounded.cpp b/plugins/grounded/grounded.cpp new file mode 100644 index 00000000000..5d27551e3d4 --- /dev/null +++ b/plugins/grounded/grounded.cpp @@ -0,0 +1,263 @@ +// Copyright The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#include "ProcessWindows.h" +#include "MumblePlugin.h" + +#include "mumble_positional_audio_utils.h" + +#include +#include +#include + +using GroundedHandle = std::tuple< ProcessWindows, procptr_t >; + +static std::unique_ptr< GroundedHandle > handle; + +/* the indexes of these float[3] from the game are + * 0: south low, north high + * 1: west low, east high + * 2: altitude, high toward sky, goes up when you jump */ +struct GroundedCam { + float top[3]; + std::uint8_t _unused1[4]; + float front[3]; + std::uint8_t _unused2[4]; + float pos[3]; +}; + +static_assert(sizeof(struct GroundedCam) == 16 + 16 + 12, "GroundedCam struct has unexpected size"); + +constexpr float unreal_to_mumble_units(float unreal) { + return unreal / 100.0f; +} + +float float3_magnitude(float f[3]) { + return sqrtf(f[0] * f[0] + f[1] * f[1] + f[2] * f[2]); +} + +bool float3_is_unit(float f[3]) { + const float err = 0.001f; + const float mag = float3_magnitude(f); + return mag > (1.0f - err) && mag < (1.0f + err); +} + + +mumble_error_t mumble_init(uint32_t) { + return MUMBLE_STATUS_OK; +} + +void mumble_shutdown() { +} + +MumbleStringWrapper mumble_getName() { + static const char name[] = "Grounded"; + + MumbleStringWrapper wrapper; + wrapper.data = name; + wrapper.size = strlen(name); + wrapper.needsReleasing = false; + + return wrapper; +} + +MumbleStringWrapper mumble_getDescription() { + static const char description[] = "Positional audio support for Grounded. Steam release version >= 1.4.3.4578."; + + MumbleStringWrapper wrapper; + wrapper.data = description; + wrapper.size = strlen(description); + wrapper.needsReleasing = false; + + return wrapper; +} + +MumbleStringWrapper mumble_getAuthor() { + static const char author[] = "MumbleDevelopers"; + + MumbleStringWrapper wrapper; + wrapper.data = author; + wrapper.size = strlen(author); + wrapper.needsReleasing = false; + + return wrapper; +} + +mumble_version_t mumble_getAPIVersion() { + return MUMBLE_PLUGIN_API_VERSION; +} + +void mumble_registerAPIFunctions(void *) { +} + +void mumble_releaseResource(const void *) { +} + +mumble_version_t mumble_getVersion() { + return { 1, 0, 0 }; +} + +uint32_t mumble_getFeatures() { + return MUMBLE_FEATURE_POSITIONAL; +} + +uint8_t mumble_initPositionalData(const char *const *programNames, const uint64_t *programPIDs, size_t programCount) { + const std::string exename = "Maine-Win64-Shipping.exe"; + + for (size_t i = 0; i < programCount; ++i) { + if (programNames[i] != exename) { + continue; + } + + ProcessWindows proc(programPIDs[i], programNames[i]); + + if (!proc.isOk()) { + continue; + } + + const Modules &modules = proc.modules(); + const auto iter = modules.find(exename); + + if (iter == modules.cend()) { + continue; + } + + // An address pointing to the start our pointer chain is moved + // into r10. findPattern and peekRIP read executable regions of + // memory and give us the address that would be copied to r10. + // + // This address is in executable code and points to a data + // page in the executable. It's not moving around between + // program launches. Instead, we use findPattern to support + // different versions of the game, hopefully future versions. + // If a new executable is shipped with a game update, and the + // start of our pointer chain in the data page moves, we can + // find it again by finding this code pattern and seeing what + // it points to. + // + // 4C 8B 15 ?? ?? ?? ?? mov r10,qword ptr ds:[?? ?? ?? ??] + // 44 8D 4A FF lea r9d,qword ptr ds:[rdx-1] + // 49 63 C1 movsxd rax,r9d + // 49 8B 34 C2 mov rsi,qword ptr ds:[r10+rax*8] + const std::vector< uint8_t > pattern = { + 0x4C, 0x8B, 0x15, '?', '?', '?', '?', /**/ + 0x44, 0x8D, 0x4A, 0xFF, /**/ + 0x49, 0x63, 0xC1, /**/ + 0x49, 0x8B, 0x34, 0xC2, /**/ + }; + + procptr_t addr, ok; + + if (!(addr = proc.findPattern(pattern, iter->second))) { + continue; + } + + if (!(addr = proc.peekRIP(addr + 0x3))) { + continue; + } + + /* Only test that we can read the memory in the address we got. + * + * Normally, the memory at that address is another pointer to + * the start of our pointer chain. But, if we're at the main + * menu and haven't loaded in to any world yet, the pointer at + * this address will be null. We should be able to read this + * memory early on, but expect to read out a null pointer if we + * haven't loaded into the world yet. */ + + if (!(proc.peek(addr, ok))) { + continue; + } + + handle = std::make_unique< GroundedHandle >(std::move(proc), addr); + + return MUMBLE_PDEC_OK; + } + + return MUMBLE_PDEC_ERROR_TEMP; +} + +void mumble_shutdownPositionalData() { + handle.reset(); +} + +enum FollowChain { + CHAIN_OK = 0, + CHAIN_LATER, + CHAIN_BAD, +}; + +enum FollowChain followPointerChain(const ProcessWindows &proc, const procptr_t start, GroundedCam &cam) { + procptr_t chain; + + /* If we can't read the starting address, the program probably quit; + * we should shut down our positional audio */ + if (!proc.peek< procptr_t >(start, chain)) { + return CHAIN_BAD; + } + + if (chain == 0) { + /* Probably at the main menu. We expect this to point to a + * valid address later. */ + return CHAIN_LATER; + } + + if (!(chain = proc.peekPtr(chain))) { + return CHAIN_BAD; + } + + if (!(chain = proc.peekPtr(chain + 0x8))) { + return CHAIN_BAD; + } + + if (!proc.peek(chain + 0x700, cam)) { + return CHAIN_BAD; + } + + return CHAIN_OK; +} + +bool mumble_fetchPositionalData(float *avatarPos, float *avatarDir, float *avatarAxis, float *cameraPos, + float *cameraDir, float *cameraAxis, const char **contextPtr, + const char **identityPtr) { + *contextPtr = ""; + *identityPtr = ""; + + const ProcessWindows &proc = std::get< 0 >(*handle); + const procptr_t start = std::get< 1 >(*handle); + GroundedCam cam; + auto result = followPointerChain(proc, start, cam); + + switch (result) { + case CHAIN_OK: + /* We expect top and front to be unit vectors in the game. */ + assert(float3_is_unit(cam.top)); + assert(float3_is_unit(cam.front)); + + avatarAxis[0] = cameraAxis[0] = -cam.top[0]; + avatarAxis[1] = cameraAxis[1] = cam.top[2]; + avatarAxis[2] = cameraAxis[2] = -cam.top[1]; + + avatarDir[0] = cameraDir[0] = -cam.front[0]; + avatarDir[1] = cameraDir[1] = cam.front[2]; + avatarDir[2] = cameraDir[2] = -cam.front[1]; + + avatarPos[0] = cameraPos[0] = unreal_to_mumble_units(cam.pos[0]); + avatarPos[1] = cameraPos[1] = unreal_to_mumble_units(cam.pos[2]); + avatarPos[2] = cameraPos[2] = unreal_to_mumble_units(cam.pos[1]); + return true; + + default: + std::fill_n(avatarPos, 3, 0.f); + std::fill_n(avatarDir, 3, 0.f); + std::fill_n(avatarAxis, 3, 0.f); + + std::fill_n(cameraPos, 3, 0.f); + std::fill_n(cameraDir, 3, 0.f); + std::fill_n(cameraAxis, 3, 0.f); + + return result == CHAIN_LATER; + } +}