// Copyright 2020-2021 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 "mumble_plugin.h" #include "mumble_plugin_utils.h" #ifdef OS_LINUX # include "ProcessLinux.h" #endif #include "ProcessWindows.h" #include #include std::unique_ptr< Process > proc; static bool isWin32 = false; #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 = Process::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; }