diff options
author | Jack <66967891+ASpoonPlaysGames@users.noreply.github.com> | 2023-12-27 00:32:01 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-27 01:32:01 +0100 |
commit | f5ab6fb5e8be7b73e6003d4145081d5e0c0ce287 (patch) | |
tree | 90f2c6a4885dbd181799e2325cf33588697674e1 /primedev/client | |
parent | bb8ed59f6891b1196c5f5bbe7346cd171c8215fa (diff) | |
download | NorthstarLauncher-1.21.2-rc3.tar.gz NorthstarLauncher-1.21.2-rc3.zip |
Folder restructuring from primedev (#624)v1.21.2-rc3v1.21.2
Copies of over the primedev folder structure for easier cherry-picking of further changes
Co-authored-by: F1F7Y <filip.bartos07@proton.me>
Diffstat (limited to 'primedev/client')
-rw-r--r-- | primedev/client/audio.cpp | 504 | ||||
-rw-r--r-- | primedev/client/audio.h | 46 | ||||
-rw-r--r-- | primedev/client/chatcommand.cpp | 36 | ||||
-rw-r--r-- | primedev/client/clientauthhooks.cpp | 72 | ||||
-rw-r--r-- | primedev/client/clientruihooks.cpp | 23 | ||||
-rw-r--r-- | primedev/client/clientvideooverrides.cpp | 41 | ||||
-rw-r--r-- | primedev/client/debugoverlay.cpp | 348 | ||||
-rw-r--r-- | primedev/client/demofixes.cpp | 25 | ||||
-rw-r--r-- | primedev/client/diskvmtfixes.cpp | 15 | ||||
-rw-r--r-- | primedev/client/languagehooks.cpp | 115 | ||||
-rw-r--r-- | primedev/client/latencyflex.cpp | 43 | ||||
-rw-r--r-- | primedev/client/localchatwriter.cpp | 449 | ||||
-rw-r--r-- | primedev/client/localchatwriter.h | 64 | ||||
-rw-r--r-- | primedev/client/modlocalisation.cpp | 55 | ||||
-rw-r--r-- | primedev/client/r2client.cpp | 13 | ||||
-rw-r--r-- | primedev/client/r2client.h | 7 | ||||
-rw-r--r-- | primedev/client/rejectconnectionfixes.cpp | 34 |
17 files changed, 1890 insertions, 0 deletions
diff --git a/primedev/client/audio.cpp b/primedev/client/audio.cpp new file mode 100644 index 00000000..aa32e390 --- /dev/null +++ b/primedev/client/audio.cpp @@ -0,0 +1,504 @@ +#include "audio.h" +#include "dedicated/dedicated.h" +#include "core/convar/convar.h" + +#include "rapidjson/error/en.h" +#include <fstream> +#include <iostream> +#include <sstream> +#include <random> + +AUTOHOOK_INIT() + +static const char* pszAudioEventName; + +ConVar* Cvar_mileslog_enable; +ConVar* Cvar_ns_print_played_sounds; + +CustomAudioManager g_CustomAudioManager; + +EventOverrideData::EventOverrideData() +{ + spdlog::warn("Initialised struct EventOverrideData without any data!"); + LoadedSuccessfully = false; +} + +// Empty stereo 48000 WAVE file +unsigned char EMPTY_WAVE[45] = {0x52, 0x49, 0x46, 0x46, 0x25, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6D, 0x74, + 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x44, 0xAC, 0x00, 0x00, 0x88, 0x58, + 0x01, 0x00, 0x02, 0x00, 0x10, 0x00, 0x64, 0x61, 0x74, 0x61, 0x74, 0x00, 0x00, 0x00, 0x00}; + +EventOverrideData::EventOverrideData(const std::string& data, const fs::path& path) +{ + if (data.length() <= 0) + { + spdlog::error("Failed reading audio override file {}: file is empty", path.string()); + return; + } + + fs::path samplesFolder = path; + samplesFolder = samplesFolder.replace_extension(); + + if (!fs::exists(samplesFolder)) + { + spdlog::error( + "Failed reading audio override file {}: samples folder doesn't exist; should be named the same as the definition file without " + "JSON extension.", + path.string()); + return; + } + + rapidjson_document dataJson; + dataJson.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>(data); + + // fail if parse error + if (dataJson.HasParseError()) + { + spdlog::error( + "Failed reading audio override file {}: encountered parse error \"{}\" at offset {}", + path.string(), + GetParseError_En(dataJson.GetParseError()), + dataJson.GetErrorOffset()); + return; + } + + // fail if it's not a json obj (could be an array, string, etc) + if (!dataJson.IsObject()) + { + spdlog::error("Failed reading audio override file {}: file is not a JSON object", path.string()); + return; + } + + // fail if no event ids given + if (!dataJson.HasMember("EventId")) + { + spdlog::error("Failed reading audio override file {}: JSON object does not have the EventId property", path.string()); + return; + } + + // array of event ids + if (dataJson["EventId"].IsArray()) + { + for (auto& eventId : dataJson["EventId"].GetArray()) + { + if (!eventId.IsString()) + { + spdlog::error( + "Failed reading audio override file {}: EventId array has a value of invalid type, all must be strings", path.string()); + return; + } + + EventIds.push_back(eventId.GetString()); + } + } + // singular event id + else if (dataJson["EventId"].IsString()) + { + EventIds.push_back(dataJson["EventId"].GetString()); + } + // incorrect type + else + { + spdlog::error( + "Failed reading audio override file {}: EventId property is of invalid type (must be a string or an array of strings)", + path.string()); + return; + } + + if (dataJson.HasMember("EventIdRegex")) + { + // array of event id regex + if (dataJson["EventIdRegex"].IsArray()) + { + for (auto& eventId : dataJson["EventIdRegex"].GetArray()) + { + if (!eventId.IsString()) + { + spdlog::error( + "Failed reading audio override file {}: EventIdRegex array has a value of invalid type, all must be strings", + path.string()); + return; + } + + const std::string& regex = eventId.GetString(); + + try + { + EventIdsRegex.push_back({regex, std::regex(regex)}); + } + catch (...) + { + spdlog::error("Malformed regex \"{}\" in audio override file {}", regex, path.string()); + return; + } + } + } + // singular event id regex + else if (dataJson["EventIdRegex"].IsString()) + { + const std::string& regex = dataJson["EventIdRegex"].GetString(); + try + { + EventIdsRegex.push_back({regex, std::regex(regex)}); + } + catch (...) + { + spdlog::error("Malformed regex \"{}\" in audio override file {}", regex, path.string()); + return; + } + } + // incorrect type + else + { + spdlog::error( + "Failed reading audio override file {}: EventIdRegex property is of invalid type (must be a string or an array of strings)", + path.string()); + return; + } + } + + if (dataJson.HasMember("AudioSelectionStrategy")) + { + if (!dataJson["AudioSelectionStrategy"].IsString()) + { + spdlog::error("Failed reading audio override file {}: AudioSelectionStrategy property must be a string", path.string()); + return; + } + + std::string strategy = dataJson["AudioSelectionStrategy"].GetString(); + + if (strategy == "sequential") + { + Strategy = AudioSelectionStrategy::SEQUENTIAL; + } + else if (strategy == "random") + { + Strategy = AudioSelectionStrategy::RANDOM; + } + else + { + spdlog::error( + "Failed reading audio override file {}: AudioSelectionStrategy string must be either \"sequential\" or \"random\"", + path.string()); + return; + } + } + + // load samples + for (fs::directory_entry file : fs::recursive_directory_iterator(samplesFolder)) + { + if (file.is_regular_file() && file.path().extension().string() == ".wav") + { + std::string pathString = file.path().string(); + + // Open the file. + std::ifstream wavStream(pathString, std::ios::binary); + + if (wavStream.fail()) + { + spdlog::error("Failed reading audio sample {}", file.path().string()); + continue; + } + + // Get file size. + wavStream.seekg(0, std::ios::end); + size_t fileSize = wavStream.tellg(); + wavStream.close(); + + // Allocate enough memory for the file. + // blank out the memory for now, then read it later + uint8_t* data = new uint8_t[fileSize]; + memcpy(data, EMPTY_WAVE, sizeof(EMPTY_WAVE)); + Samples.push_back({fileSize, std::unique_ptr<uint8_t[]>(data)}); + + // thread off the file read + // should we spawn one thread per read? or should there be a cap to the number of reads at once? + std::thread readThread( + [pathString, fileSize, data] + { + std::shared_lock lock(g_CustomAudioManager.m_loadingMutex); + std::ifstream wavStream(pathString, std::ios::binary); + + // would be weird if this got hit, since it would've worked previously + if (wavStream.fail()) + { + spdlog::error("Failed async read of audio sample {}", pathString); + return; + } + + // read from after the header first to preserve the empty header, then read the header last + wavStream.seekg(0, std::ios::beg); + wavStream.read(reinterpret_cast<char*>(data), fileSize); + wavStream.close(); + + spdlog::info("Finished async read of audio sample {}", pathString); + }); + + readThread.detach(); + } + } + + /* + if (dataJson.HasMember("EnableOnLoopedSounds")) + { + if (!dataJson["EnableOnLoopedSounds"].IsBool()) + { + spdlog::error("Failed reading audio override file {}: EnableOnLoopedSounds property is of invalid type (must be a bool)", + path.string()); return; + } + + EnableOnLoopedSounds = dataJson["EnableOnLoopedSounds"].GetBool(); + } + */ + + if (Samples.size() == 0) + spdlog::warn("Audio override {} has no valid samples! Sounds will not play for this event.", path.string()); + + spdlog::info("Loaded audio override file {}", path.string()); + + LoadedSuccessfully = true; +} + +bool CustomAudioManager::TryLoadAudioOverride(const fs::path& defPath) +{ + if (IsDedicatedServer()) + return true; // silently fail + + std::ifstream jsonStream(defPath); + std::stringstream jsonStringStream; + + // fail if no audio json + if (jsonStream.fail()) + { + spdlog::warn("Unable to read audio override from file {}", defPath.string()); + return false; + } + + while (jsonStream.peek() != EOF) + jsonStringStream << (char)jsonStream.get(); + + jsonStream.close(); + + std::shared_ptr<EventOverrideData> data = std::make_shared<EventOverrideData>(jsonStringStream.str(), defPath); + + if (!data->LoadedSuccessfully) + return false; // no logging, the constructor has probably already logged + + for (const std::string& eventId : data->EventIds) + { + spdlog::info("Registering sound event {}", eventId); + m_loadedAudioOverrides.insert({eventId, data}); + } + + for (const auto& eventIdRegexData : data->EventIdsRegex) + { + spdlog::info("Registering sound event regex {}", eventIdRegexData.first); + m_loadedAudioOverridesRegex.insert({eventIdRegexData.first, data}); + } + + return true; +} + +typedef void (*MilesStopAll_Type)(); +MilesStopAll_Type MilesStopAll; + +void CustomAudioManager::ClearAudioOverrides() +{ + if (IsDedicatedServer()) + return; + + if (m_loadedAudioOverrides.size() > 0 || m_loadedAudioOverridesRegex.size() > 0) + { + // stop all miles sounds beforehand + // miles_stop_all + + MilesStopAll(); + + // this is cancer but it works + Sleep(50); + } + + // slightly (very) bad + // wait for all audio reads to complete so we don't kill preexisting audio buffers as we're writing to them + std::unique_lock lock(m_loadingMutex); + + m_loadedAudioOverrides.clear(); + m_loadedAudioOverridesRegex.clear(); +} + +template <typename Iter, typename RandomGenerator> Iter select_randomly(Iter start, Iter end, RandomGenerator& g) +{ + std::uniform_int_distribution<> dis(0, std::distance(start, end) - 1); + std::advance(start, dis(g)); + return start; +} + +template <typename Iter> Iter select_randomly(Iter start, Iter end) +{ + static std::random_device rd; + static std::mt19937 gen(rd()); + return select_randomly(start, end, gen); +} + +bool ShouldPlayAudioEvent(const char* eventName, const std::shared_ptr<EventOverrideData>& data) +{ + std::string eventNameString = eventName; + std::string eventNameStringBlacklistEntry = ("!" + eventNameString); + + for (const std::string& name : data->EventIds) + { + if (name == eventNameStringBlacklistEntry) + return false; // event blacklisted + + if (name == "*") + { + // check for bad sounds I guess? + // really feel like this should be an option but whatever + if (!!strstr(eventName, "_amb_") || !!strstr(eventName, "_emit_") || !!strstr(eventName, "amb_")) + return false; // would play static noise, I hate this + } + } + + return true; // good to go +} + +// clang-format off +AUTOHOOK(LoadSampleMetadata, mileswin64.dll + 0xF110, +bool, __fastcall, (void* sample, void* audioBuffer, unsigned int audioBufferLength, int audioType)) +// clang-format on +{ + // Raw source, used for voice data only + if (audioType == 0) + return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType); + + const char* eventName = pszAudioEventName; + + if (Cvar_ns_print_played_sounds->GetInt() > 0) + spdlog::info("[AUDIO] Playing event {}", eventName); + + auto iter = g_CustomAudioManager.m_loadedAudioOverrides.find(eventName); + std::shared_ptr<EventOverrideData> overrideData; + + if (iter == g_CustomAudioManager.m_loadedAudioOverrides.end()) + { + // override for that specific event not found, try wildcard + iter = g_CustomAudioManager.m_loadedAudioOverrides.find("*"); + + if (iter == g_CustomAudioManager.m_loadedAudioOverrides.end()) + { + // not found + + // try regex + for (const auto& item : g_CustomAudioManager.m_loadedAudioOverridesRegex) + for (const auto& regexData : item.second->EventIdsRegex) + if (std::regex_search(eventName, regexData.second)) + overrideData = item.second; + + if (!overrideData) + // not found either + return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType); + else + { + // cache found pattern to improve performance + g_CustomAudioManager.m_loadedAudioOverrides[eventName] = overrideData; + } + } + else + overrideData = iter->second; + } + else + overrideData = iter->second; + + if (!ShouldPlayAudioEvent(eventName, overrideData)) + return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType); + + void* data = 0; + unsigned int dataLength = 0; + + if (overrideData->Samples.size() == 0) + { + // 0 samples, turn off this particular event. + + // using a dummy empty wave file + data = EMPTY_WAVE; + dataLength = sizeof(EMPTY_WAVE); + } + else + { + std::pair<size_t, std::unique_ptr<uint8_t[]>>* dat = NULL; + + switch (overrideData->Strategy) + { + case AudioSelectionStrategy::RANDOM: + dat = &*select_randomly(overrideData->Samples.begin(), overrideData->Samples.end()); + break; + case AudioSelectionStrategy::SEQUENTIAL: + default: + dat = &overrideData->Samples[overrideData->CurrentIndex++]; + if (overrideData->CurrentIndex >= overrideData->Samples.size()) + overrideData->CurrentIndex = 0; // reset back to the first sample entry + break; + } + + if (!dat) + spdlog::warn("Could not get sample data from override struct for event {}! Shouldn't happen", eventName); + else + { + data = dat->second.get(); + dataLength = dat->first; + } + } + + if (!data) + { + spdlog::warn("Could not fetch override sample data for event {}! Using original data instead.", eventName); + return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType); + } + + audioBuffer = data; + audioBufferLength = dataLength; + + // most important change: set the sample class buffer so that the correct audio plays + *(void**)((uintptr_t)sample + 0xE8) = audioBuffer; + *(unsigned int*)((uintptr_t)sample + 0xF0) = audioBufferLength; + + // 64 - Auto-detect sample type + bool res = LoadSampleMetadata(sample, audioBuffer, audioBufferLength, 64); + if (!res) + spdlog::error("LoadSampleMetadata failed! The game will crash :("); + + return res; +} + +// clang-format off +AUTOHOOK(sub_1800294C0, mileswin64.dll + 0x294C0, +void*, __fastcall, (void* a1, void* a2)) +// clang-format on +{ + pszAudioEventName = reinterpret_cast<const char*>((*((__int64*)a2 + 6))); + return sub_1800294C0(a1, a2); +} + +// clang-format off +AUTOHOOK(MilesLog, client.dll + 0x57DAD0, +void, __fastcall, (int level, const char* string)) +// clang-format on +{ + if (!Cvar_mileslog_enable->GetBool()) + return; + + spdlog::info("[MSS] {} - {}", level, string); +} + +ON_DLL_LOAD_RELIESON("engine.dll", MilesLogFuncHooks, ConVar, (CModule module)) +{ + Cvar_mileslog_enable = new ConVar("mileslog_enable", "0", FCVAR_NONE, "Enables/disables whether the mileslog func should be logged"); +} + +ON_DLL_LOAD_CLIENT_RELIESON("client.dll", AudioHooks, ConVar, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + Cvar_ns_print_played_sounds = new ConVar("ns_print_played_sounds", "0", FCVAR_NONE, ""); + MilesStopAll = module.Offset(0x580850).RCast<MilesStopAll_Type>(); +} diff --git a/primedev/client/audio.h b/primedev/client/audio.h new file mode 100644 index 00000000..15fd1a35 --- /dev/null +++ b/primedev/client/audio.h @@ -0,0 +1,46 @@ +#pragma once + +#include <vector> +#include <filesystem> +#include <regex> +#include <shared_mutex> + +enum class AudioSelectionStrategy +{ + INVALID = -1, + SEQUENTIAL, + RANDOM +}; + +class EventOverrideData +{ +public: + EventOverrideData(const std::string&, const fs::path&); + EventOverrideData(); + +public: + bool LoadedSuccessfully = false; + + std::vector<std::string> EventIds = {}; + std::vector<std::pair<std::string, std::regex>> EventIdsRegex = {}; + + std::vector<std::pair<size_t, std::unique_ptr<uint8_t[]>>> Samples = {}; + + AudioSelectionStrategy Strategy = AudioSelectionStrategy::SEQUENTIAL; + size_t CurrentIndex = 0; + + bool EnableOnLoopedSounds = false; +}; + +class CustomAudioManager +{ +public: + bool TryLoadAudioOverride(const fs::path&); + void ClearAudioOverrides(); + + std::shared_mutex m_loadingMutex; + std::unordered_map<std::string, std::shared_ptr<EventOverrideData>> m_loadedAudioOverrides = {}; + std::unordered_map<std::string, std::shared_ptr<EventOverrideData>> m_loadedAudioOverridesRegex = {}; +}; + +extern CustomAudioManager g_CustomAudioManager; diff --git a/primedev/client/chatcommand.cpp b/primedev/client/chatcommand.cpp new file mode 100644 index 00000000..9cf34e43 --- /dev/null +++ b/primedev/client/chatcommand.cpp @@ -0,0 +1,36 @@ +#include "core/convar/convar.h" +#include "core/convar/concommand.h" +#include "localchatwriter.h" + +// note: isIngameChat is an int64 because the whole register the arg is stored in needs to be 0'd out to work +// if isIngameChat is false, we use network chat instead +void(__fastcall* ClientSayText)(void* a1, const char* message, uint64_t isIngameChat, bool isTeamChat); + +void ConCommand_say(const CCommand& args) +{ + if (args.ArgC() >= 2) + ClientSayText(nullptr, args.ArgS(), true, false); +} + +void ConCommand_say_team(const CCommand& args) +{ + if (args.ArgC() >= 2) + ClientSayText(nullptr, args.ArgS(), true, true); +} + +void ConCommand_log(const CCommand& args) +{ + if (args.ArgC() >= 2) + { + LocalChatWriter(LocalChatWriter::GameContext).WriteLine(args.ArgS()); + } +} + +ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", ClientChatCommand, ConCommand, (CModule module)) +{ + ClientSayText = + module.Offset(0x54780).RCast<void(__fastcall*)(void* a1, const char* message, uint64_t isIngameChat, bool isTeamChat)>(); + RegisterConCommand("say", ConCommand_say, "Enters a message in public chat", FCVAR_CLIENTDLL); + RegisterConCommand("say_team", ConCommand_say_team, "Enters a message in team chat", FCVAR_CLIENTDLL); + RegisterConCommand("log", ConCommand_log, "Log a message to the local chat window", FCVAR_CLIENTDLL); +} diff --git a/primedev/client/clientauthhooks.cpp b/primedev/client/clientauthhooks.cpp new file mode 100644 index 00000000..35ae3aa7 --- /dev/null +++ b/primedev/client/clientauthhooks.cpp @@ -0,0 +1,72 @@ +#include "masterserver/masterserver.h" +#include "core/convar/convar.h" +#include "client/r2client.h" +#include "core/vanilla.h" + +AUTOHOOK_INIT() + +ConVar* Cvar_ns_has_agreed_to_send_token; + +// mirrored in script +const int NOT_DECIDED_TO_SEND_TOKEN = 0; +const int AGREED_TO_SEND_TOKEN = 1; +const int DISAGREED_TO_SEND_TOKEN = 2; + +// clang-format off +AUTOHOOK(AuthWithStryder, engine.dll + 0x1843A0, +void, __fastcall, (void* a1)) +// clang-format on +{ + // don't attempt to do Atlas auth if we are in vanilla compatibility mode + // this prevents users from joining untrustworthy servers (unless they use a concommand or something) + if (g_pVanillaCompatibility->GetVanillaCompatibility()) + { + AuthWithStryder(a1); + return; + } + + // game will call this forever, until it gets a valid auth key + // so, we need to manually invalidate our key until we're authed with northstar, then we'll allow game to auth with stryder + if (!g_pMasterServerManager->m_bOriginAuthWithMasterServerDone && Cvar_ns_has_agreed_to_send_token->GetInt() != DISAGREED_TO_SEND_TOKEN) + { + // if player has agreed to send token and we aren't already authing, try to auth + if (Cvar_ns_has_agreed_to_send_token->GetInt() == AGREED_TO_SEND_TOKEN && + !g_pMasterServerManager->m_bOriginAuthWithMasterServerInProgress) + g_pMasterServerManager->AuthenticateOriginWithMasterServer(g_pLocalPlayerUserID, g_pLocalPlayerOriginToken); + + // invalidate key so auth will fail + *g_pLocalPlayerOriginToken = 0; + } + + AuthWithStryder(a1); +} + +char* p3PToken; + +// clang-format off +AUTOHOOK(Auth3PToken, engine.dll + 0x183760, +char*, __fastcall, ()) +// clang-format on +{ + if (!g_pVanillaCompatibility->GetVanillaCompatibility() && g_pMasterServerManager->m_sOwnClientAuthToken[0]) + { + memset(p3PToken, 0x0, 1024); + strcpy(p3PToken, "Protocol 3: Protect the Pilot"); + } + + return Auth3PToken(); +} + +ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", ClientAuthHooks, ConVar, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + p3PToken = module.Offset(0x13979D80).RCast<char*>(); + + // this cvar will save to cfg once initially agreed with + Cvar_ns_has_agreed_to_send_token = new ConVar( + "ns_has_agreed_to_send_token", + "0", + FCVAR_ARCHIVE_PLAYERPROFILE, + "whether the user has agreed to send their origin token to the northstar masterserver"); +} diff --git a/primedev/client/clientruihooks.cpp b/primedev/client/clientruihooks.cpp new file mode 100644 index 00000000..ad50d11a --- /dev/null +++ b/primedev/client/clientruihooks.cpp @@ -0,0 +1,23 @@ +#include "core/convar/convar.h" + +AUTOHOOK_INIT() + +ConVar* Cvar_rui_drawEnable; + +// clang-format off +AUTOHOOK(DrawRUIFunc, engine.dll + 0xFC500, +bool, __fastcall, (void* a1, float* a2)) +// clang-format on +{ + if (!Cvar_rui_drawEnable->GetBool()) + return 0; + + return DrawRUIFunc(a1, a2); +} + +ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", RUI, ConVar, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + Cvar_rui_drawEnable = new ConVar("rui_drawEnable", "1", FCVAR_CLIENTDLL, "Controls whether RUI should be drawn"); +} diff --git a/primedev/client/clientvideooverrides.cpp b/primedev/client/clientvideooverrides.cpp new file mode 100644 index 00000000..d8aa2754 --- /dev/null +++ b/primedev/client/clientvideooverrides.cpp @@ -0,0 +1,41 @@ +#include "mods/modmanager.h" + +AUTOHOOK_INIT() + +// clang-format off +AUTOHOOK_PROCADDRESS(BinkOpen, bink2w64.dll, BinkOpen, +void*, __fastcall, (const char* path, uint32_t flags)) +// clang-format on +{ + std::string filename(fs::path(path).filename().string()); + spdlog::info("BinkOpen {}", filename); + + // figure out which mod is handling the bink + Mod* fileOwner = nullptr; + for (Mod& mod : g_pModManager->m_LoadedMods) + { + if (!mod.m_bEnabled) + continue; + + if (std::find(mod.BinkVideos.begin(), mod.BinkVideos.end(), filename) != mod.BinkVideos.end()) + fileOwner = &mod; + } + + if (fileOwner) + { + // create new path + fs::path binkPath(fileOwner->m_ModDirectory / "media" / filename); + return BinkOpen(binkPath.string().c_str(), flags); + } + else + return BinkOpen(path, flags); +} + +ON_DLL_LOAD_CLIENT("engine.dll", BinkVideo, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + // remove engine check for whether the bik we're trying to load exists in r2/media, as this will fail for biks in mods + // note: the check in engine is actually unnecessary, so it's just useless in practice and we lose nothing by removing it + module.Offset(0x459AD).NOP(6); +} diff --git a/primedev/client/debugoverlay.cpp b/primedev/client/debugoverlay.cpp new file mode 100644 index 00000000..e231054d --- /dev/null +++ b/primedev/client/debugoverlay.cpp @@ -0,0 +1,348 @@ +#include "dedicated/dedicated.h" +#include "core/convar/cvar.h" +#include "core/math/vector.h" + +AUTOHOOK_INIT() + +enum OverlayType_t +{ + OVERLAY_BOX = 0, + OVERLAY_SPHERE, + OVERLAY_LINE, + OVERLAY_SMARTAMMO, + OVERLAY_TRIANGLE, + OVERLAY_SWEPT_BOX, + // [Fifty]: the 2 bellow i did not confirm, rest are good + OVERLAY_BOX2, + OVERLAY_CAPSULE +}; + +struct OverlayBase_t +{ + OverlayBase_t() + { + m_Type = OVERLAY_BOX; + m_nServerCount = -1; + m_nCreationTick = -1; + m_flEndTime = 0.0f; + m_pNextOverlay = NULL; + } + + OverlayType_t m_Type; // What type of overlay is it? + int m_nCreationTick; // Duration -1 means go away after this frame # + int m_nServerCount; // Latch server count, too + float m_flEndTime; // When does this box go away + OverlayBase_t* m_pNextOverlay; + __int64 m_pUnk; +}; + +struct OverlayLine_t : public OverlayBase_t +{ + OverlayLine_t() + { + m_Type = OVERLAY_LINE; + } + + Vector3 origin; + Vector3 dest; + int r; + int g; + int b; + int a; + bool noDepthTest; +}; + +struct OverlayBox_t : public OverlayBase_t +{ + OverlayBox_t() + { + m_Type = OVERLAY_BOX; + } + + Vector3 origin; + Vector3 mins; + Vector3 maxs; + QAngle angles; + int r; + int g; + int b; + int a; +}; + +struct OverlayTriangle_t : public OverlayBase_t +{ + OverlayTriangle_t() + { + m_Type = OVERLAY_TRIANGLE; + } + + Vector3 p1; + Vector3 p2; + Vector3 p3; + int r; + int g; + int b; + int a; + bool noDepthTest; +}; + +struct OverlaySweptBox_t : public OverlayBase_t +{ + OverlaySweptBox_t() + { + m_Type = OVERLAY_SWEPT_BOX; + } + + Vector3 start; + Vector3 end; + Vector3 mins; + Vector3 maxs; + QAngle angles; + int r; + int g; + int b; + int a; +}; + +struct OverlaySphere_t : public OverlayBase_t +{ + OverlaySphere_t() + { + m_Type = OVERLAY_SPHERE; + } + + Vector3 vOrigin; + float flRadius; + int nTheta; + int nPhi; + int r; + int g; + int b; + int a; + bool m_bWireframe; +}; + +typedef bool (*OverlayBase_t__IsDeadType)(OverlayBase_t* a1); +static OverlayBase_t__IsDeadType OverlayBase_t__IsDead; +typedef void (*OverlayBase_t__DestroyOverlayType)(OverlayBase_t* a1); +static OverlayBase_t__DestroyOverlayType OverlayBase_t__DestroyOverlay; + +static ConVar* Cvar_enable_debug_overlays; + +LPCRITICAL_SECTION s_OverlayMutex; + +// Render Line +typedef void (*RenderLineType)(const Vector3& v1, const Vector3& v2, Color c, bool bZBuffer); +static RenderLineType RenderLine; + +// Render box +typedef void (*RenderBoxType)( + const Vector3& vOrigin, const QAngle& angles, const Vector3& vMins, const Vector3& vMaxs, Color c, bool bZBuffer, bool bInsideOut); +static RenderBoxType RenderBox; + +// Render wireframe box +static RenderBoxType RenderWireframeBox; + +// Render swept box +typedef void (*RenderWireframeSweptBoxType)( + const Vector3& vStart, const Vector3& vEnd, const QAngle& angles, const Vector3& vMins, const Vector3& vMaxs, Color c, bool bZBuffer); +RenderWireframeSweptBoxType RenderWireframeSweptBox; + +// Render Triangle +typedef void (*RenderTriangleType)(const Vector3& p1, const Vector3& p2, const Vector3& p3, Color c, bool bZBuffer); +static RenderTriangleType RenderTriangle; + +// Render Axis +typedef void (*RenderAxisType)(const Vector3& vOrigin, float flScale, bool bZBuffer); +static RenderAxisType RenderAxis; + +// I dont know +typedef void (*RenderUnknownType)(const Vector3& vUnk, float flUnk, bool bUnk); +static RenderUnknownType RenderUnknown; + +// Render Sphere +typedef void (*RenderSphereType)(const Vector3& vCenter, float flRadius, int nTheta, int nPhi, Color c, bool bZBuffer); +static RenderSphereType RenderSphere; + +OverlayBase_t** s_pOverlays; + +int* g_nRenderTickCount; +int* g_nOverlayTickCount; + +// clang-format off +AUTOHOOK(DrawOverlay, engine.dll + 0xABCB0, +void, __fastcall, (OverlayBase_t * pOverlay)) +// clang-format on +{ + EnterCriticalSection(s_OverlayMutex); + + switch (pOverlay->m_Type) + { + case OVERLAY_SMARTAMMO: + case OVERLAY_LINE: + { + OverlayLine_t* pLine = static_cast<OverlayLine_t*>(pOverlay); + RenderLine(pLine->origin, pLine->dest, Color(pLine->r, pLine->g, pLine->b, pLine->a), pLine->noDepthTest); + } + break; + case OVERLAY_BOX: + { + OverlayBox_t* pCurrBox = static_cast<OverlayBox_t*>(pOverlay); + if (pCurrBox->a > 0) + { + RenderBox( + pCurrBox->origin, + pCurrBox->angles, + pCurrBox->mins, + pCurrBox->maxs, + Color(pCurrBox->r, pCurrBox->g, pCurrBox->b, pCurrBox->a), + false, + false); + } + if (pCurrBox->a < 255) + { + RenderWireframeBox( + pCurrBox->origin, + pCurrBox->angles, + pCurrBox->mins, + pCurrBox->maxs, + Color(pCurrBox->r, pCurrBox->g, pCurrBox->b, 255), + false, + false); + } + } + break; + case OVERLAY_TRIANGLE: + { + OverlayTriangle_t* pTriangle = static_cast<OverlayTriangle_t*>(pOverlay); + RenderTriangle( + pTriangle->p1, + pTriangle->p2, + pTriangle->p3, + Color(pTriangle->r, pTriangle->g, pTriangle->b, pTriangle->a), + pTriangle->noDepthTest); + } + break; + case OVERLAY_SWEPT_BOX: + { + OverlaySweptBox_t* pBox = static_cast<OverlaySweptBox_t*>(pOverlay); + RenderWireframeSweptBox( + pBox->start, pBox->end, pBox->angles, pBox->mins, pBox->maxs, Color(pBox->r, pBox->g, pBox->b, pBox->a), false); + } + break; + case OVERLAY_SPHERE: + { + OverlaySphere_t* pSphere = static_cast<OverlaySphere_t*>(pOverlay); + RenderSphere( + pSphere->vOrigin, + pSphere->flRadius, + pSphere->nTheta, + pSphere->nPhi, + Color(pSphere->r, pSphere->g, pSphere->b, pSphere->a), + false); + } + break; + default: + { + spdlog::warn("Unimplemented overlay type {}", pOverlay->m_Type); + } + break; + } + + LeaveCriticalSection(s_OverlayMutex); +} + +// clang-format off +AUTOHOOK(DrawAllOverlays, engine.dll + 0xAB780, +void, __fastcall, (bool bRender)) +// clang-format on +{ + EnterCriticalSection(s_OverlayMutex); + + OverlayBase_t* pCurrOverlay = *s_pOverlays; // rbx + OverlayBase_t* pPrevOverlay = nullptr; // rsi + OverlayBase_t* pNextOverlay = nullptr; // rdi + + int m_nCreationTick; // eax + bool bShouldDraw; // zf + int m_pUnk; // eax + + while (pCurrOverlay) + { + if (OverlayBase_t__IsDead(pCurrOverlay)) + { + if (pPrevOverlay) + { + pPrevOverlay->m_pNextOverlay = pCurrOverlay->m_pNextOverlay; + } + else + { + *s_pOverlays = pCurrOverlay->m_pNextOverlay; + } + + pNextOverlay = pCurrOverlay->m_pNextOverlay; + OverlayBase_t__DestroyOverlay(pCurrOverlay); + pCurrOverlay = pNextOverlay; + } + else + { + if (pCurrOverlay->m_nCreationTick == -1) + { + m_pUnk = pCurrOverlay->m_pUnk; + + if (m_pUnk == -1) + { + bShouldDraw = true; + } + else + { + bShouldDraw = m_pUnk == *g_nOverlayTickCount; + } + } + else + { + bShouldDraw = pCurrOverlay->m_nCreationTick == *g_nRenderTickCount; + } + + if (bShouldDraw && bRender && (Cvar_enable_debug_overlays->GetBool() || pCurrOverlay->m_Type == OVERLAY_SMARTAMMO)) + { + DrawOverlay(pCurrOverlay); + } + + pPrevOverlay = pCurrOverlay; + pCurrOverlay = pCurrOverlay->m_pNextOverlay; + } + } + + LeaveCriticalSection(s_OverlayMutex); +} + +ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", DebugOverlay, ConVar, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + OverlayBase_t__IsDead = module.Offset(0xACAC0).RCast<OverlayBase_t__IsDeadType>(); + OverlayBase_t__DestroyOverlay = module.Offset(0xAB680).RCast<OverlayBase_t__DestroyOverlayType>(); + + RenderLine = module.Offset(0x192A70).RCast<RenderLineType>(); + RenderBox = module.Offset(0x192520).RCast<RenderBoxType>(); + RenderWireframeBox = module.Offset(0x193DA0).RCast<RenderBoxType>(); + RenderWireframeSweptBox = module.Offset(0x1945A0).RCast<RenderWireframeSweptBoxType>(); + RenderTriangle = module.Offset(0x193940).RCast<RenderTriangleType>(); + RenderAxis = module.Offset(0x1924D0).RCast<RenderAxisType>(); + RenderSphere = module.Offset(0x194170).RCast<RenderSphereType>(); + RenderUnknown = module.Offset(0x1924E0).RCast<RenderUnknownType>(); + + s_OverlayMutex = module.Offset(0x10DB0A38).RCast<LPCRITICAL_SECTION>(); + + s_pOverlays = module.Offset(0x10DB0968).RCast<OverlayBase_t**>(); + + g_nRenderTickCount = module.Offset(0x10DB0984).RCast<int*>(); + g_nOverlayTickCount = module.Offset(0x10DB0980).RCast<int*>(); + + // not in g_pCVar->FindVar by this point for whatever reason, so have to get from memory + Cvar_enable_debug_overlays = module.Offset(0x10DB0990).RCast<ConVar*>(); + Cvar_enable_debug_overlays->SetValue(false); + Cvar_enable_debug_overlays->m_pszDefaultValue = (char*)"0"; + Cvar_enable_debug_overlays->AddFlags(FCVAR_CHEAT); +} diff --git a/primedev/client/demofixes.cpp b/primedev/client/demofixes.cpp new file mode 100644 index 00000000..344764ba --- /dev/null +++ b/primedev/client/demofixes.cpp @@ -0,0 +1,25 @@ +#include "core/convar/convar.h" + +ON_DLL_LOAD_CLIENT("engine.dll", EngineDemoFixes, (CModule module)) +{ + // allow demo recording on loopback + module.Offset(0x8E1B1).NOP(2); + module.Offset(0x56CC3).NOP(2); +} + +ON_DLL_LOAD_CLIENT_RELIESON("client.dll", ClientDemoFixes, ConVar, (CModule module)) +{ + // change default values of demo cvars to enable them by default, but not autorecord + // this is before Host_Init, the setvalue calls here will get overwritten by custom cfgs/launch options + ConVar* Cvar_demo_enableDemos = g_pCVar->FindVar("demo_enabledemos"); + Cvar_demo_enableDemos->m_pszDefaultValue = "1"; + Cvar_demo_enableDemos->SetValue(true); + + ConVar* Cvar_demo_writeLocalFile = g_pCVar->FindVar("demo_writeLocalFile"); + Cvar_demo_writeLocalFile->m_pszDefaultValue = "1"; + Cvar_demo_writeLocalFile->SetValue(true); + + ConVar* Cvar_demo_autoRecord = g_pCVar->FindVar("demo_autoRecord"); + Cvar_demo_autoRecord->m_pszDefaultValue = "0"; + Cvar_demo_autoRecord->SetValue(false); +} diff --git a/primedev/client/diskvmtfixes.cpp b/primedev/client/diskvmtfixes.cpp new file mode 100644 index 00000000..4ab951c0 --- /dev/null +++ b/primedev/client/diskvmtfixes.cpp @@ -0,0 +1,15 @@ + +ON_DLL_LOAD_CLIENT("materialsystem_dx11.dll", DiskVMTFixes, (CModule module)) +{ + // in retail VMTs will never load if cache read is invalid due to a special case for them in KeyValues::LoadFromFile + // this effectively makes it impossible to load them from mods because we invalidate cache for doing this + // so uhh, stop that from happening + + // tbh idk why they even changed any of this what's the point it looks like it works fine who cares my god + + // matsystem KeyValues::LoadFromFile: patch special case on cache read failure for vmts + module.Offset(0x1281B9).Patch("EB"); + + // CMaterialSystem::FindMaterial: don't call function that crashes if previous patch is applied + module.Offset(0x5F55A).NOP(5); +} diff --git a/primedev/client/languagehooks.cpp b/primedev/client/languagehooks.cpp new file mode 100644 index 00000000..35ca5659 --- /dev/null +++ b/primedev/client/languagehooks.cpp @@ -0,0 +1,115 @@ +#include "core/tier0.h" + +#include <filesystem> +#include <regex> + +AUTOHOOK_INIT() + +typedef LANGID (*Tier0_DetectDefaultLanguageType)(); + +bool CheckLangAudioExists(char* lang) +{ + std::string path {"r2\\sound\\general_"}; + path += lang; + path += ".mstr"; + return fs::exists(path); +} + +std::vector<std::string> file_list(fs::path dir, std::regex ext_pattern) +{ + std::vector<std::string> result; + + if (!fs::exists(dir) || !fs::is_directory(dir)) + return result; + + using iterator = fs::directory_iterator; + + const iterator end; + for (iterator iter {dir}; iter != end; ++iter) + { + const std::string filename = iter->path().filename().string(); + std::smatch matches; + if (fs::is_regular_file(*iter) && std::regex_match(filename, matches, ext_pattern)) + { + result.push_back(std::move(matches.str(1))); + } + } + + return result; +} + +std::string GetAnyInstalledAudioLanguage() +{ + for (const auto& lang : file_list("r2\\sound\\", std::regex(".*?general_([a-z]+)_patch_1\\.mstr"))) + if (lang != "general" || lang != "") + return lang; + return "NO LANGUAGE DETECTED"; +} + +// clang-format off +AUTOHOOK(GetGameLanguage, tier0.dll + 0xF560, +char*, __fastcall, ()) +// clang-format on +{ + auto tier0Handle = GetModuleHandleA("tier0.dll"); + auto Tier0_DetectDefaultLanguageType = GetProcAddress(tier0Handle, "Tier0_DetectDefaultLanguage"); + char* ingameLang1 = (char*)tier0Handle + 0xA9B60; // one of the globals we need to override if overriding lang (size: 256) + bool& canOriginDictateLang = *(bool*)((char*)tier0Handle + 0xA9A90); + + const char* forcedLanguage; + if (CommandLine()->CheckParm("-language", &forcedLanguage)) + { + if (!CheckLangAudioExists((char*)forcedLanguage)) + { + spdlog::info( + "User tried to force the language (-language) to \"{}\", but audio for this language doesn't exist and the game is bound " + "to error, falling back to next option...", + forcedLanguage); + } + else + { + spdlog::info("User forcing the language (-language) to: {}", forcedLanguage); + strncpy(ingameLang1, forcedLanguage, 256); + return ingameLang1; + } + } + + canOriginDictateLang = true; // let it try + { + auto lang = GetGameLanguage(); + if (!CheckLangAudioExists(lang)) + { + if (strcmp(lang, "russian") != + 0) // don't log for "russian" since it's the default and that means Origin detection just didn't change it most likely + spdlog::info( + "Origin detected language \"{}\", but we do not have audio for it installed, falling back to the next option", lang); + } + else + { + spdlog::info("Origin detected language: {}", lang); + return lang; + } + } + + Tier0_DetectDefaultLanguageType(); // force the global in tier0 to be populated with language inferred from user's system rather than + // defaulting to Russian + canOriginDictateLang = false; // Origin has no say anymore, we will fallback to user's system setup language + auto lang = GetGameLanguage(); + spdlog::info("Detected system language: {}", lang); + if (!CheckLangAudioExists(lang)) + { + spdlog::warn("Caution, audio for this language does NOT exist. You might want to override your game language with -language " + "command line option."); + auto lang = GetAnyInstalledAudioLanguage(); + spdlog::warn("Falling back to the first installed audio language: {}", lang.c_str()); + strncpy(ingameLang1, lang.c_str(), 256); + return ingameLang1; + } + + return lang; +} + +ON_DLL_LOAD_CLIENT("tier0.dll", LanguageHooks, (CModule module)) +{ + AUTOHOOK_DISPATCH() +} diff --git a/primedev/client/latencyflex.cpp b/primedev/client/latencyflex.cpp new file mode 100644 index 00000000..25e38c7a --- /dev/null +++ b/primedev/client/latencyflex.cpp @@ -0,0 +1,43 @@ +#include "core/convar/convar.h" + +AUTOHOOK_INIT() + +ConVar* Cvar_r_latencyflex; + +void (*m_winelfx_WaitAndBeginFrame)(); + +// clang-format off +AUTOHOOK(OnRenderStart, client.dll + 0x1952C0, +void, __fastcall, ()) +// clang-format on +{ + if (Cvar_r_latencyflex->GetBool() && m_winelfx_WaitAndBeginFrame) + m_winelfx_WaitAndBeginFrame(); + + OnRenderStart(); +} + +ON_DLL_LOAD_CLIENT_RELIESON("client.dll", LatencyFlex, ConVar, (CModule module)) +{ + // Connect to the LatencyFleX service + // LatencyFleX is an open source vendor agnostic replacement for Nvidia Reflex input latency reduction technology. + // https://ishitatsuyuki.github.io/post/latencyflex/ + HMODULE pLfxModule; + + if (pLfxModule = LoadLibraryA("latencyflex_layer.dll")) + m_winelfx_WaitAndBeginFrame = + reinterpret_cast<void (*)()>(reinterpret_cast<void*>(GetProcAddress(pLfxModule, "lfx_WaitAndBeginFrame"))); + else if (pLfxModule = LoadLibraryA("latencyflex_wine.dll")) + m_winelfx_WaitAndBeginFrame = + reinterpret_cast<void (*)()>(reinterpret_cast<void*>(GetProcAddress(pLfxModule, "winelfx_WaitAndBeginFrame"))); + else + { + spdlog::info("Unable to load LatencyFleX library, LatencyFleX disabled."); + return; + } + + AUTOHOOK_DISPATCH() + + spdlog::info("LatencyFleX initialized."); + Cvar_r_latencyflex = new ConVar("r_latencyflex", "1", FCVAR_ARCHIVE, "Whether or not to use LatencyFleX input latency reduction."); +} diff --git a/primedev/client/localchatwriter.cpp b/primedev/client/localchatwriter.cpp new file mode 100644 index 00000000..35cc065f --- /dev/null +++ b/primedev/client/localchatwriter.cpp @@ -0,0 +1,449 @@ +#include "localchatwriter.h" + +class vgui_BaseRichText_vtable; + +class vgui_BaseRichText +{ +public: + vgui_BaseRichText_vtable* vtable; +}; + +class vgui_BaseRichText_vtable +{ +public: + char unknown1[1880]; + + void(__fastcall* InsertChar)(vgui_BaseRichText* self, wchar_t ch); + + // yes these are swapped from the Source 2013 code, who knows why + void(__fastcall* InsertStringWide)(vgui_BaseRichText* self, const wchar_t* wszText); + void(__fastcall* InsertStringAnsi)(vgui_BaseRichText* self, const char* text); + + void(__fastcall* SelectNone)(vgui_BaseRichText* self); + void(__fastcall* SelectAllText)(vgui_BaseRichText* self); + void(__fastcall* SelectNoText)(vgui_BaseRichText* self); + void(__fastcall* CutSelected)(vgui_BaseRichText* self); + void(__fastcall* CopySelected)(vgui_BaseRichText* self); + void(__fastcall* SetPanelInteractive)(vgui_BaseRichText* self, bool bInteractive); + void(__fastcall* SetUnusedScrollbarInvisible)(vgui_BaseRichText* self, bool bInvis); + + void* unknown2; + + void(__fastcall* GotoTextStart)(vgui_BaseRichText* self); + void(__fastcall* GotoTextEnd)(vgui_BaseRichText* self); + + void* unknown3[3]; + + void(__fastcall* SetVerticalScrollbar)(vgui_BaseRichText* self, bool state); + void(__fastcall* SetMaximumCharCount)(vgui_BaseRichText* self, int maxChars); + void(__fastcall* InsertColorChange)(vgui_BaseRichText* self, Color col); + void(__fastcall* InsertIndentChange)(vgui_BaseRichText* self, int pixelsIndent); + void(__fastcall* InsertClickableTextStart)(vgui_BaseRichText* self, const char* pchClickAction); + void(__fastcall* InsertClickableTextEnd)(vgui_BaseRichText* self); + void(__fastcall* InsertPossibleURLString)(vgui_BaseRichText* self, const char* text, Color URLTextColor, Color normalTextColor); + void(__fastcall* InsertFade)(vgui_BaseRichText* self, float flSustain, float flLength); + void(__fastcall* ResetAllFades)(vgui_BaseRichText* self, bool bHold, bool bOnlyExpired, float flNewSustain); + void(__fastcall* SetToFullHeight)(vgui_BaseRichText* self); + int(__fastcall* GetNumLines)(vgui_BaseRichText* self); +}; + +class CGameSettings +{ +public: + char unknown1[92]; + int isChatEnabled; +}; + +// Not sure what this actually refers to but chatFadeLength and chatFadeSustain +// have their value at the same offset +class CGameFloatVar +{ +public: + char unknown1[88]; + float value; +}; + +CGameSettings** gGameSettings; +CGameFloatVar** gChatFadeLength; +CGameFloatVar** gChatFadeSustain; + +CHudChat** CHudChat::allHuds; + +typedef void(__fastcall* ConvertANSIToUnicodeType)(LPCSTR ansi, int ansiCharLength, LPWSTR unicode, int unicodeCharLength); +ConvertANSIToUnicodeType ConvertANSIToUnicode; + +LocalChatWriter::SwatchColor swatchColors[4] = { + LocalChatWriter::MainTextColor, + LocalChatWriter::SameTeamNameColor, + LocalChatWriter::EnemyTeamNameColor, + LocalChatWriter::NetworkNameColor, +}; + +Color darkColors[8] = { + Color {0, 0, 0, 255}, + Color {205, 49, 49, 255}, + Color {13, 188, 121, 255}, + Color {229, 229, 16, 255}, + Color {36, 114, 200, 255}, + Color {188, 63, 188, 255}, + Color {17, 168, 205, 255}, + Color {229, 229, 229, 255}}; + +Color lightColors[8] = { + Color {102, 102, 102, 255}, + Color {241, 76, 76, 255}, + Color {35, 209, 139, 255}, + Color {245, 245, 67, 255}, + Color {59, 142, 234, 255}, + Color {214, 112, 214, 255}, + Color {41, 184, 219, 255}, + Color {255, 255, 255, 255}}; + +class AnsiEscapeParser +{ +public: + explicit AnsiEscapeParser(LocalChatWriter* writer) : m_writer(writer) {} + + void HandleVal(unsigned long val) + { + switch (m_next) + { + case Next::ControlType: + m_next = HandleControlType(val); + break; + case Next::ForegroundType: + m_next = HandleForegroundType(val); + break; + case Next::Foreground8Bit: + m_next = HandleForeground8Bit(val); + break; + case Next::ForegroundR: + m_next = HandleForegroundR(val); + break; + case Next::ForegroundG: + m_next = HandleForegroundG(val); + break; + case Next::ForegroundB: + m_next = HandleForegroundB(val); + break; + } + } + +private: + enum class Next + { + ControlType, + ForegroundType, + Foreground8Bit, + ForegroundR, + ForegroundG, + ForegroundB + }; + + LocalChatWriter* m_writer; + Next m_next = Next::ControlType; + Color m_expandedColor {0, 0, 0, 0}; + + Next HandleControlType(unsigned long val) + { + // Reset + if (val == 0 || val == 39) + { + m_writer->InsertSwatchColorChange(LocalChatWriter::MainTextColor); + return Next::ControlType; + } + + // Dark foreground color + if (val >= 30 && val < 38) + { + m_writer->InsertColorChange(darkColors[val - 30]); + return Next::ControlType; + } + + // Light foreground color + if (val >= 90 && val < 98) + { + m_writer->InsertColorChange(lightColors[val - 90]); + return Next::ControlType; + } + + // Game swatch color + if (val >= 110 && val < 114) + { + m_writer->InsertSwatchColorChange(swatchColors[val - 110]); + return Next::ControlType; + } + + // Expanded foreground color + if (val == 38) + { + return Next::ForegroundType; + } + + return Next::ControlType; + } + + Next HandleForegroundType(unsigned long val) + { + // Next values are r,g,b + if (val == 2) + { + m_expandedColor.SetColor(0, 0, 0, 255); + return Next::ForegroundR; + } + // Next value is 8-bit swatch color + if (val == 5) + { + return Next::Foreground8Bit; + } + + // Invalid + return Next::ControlType; + } + + Next HandleForeground8Bit(unsigned long val) + { + if (val < 8) + { + m_writer->InsertColorChange(darkColors[val]); + } + else if (val < 16) + { + m_writer->InsertColorChange(lightColors[val - 8]); + } + else if (val < 232) + { + unsigned char code = val - 16; + unsigned char blue = code % 6; + unsigned char green = ((code - blue) / 6) % 6; + unsigned char red = (code - blue - (green * 6)) / 36; + m_writer->InsertColorChange(Color {(unsigned char)(red * 51), (unsigned char)(green * 51), (unsigned char)(blue * 51), 255}); + } + else if (val < UCHAR_MAX) + { + unsigned char brightness = (val - 232) * 10 + 8; + m_writer->InsertColorChange(Color {brightness, brightness, brightness, 255}); + } + + return Next::ControlType; + } + + Next HandleForegroundR(unsigned long val) + { + if (val >= UCHAR_MAX) + return Next::ControlType; + + m_expandedColor[0] = (unsigned char)val; + return Next::ForegroundG; + } + + Next HandleForegroundG(unsigned long val) + { + if (val >= UCHAR_MAX) + return Next::ControlType; + + m_expandedColor[1] = (unsigned char)val; + return Next::ForegroundB; + } + + Next HandleForegroundB(unsigned long val) + { + if (val >= UCHAR_MAX) + return Next::ControlType; + + m_expandedColor[2] = (unsigned char)val; + m_writer->InsertColorChange(m_expandedColor); + return Next::ControlType; + } +}; + +LocalChatWriter::LocalChatWriter(Context context) : m_context(context) {} + +void LocalChatWriter::Write(const char* str) +{ + char writeBuffer[256]; + + while (true) + { + const char* startOfEscape = strstr(str, "\033["); + + if (startOfEscape == NULL) + { + // No more escape sequences, write the remaining text and exit + InsertText(str); + break; + } + + if (startOfEscape != str) + { + // There is some text before the escape sequence, just print that + size_t copyChars = startOfEscape - str; + if (copyChars > 255) + copyChars = 255; + + strncpy_s(writeBuffer, copyChars + 1, str, copyChars); + + InsertText(writeBuffer); + } + + const char* escape = startOfEscape + 2; + str = ApplyAnsiEscape(escape); + } +} + +void LocalChatWriter::WriteLine(const char* str) +{ + InsertChar(L'\n'); + InsertSwatchColorChange(MainTextColor); + Write(str); +} + +void LocalChatWriter::InsertChar(wchar_t ch) +{ + for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + + hud->m_richText->vtable->InsertChar(hud->m_richText, ch); + } + + if (ch != L'\n') + { + InsertDefaultFade(); + } +} + +void LocalChatWriter::InsertText(const char* str) +{ + spdlog::info(str); + + WCHAR messageUnicode[288]; + ConvertANSIToUnicode(str, -1, messageUnicode, 274); + + for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + + hud->m_richText->vtable->InsertStringWide(hud->m_richText, messageUnicode); + } + + InsertDefaultFade(); +} + +void LocalChatWriter::InsertText(const wchar_t* str) +{ + for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + + hud->m_richText->vtable->InsertStringWide(hud->m_richText, str); + } + + InsertDefaultFade(); +} + +void LocalChatWriter::InsertColorChange(Color color) +{ + for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + + hud->m_richText->vtable->InsertColorChange(hud->m_richText, color); + } +} + +static Color GetHudSwatchColor(CHudChat* hud, LocalChatWriter::SwatchColor swatchColor) +{ + switch (swatchColor) + { + case LocalChatWriter::MainTextColor: + return hud->m_mainTextColor; + + case LocalChatWriter::SameTeamNameColor: + return hud->m_sameTeamColor; + + case LocalChatWriter::EnemyTeamNameColor: + return hud->m_enemyTeamColor; + + case LocalChatWriter::NetworkNameColor: + return hud->m_networkNameColor; + } + + return Color(0, 0, 0, 0); +} + +void LocalChatWriter::InsertSwatchColorChange(SwatchColor swatchColor) +{ + for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + hud->m_richText->vtable->InsertColorChange(hud->m_richText, GetHudSwatchColor(hud, swatchColor)); + } +} + +const char* LocalChatWriter::ApplyAnsiEscape(const char* escape) +{ + AnsiEscapeParser decoder(this); + while (true) + { + char* afterControlType = NULL; + unsigned long controlType = strtoul(escape, &afterControlType, 10); + + // Malformed cases: + // afterControlType = NULL: strtoul errored + // controlType = 0 and escape doesn't actually start with 0: wasn't a number + if (afterControlType == NULL || (controlType == 0 && escape[0] != '0')) + { + return escape; + } + + decoder.HandleVal(controlType); + + // m indicates the end of the sequence + if (afterControlType[0] == 'm') + { + return afterControlType + 1; + } + + // : or ; indicates more values remain, anything else is malformed + if (afterControlType[0] != ':' && afterControlType[0] != ';') + { + return afterControlType; + } + + escape = afterControlType + 1; + } +} + +void LocalChatWriter::InsertDefaultFade() +{ + float fadeLength = 0.f; + float fadeSustain = 0.f; + if ((*gGameSettings)->isChatEnabled) + { + fadeLength = (*gChatFadeLength)->value; + fadeSustain = (*gChatFadeSustain)->value; + } + + for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + hud->m_richText->vtable->InsertFade(hud->m_richText, fadeSustain, fadeLength); + } +} + +ON_DLL_LOAD_CLIENT("client.dll", LocalChatWriter, (CModule module)) +{ + gGameSettings = module.Offset(0x11BAA48).RCast<CGameSettings**>(); + gChatFadeLength = module.Offset(0x11BAB78).RCast<CGameFloatVar**>(); + gChatFadeSustain = module.Offset(0x11BAC08).RCast<CGameFloatVar**>(); + CHudChat::allHuds = module.Offset(0x11BA9E8).RCast<CHudChat**>(); + + ConvertANSIToUnicode = module.Offset(0x7339A0).RCast<ConvertANSIToUnicodeType>(); +} diff --git a/primedev/client/localchatwriter.h b/primedev/client/localchatwriter.h new file mode 100644 index 00000000..acf6f87e --- /dev/null +++ b/primedev/client/localchatwriter.h @@ -0,0 +1,64 @@ +#pragma once +#include "core/math/color.h" + +class vgui_BaseRichText; + +class CHudChat +{ +public: + static CHudChat** allHuds; + + char unknown1[720]; + + Color m_sameTeamColor; + Color m_enemyTeamColor; + Color m_mainTextColor; + Color m_networkNameColor; + + char unknown2[12]; + + int m_unknownContext; + + char unknown3[8]; + + vgui_BaseRichText* m_richText; + + CHudChat* next; + CHudChat* previous; +}; + +class LocalChatWriter +{ +public: + enum Context + { + NetworkContext = 0, + GameContext = 1 + }; + enum SwatchColor + { + MainTextColor, + SameTeamNameColor, + EnemyTeamNameColor, + NetworkNameColor + }; + + explicit LocalChatWriter(Context context); + + // Custom chat writing with ANSI escape codes + void Write(const char* str); + void WriteLine(const char* str); + + // Low-level RichText access + void InsertChar(wchar_t ch); + void InsertText(const char* str); + void InsertText(const wchar_t* str); + void InsertColorChange(Color color); + void InsertSwatchColorChange(SwatchColor color); + +private: + Context m_context; + + const char* ApplyAnsiEscape(const char* escape); + void InsertDefaultFade(); +}; diff --git a/primedev/client/modlocalisation.cpp b/primedev/client/modlocalisation.cpp new file mode 100644 index 00000000..2b73876b --- /dev/null +++ b/primedev/client/modlocalisation.cpp @@ -0,0 +1,55 @@ +#include "mods/modmanager.h" + +AUTOHOOK_INIT() + +void* g_pVguiLocalize; + +// clang-format off +AUTOHOOK(CLocalize__AddFile, localize.dll + 0x6D80, +bool, __fastcall, (void* pVguiLocalize, const char* path, const char* pathId, bool bIncludeFallbackSearchPaths)) +// clang-format on +{ + // save this for later + g_pVguiLocalize = pVguiLocalize; + + bool ret = CLocalize__AddFile(pVguiLocalize, path, pathId, bIncludeFallbackSearchPaths); + if (ret) + spdlog::info("Loaded localisation file {} successfully", path); + + return true; +} + +// clang-format off +AUTOHOOK(CLocalize__ReloadLocalizationFiles, localize.dll + 0xB830, +void, __fastcall, (void* pVguiLocalize)) +// clang-format on +{ + // load all mod localization manually, so we keep track of all files, not just previously loaded ones + for (Mod mod : g_pModManager->m_LoadedMods) + if (mod.m_bEnabled) + for (std::string& localisationFile : mod.LocalisationFiles) + CLocalize__AddFile(g_pVguiLocalize, localisationFile.c_str(), nullptr, false); + + spdlog::info("reloading localization..."); + CLocalize__ReloadLocalizationFiles(pVguiLocalize); +} + +// clang-format off +AUTOHOOK(CEngineVGui__Init, engine.dll + 0x247E10, +void, __fastcall, (void* self)) +// clang-format on +{ + CEngineVGui__Init(self); // this loads r1_english, valve_english, dev_english + + // previously we did this in CLocalize::AddFile, but for some reason it won't properly overwrite localization from + // files loaded previously if done there, very weird but this works so whatever + for (Mod mod : g_pModManager->m_LoadedMods) + if (mod.m_bEnabled) + for (std::string& localisationFile : mod.LocalisationFiles) + CLocalize__AddFile(g_pVguiLocalize, localisationFile.c_str(), nullptr, false); +} + +ON_DLL_LOAD_CLIENT("localize.dll", Localize, (CModule module)) +{ + AUTOHOOK_DISPATCH() +} diff --git a/primedev/client/r2client.cpp b/primedev/client/r2client.cpp new file mode 100644 index 00000000..c8e59d74 --- /dev/null +++ b/primedev/client/r2client.cpp @@ -0,0 +1,13 @@ +#include "r2client.h" + +char* g_pLocalPlayerUserID; +char* g_pLocalPlayerOriginToken; +GetBaseLocalClientType GetBaseLocalClient; + +ON_DLL_LOAD("engine.dll", R2EngineClient, (CModule module)) +{ + g_pLocalPlayerUserID = module.Offset(0x13F8E688).RCast<char*>(); + g_pLocalPlayerOriginToken = module.Offset(0x13979C80).RCast<char*>(); + + GetBaseLocalClient = module.Offset(0x78200).RCast<GetBaseLocalClientType>(); +} diff --git a/primedev/client/r2client.h b/primedev/client/r2client.h new file mode 100644 index 00000000..ea263dbc --- /dev/null +++ b/primedev/client/r2client.h @@ -0,0 +1,7 @@ +#pragma once + +extern char* g_pLocalPlayerUserID; +extern char* g_pLocalPlayerOriginToken; + +typedef void* (*GetBaseLocalClientType)(); +extern GetBaseLocalClientType GetBaseLocalClient; diff --git a/primedev/client/rejectconnectionfixes.cpp b/primedev/client/rejectconnectionfixes.cpp new file mode 100644 index 00000000..1b326a3c --- /dev/null +++ b/primedev/client/rejectconnectionfixes.cpp @@ -0,0 +1,34 @@ +#include "engine/r2engine.h"
+
+AUTOHOOK_INIT()
+
+// this is called from when our connection is rejected, this is the only case we're hooking this for
+// clang-format off
+AUTOHOOK(COM_ExplainDisconnection, engine.dll + 0x1342F0,
+void,, (bool a1, const char* fmt, ...))
+// clang-format on
+{
+ va_list va;
+ va_start(va, fmt);
+ char buf[4096];
+ vsnprintf_s(buf, 4096, fmt, va);
+ va_end(va);
+
+ // slightly hacky comparison, but patching the function that calls this for reject would be worse
+ if (!strncmp(fmt, "Connection rejected: ", 21))
+ {
+ // when COM_ExplainDisconnection is called from engine.dll + 19ff1c for connection rejected, it doesn't
+ // call Host_Disconnect, which properly shuts down listen server
+ // not doing this gets our client in a pretty weird state so we need to shut it down manually here
+
+ // don't call Cbuf_Execute because we don't need this called immediately
+ Cbuf_AddText(Cbuf_GetCurrentPlayer(), "disconnect", cmd_source_t::kCommandSrcCode);
+ }
+
+ return COM_ExplainDisconnection(a1, "%s", buf);
+}
+
+ON_DLL_LOAD_CLIENT("engine.dll", RejectConnectionFixes, (CModule module))
+{
+ AUTOHOOK_DISPATCH()
+}
|