Skip to content

Commit

Permalink
FEAT(positional-audio): plugin for Grounded
Browse files Browse the repository at this point in the history
Positional audio support for the Steam release of Grounded.

Tested on 1.4.3.4578, 1.4.4.4634, and 1.4.5.4679.
  • Loading branch information
sqwishy committed Oct 3, 2024
1 parent 2b64b2c commit e24addd
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 0 deletions.
1 change: 1 addition & 0 deletions plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ if(WIN32 OR (UNIX AND CMAKE_SYSTEM_NAME STREQUAL "Linux"))
"ffxiv"
"ffxiv_x64"
"gmod"
"grounded"
"gtaiv"
"gtasa"
"gtav"
Expand Down
4 changes: 4 additions & 0 deletions plugins/HostWindows.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions plugins/HostWindows.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class HostWindows {
Modules modules() const;

HostWindows(const procid_t pid);
HostWindows(const HostWindows &) = delete;
HostWindows(HostWindows &&other) noexcept;
virtual ~HostWindows();
};

Expand Down
1 change: 1 addition & 0 deletions plugins/ProcessBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
};

Expand Down
1 change: 1 addition & 0 deletions plugins/ProcessWindows.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
};

Expand Down
17 changes: 17 additions & 0 deletions plugins/grounded/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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 <https://www.mumble.info/LICENSE>.

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()
262 changes: 262 additions & 0 deletions plugins/grounded/grounded.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
// 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 <https://www.mumble.info/LICENSE>.

#include "ProcessWindows.h"
#include "MumblePlugin.h"

#include "mumble_positional_audio_utils.h"

#include <cassert>
#include <cstring>

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;
}
}

0 comments on commit e24addd

Please sign in to comment.