349 lines
8.8 KiB
C++
349 lines
8.8 KiB
C++
// Copyright 2020-2023 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>.
|
|
|
|
#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
|
|
#include "mumble_legacy_plugin.h"
|
|
|
|
#include "mumble_positional_audio_utils.h"
|
|
|
|
#ifdef OS_LINUX
|
|
# include "ProcessLinux.h"
|
|
#endif
|
|
#include "ProcessWindows.h"
|
|
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <sstream>
|
|
|
|
std::unique_ptr< ProcessBase > proc;
|
|
|
|
#ifdef OS_WINDOWS
|
|
static constexpr bool isWin32 = true;
|
|
#else
|
|
static bool isWin32 = false;
|
|
#endif
|
|
|
|
#include "client.h"
|
|
#include "common.h"
|
|
#include "engine.h"
|
|
|
|
static inline void anglesToVectors(const float (&angles)[3], float *front, float *top) {
|
|
// Calculate pitch
|
|
float pitchSin, pitchCos;
|
|
sinCos(degreesToRadians(angles[0]), pitchSin, pitchCos);
|
|
|
|
// Calculate yaw
|
|
float yawSin, yawCos;
|
|
sinCos(degreesToRadians(angles[1]), yawSin, yawCos);
|
|
|
|
// Calculate roll
|
|
float rollSin, rollCos;
|
|
sinCos(degreesToRadians(angles[2]), rollSin, rollCos);
|
|
|
|
// Calculate front vector
|
|
front[0] = pitchCos * yawCos;
|
|
front[1] = -pitchSin;
|
|
front[2] = pitchCos * yawSin;
|
|
|
|
// Calculate top vector
|
|
top[0] = (rollCos * pitchSin * yawCos + -rollSin * -yawSin);
|
|
top[1] = rollCos * pitchCos;
|
|
top[2] = (rollCos * pitchSin * yawSin + -rollSin * yawCos);
|
|
}
|
|
|
|
procptr_t localClient, localPlayer;
|
|
int32_t signOnStateOffset, deadFlagOffset, rotationOffset, originPositionOffset, eyesPositionOffsetOffset,
|
|
netInfoOffset, levelNameOffset;
|
|
|
|
static int fetch(float *avatarPos, float *avatarFront, float *avatarTop, float *cameraPos, float *cameraFront,
|
|
float *cameraTop, std::string &context, std::wstring &identity) {
|
|
const auto signOnState = proc->peek< uint32_t >(localClient + signOnStateOffset);
|
|
|
|
// 0: SIGNONSTATE_NONE
|
|
// 1: SIGNONSTATE_CHALLENGE
|
|
// 2: SIGNONSTATE_CONNECTED
|
|
// 3: SIGNONSTATE_NEW
|
|
// 4: SIGNONSTATE_PRESPAWN
|
|
// 5: SIGNONSTATE_SPAWN
|
|
// 6: SIGNONSTATE_FULL
|
|
// 7: SIGNONSTATE_CHANGELEVEL
|
|
if (signOnState != 6) {
|
|
return false;
|
|
}
|
|
|
|
const auto isPlayerDead = proc->peek< bool >(localPlayer + deadFlagOffset);
|
|
if (isPlayerDead) {
|
|
context.clear();
|
|
identity.clear();
|
|
|
|
for (uint8_t i = 0; i < 3; ++i) {
|
|
avatarPos[i] = avatarFront[i] = avatarTop[i] = cameraPos[i] = cameraFront[i] = cameraTop[i] = 0.0f;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
float rotation[3];
|
|
if (!proc->peek(localPlayer + rotationOffset, rotation, sizeof(rotation))) {
|
|
return false;
|
|
}
|
|
|
|
float originPosition[3];
|
|
if (!proc->peek(localPlayer + originPositionOffset, originPosition, sizeof(originPosition))) {
|
|
return false;
|
|
}
|
|
|
|
float eyesPositionOffset[3];
|
|
if (!proc->peek(localPlayer + eyesPositionOffsetOffset, eyesPositionOffset, sizeof(eyesPositionOffset))) {
|
|
return false;
|
|
}
|
|
|
|
NetInfo ni;
|
|
if (!proc->peek(localClient + netInfoOffset, ni)) {
|
|
return false;
|
|
}
|
|
|
|
std::string host;
|
|
|
|
// 0: NA_NULL
|
|
// 1: NA_LOOPBACK
|
|
// 2: NA_BROADCAST
|
|
// 3: NA_IP
|
|
switch (ni.type) {
|
|
case 3: {
|
|
std::ostringstream ss;
|
|
for (uint8_t i = 0; i < 4; ++i) {
|
|
ss << std::to_string(ni.ip[i]);
|
|
if (i < 3) {
|
|
ss << ".";
|
|
}
|
|
}
|
|
|
|
ss << ":" << networkToHost(ni.port);
|
|
host = ss.str();
|
|
break;
|
|
}
|
|
default:
|
|
return true;
|
|
}
|
|
|
|
// 40 is the size of the char array in the engine's code
|
|
char map[40];
|
|
if (!proc->peek(localClient + levelNameOffset, map, sizeof(map))) {
|
|
map[0] = '\0';
|
|
}
|
|
|
|
// Begin context
|
|
std::ostringstream ocontext;
|
|
ocontext << " {\"Host\": \"" << host << "\"}"; // Set context with server's IP address and port.
|
|
context = ocontext.str();
|
|
// End context
|
|
|
|
// Begin identity
|
|
std::wostringstream oidentity;
|
|
oidentity << "{";
|
|
|
|
// Map
|
|
escape(map, sizeof(map));
|
|
if (strcmp(map, "") != 0) {
|
|
oidentity << std::endl << "\"Map\": \"" << map << "\""; // Set map name in identity.
|
|
} else {
|
|
oidentity << std::endl << "\"Map\": null";
|
|
}
|
|
|
|
oidentity << std::endl << "}";
|
|
identity = oidentity.str();
|
|
// End identity
|
|
|
|
// Mumble | Game
|
|
// X | X
|
|
// Y | Z
|
|
// Z | Y
|
|
//
|
|
// originPosition corresponds to the origin of the player model,
|
|
// usually the point (i.e. feet) where it touches the ground.
|
|
// eyesPositionOffset is the difference between the eyes and the origin position.
|
|
// We use the eyes' position because they are closer to the mouth.
|
|
avatarPos[0] = originPosition[0] + eyesPositionOffset[0];
|
|
avatarPos[1] = originPosition[2] + eyesPositionOffset[2];
|
|
avatarPos[2] = originPosition[1] + eyesPositionOffset[1];
|
|
|
|
anglesToVectors(rotation, avatarFront, avatarTop);
|
|
|
|
// Sync camera vectors with avatar ones.
|
|
//
|
|
// This is not ideal, but it's not an issue unless 3rd person mode is enabled.
|
|
// The command to set it is "thirdperson" and requires "sv_cheats" to be enabled.
|
|
// For reference, the functions that return what we need are MainViewOrigin() and MainViewAngles().
|
|
for (uint8_t i = 0; i < 3; ++i) {
|
|
cameraPos[i] = avatarPos[i] /= 39.37f; // Convert from inches to meters.
|
|
cameraFront[i] = avatarFront[i];
|
|
cameraTop[i] = avatarTop[i];
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool tryInit(const std::multimap< std::wstring, unsigned long long int > &pids) {
|
|
const std::vector< std::string > names{
|
|
#ifdef OS_LINUX
|
|
"hl2_linux",
|
|
#endif
|
|
"left4dead2.exe", "left4dead.exe"
|
|
};
|
|
|
|
for (const auto &name : names) {
|
|
const auto id = ProcessBase::find(name, pids);
|
|
if (!id) {
|
|
continue;
|
|
}
|
|
#ifdef OS_WINDOWS
|
|
proc.reset(new ProcessWindows(id, name));
|
|
#else
|
|
isWin32 = HostLinux::isWine(id);
|
|
if (isWin32) {
|
|
proc.reset(new ProcessWindows(id, name));
|
|
} else {
|
|
proc.reset(new ProcessLinux(id, name));
|
|
}
|
|
#endif
|
|
if (proc->isOk()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static int tryLock(const std::multimap< std::wstring, unsigned long long int > &pids) {
|
|
if (!tryInit(pids)) {
|
|
return false;
|
|
}
|
|
|
|
const auto modules = proc->modules();
|
|
|
|
auto iter = modules.find(isWin32 ? "engine.dll" : "engine.so");
|
|
if (iter == modules.cend()) {
|
|
return false;
|
|
}
|
|
|
|
const auto engine = iter->second.baseAddress();
|
|
if (!engine) {
|
|
return false;
|
|
}
|
|
|
|
iter = modules.find(isWin32 ? "client.dll" : "client.so");
|
|
if (iter == modules.cend()) {
|
|
return false;
|
|
}
|
|
|
|
const auto client = iter->second.baseAddress();
|
|
if (!client) {
|
|
return false;
|
|
}
|
|
|
|
const auto engineInterfaces = getInterfaces(engine);
|
|
|
|
const auto engineClient = getInterfaceAddress("VEngineClient013", engineInterfaces);
|
|
if (!engineClient) {
|
|
return false;
|
|
}
|
|
|
|
localClient = getLocalClient(engineClient);
|
|
if (!localClient) {
|
|
return false;
|
|
}
|
|
|
|
signOnStateOffset = getSignOnStateOffset(engineClient);
|
|
if (!signOnStateOffset) {
|
|
return false;
|
|
}
|
|
|
|
const auto signOnState = proc->peek< uint32_t >(localClient + signOnStateOffset);
|
|
if (signOnState != 6) {
|
|
return false;
|
|
}
|
|
|
|
const auto clientInterfaces = getInterfaces(client);
|
|
|
|
const auto clientEntityList = getInterfaceAddress("VClientEntityList003", clientInterfaces);
|
|
if (!clientEntityList) {
|
|
return false;
|
|
}
|
|
|
|
localPlayer = getLocalPlayer(localClient, clientEntityList, engineClient);
|
|
if (!localPlayer) {
|
|
return false;
|
|
}
|
|
|
|
// "pl" is the offset to the internal player class.
|
|
const auto pl = getDataVarFromEntity("pl", localPlayer);
|
|
if (!pl) {
|
|
return false;
|
|
}
|
|
|
|
// "deadflag" is the offset relative to "pl".
|
|
//
|
|
// For some reason it's missing from Left 4 Dead 2 on Linux, along with many other datavars we don't need.
|
|
// The offset (0x4) has never changed across the various engine versions released during the years (10+), thus we
|
|
// force it.
|
|
const auto deadflag = getDataVarFromEntity("deadflag", localPlayer);
|
|
deadFlagOffset = pl + (deadflag ? deadflag : 0x4);
|
|
|
|
originPositionOffset = getDataVarFromEntity("m_vecAbsOrigin", localPlayer);
|
|
if (!originPositionOffset) {
|
|
return false;
|
|
}
|
|
|
|
eyesPositionOffsetOffset = getDataVarFromEntity("m_vecViewOffset", localPlayer);
|
|
if (!eyesPositionOffsetOffset) {
|
|
return false;
|
|
}
|
|
|
|
rotationOffset = getDataVarFromEntity("m_angAbsRotation", localPlayer);
|
|
if (!rotationOffset) {
|
|
return false;
|
|
}
|
|
|
|
netInfoOffset = getNetInfoOffset(localClient, engineClient);
|
|
if (!netInfoOffset) {
|
|
return false;
|
|
}
|
|
|
|
levelNameOffset = getLevelNameOffset(engineClient);
|
|
if (!levelNameOffset) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static const std::wstring longDesc() {
|
|
return std::wstring(L"Supports Source Engine games with context and identity support.");
|
|
}
|
|
|
|
static std::wstring description(L"Source Engine");
|
|
static std::wstring shortName(L"Source Engine");
|
|
|
|
static int tryLock1() {
|
|
return tryLock(std::multimap< std::wstring, unsigned long long int >());
|
|
}
|
|
|
|
static void nullUnlock() {
|
|
}
|
|
|
|
static MumblePlugin sePlug = { MUMBLE_PLUGIN_MAGIC, description, shortName, nullptr, nullptr, tryLock1,
|
|
nullUnlock, longDesc, fetch };
|
|
|
|
static MumblePlugin2 sePlug2 = { MUMBLE_PLUGIN_MAGIC_2, MUMBLE_PLUGIN_VERSION, tryLock };
|
|
|
|
extern "C" MUMBLE_PLUGIN_EXPORT MumblePlugin *getMumblePlugin() {
|
|
return &sePlug;
|
|
}
|
|
|
|
extern "C" MUMBLE_PLUGIN_EXPORT MumblePlugin2 *getMumblePlugin2() {
|
|
return &sePlug2;
|
|
}
|