diff options
author | Emma Miler <emma.pi@protonmail.com> | 2022-12-19 19:32:16 +0100 |
---|---|---|
committer | GeckoEidechse <gecko.eidechse+git@pm.me> | 2023-01-04 14:45:48 +0100 |
commit | 27afb0ba38dcf0e74a4d09ba43e73261542b8e96 (patch) | |
tree | 28c737bdecc761fc8ca5257adfafaf2b325e1918 /NorthstarDLL/client | |
parent | af64117f09ba9b70d27c6b6da885d0474180849b (diff) | |
download | NorthstarLauncher-27afb0ba38dcf0e74a4d09ba43e73261542b8e96.tar.gz NorthstarLauncher-27afb0ba38dcf0e74a4d09ba43e73261542b8e96.zip |
Restructuring (#365)
* Remove launcher proxy
* Restructuring
* More restructuring
* Fix include dirs
* Fix merge
* Remove clang thing
* Filters
* Oops
Diffstat (limited to 'NorthstarDLL/client')
-rw-r--r-- | NorthstarDLL/client/audio.cpp | 507 | ||||
-rw-r--r-- | NorthstarDLL/client/audio.h | 46 | ||||
-rw-r--r-- | NorthstarDLL/client/chatcommand.cpp | 36 | ||||
-rw-r--r-- | NorthstarDLL/client/clientauthhooks.cpp | 64 | ||||
-rw-r--r-- | NorthstarDLL/client/clientruihooks.cpp | 24 | ||||
-rw-r--r-- | NorthstarDLL/client/clientvideooverrides.cpp | 42 | ||||
-rw-r--r-- | NorthstarDLL/client/debugoverlay.cpp | 141 | ||||
-rw-r--r-- | NorthstarDLL/client/demofixes.cpp | 26 | ||||
-rw-r--r-- | NorthstarDLL/client/diskvmtfixes.cpp | 16 | ||||
-rw-r--r-- | NorthstarDLL/client/languagehooks.cpp | 116 | ||||
-rw-r--r-- | NorthstarDLL/client/latencyflex.cpp | 44 | ||||
-rw-r--r-- | NorthstarDLL/client/localchatwriter.cpp | 450 | ||||
-rw-r--r-- | NorthstarDLL/client/localchatwriter.h | 65 | ||||
-rw-r--r-- | NorthstarDLL/client/modlocalisation.cpp | 35 | ||||
-rw-r--r-- | NorthstarDLL/client/r2client.cpp | 20 | ||||
-rw-r--r-- | NorthstarDLL/client/r2client.h | 11 |
16 files changed, 1643 insertions, 0 deletions
diff --git a/NorthstarDLL/client/audio.cpp b/NorthstarDLL/client/audio.cpp new file mode 100644 index 00000000..f0bc385b --- /dev/null +++ b/NorthstarDLL/client/audio.cpp @@ -0,0 +1,507 @@ +#include "pch.h" +#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() + +extern "C" +{ + // should be called only in LoadSampleMetadata_Hook + extern void* __fastcall Audio_GetParentEvent(); +} + +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 +} + +// forward declare +bool __declspec(noinline) __fastcall LoadSampleMetadata_Internal( + uintptr_t parentEvent, void* sample, void* audioBuffer, unsigned int audioBufferLength, int audioType); + +// DO NOT TOUCH THIS FUNCTION +// The actual logic of it in a separate function (forcefully not inlined) to preserve the r12 register, which holds the event pointer. +// clang-format off +AUTOHOOK(LoadSampleMetadata, mileswin64.dll + 0xF110, +bool, __fastcall, (void* sample, void* audioBuffer, unsigned int audioBufferLength, int audioType)) +// clang-format on +{ + uintptr_t parentEvent = (uintptr_t)Audio_GetParentEvent(); + + // Raw source, used for voice data only + if (audioType == 0) + return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType); + + return LoadSampleMetadata_Internal(parentEvent, sample, audioBuffer, audioBufferLength, audioType); +} + +// DO NOT INLINE THIS FUNCTION +// See comment below. +bool __declspec(noinline) __fastcall LoadSampleMetadata_Internal( + uintptr_t parentEvent, void* sample, void* audioBuffer, unsigned int audioBufferLength, int audioType) +{ + char* eventName = (char*)parentEvent + 0x110; + + 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(MilesLog, client.dll + 0x57DAD0, +void, __fastcall, (int level, const char* string)) +// clang-format on +{ + spdlog::info("[MSS] {} - {}", level, string); +} + +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).As<MilesStopAll_Type>(); +} diff --git a/NorthstarDLL/client/audio.h b/NorthstarDLL/client/audio.h new file mode 100644 index 00000000..26cda205 --- /dev/null +++ b/NorthstarDLL/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/NorthstarDLL/client/chatcommand.cpp b/NorthstarDLL/client/chatcommand.cpp new file mode 100644 index 00000000..76ed9784 --- /dev/null +++ b/NorthstarDLL/client/chatcommand.cpp @@ -0,0 +1,36 @@ +#include "pch.h" +#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).As<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/NorthstarDLL/client/clientauthhooks.cpp b/NorthstarDLL/client/clientauthhooks.cpp new file mode 100644 index 00000000..904ecb2f --- /dev/null +++ b/NorthstarDLL/client/clientauthhooks.cpp @@ -0,0 +1,64 @@ +#include "pch.h" +#include "masterserver/masterserver.h" +#include "core/convar/convar.h" +#include "client/r2client.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 +{ + // 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(R2::g_pLocalPlayerUserID, R2::g_pLocalPlayerOriginToken); + + // invalidate key so auth will fail + *R2::g_pLocalPlayerOriginToken = 0; + } + + AuthWithStryder(a1); +} + +char* p3PToken; + +// clang-format off +AUTOHOOK(Auth3PToken, engine.dll + 0x183760, +char*, __fastcall, ()) +// clang-format on +{ + if (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).As<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/NorthstarDLL/client/clientruihooks.cpp b/NorthstarDLL/client/clientruihooks.cpp new file mode 100644 index 00000000..7896daa8 --- /dev/null +++ b/NorthstarDLL/client/clientruihooks.cpp @@ -0,0 +1,24 @@ +#include "pch.h" +#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/NorthstarDLL/client/clientvideooverrides.cpp b/NorthstarDLL/client/clientvideooverrides.cpp new file mode 100644 index 00000000..1a5924c7 --- /dev/null +++ b/NorthstarDLL/client/clientvideooverrides.cpp @@ -0,0 +1,42 @@ +#include "pch.h" +#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/NorthstarDLL/client/debugoverlay.cpp b/NorthstarDLL/client/debugoverlay.cpp new file mode 100644 index 00000000..dd273227 --- /dev/null +++ b/NorthstarDLL/client/debugoverlay.cpp @@ -0,0 +1,141 @@ +#include "pch.h" +#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_TRIANGLE, + OVERLAY_SWEPT_BOX, + 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; + void* 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; +}; + +static HMODULE sEngineModule; + +typedef void (*RenderLineType)(Vector3 v1, Vector3 v2, Color c, bool bZBuffer); +static RenderLineType RenderLine; +typedef void (*RenderBoxType)(Vector3 vOrigin, QAngle angles, Vector3 vMins, Vector3 vMaxs, Color c, bool bZBuffer, bool bInsideOut); +static RenderBoxType RenderBox; +static RenderBoxType RenderWireframeBox; + +// clang-format off +AUTOHOOK(DrawOverlay, engine.dll + 0xABCB0, +void, __fastcall, (OverlayBase_t * pOverlay)) +// clang-format on +{ + EnterCriticalSection((LPCRITICAL_SECTION)((char*)sEngineModule + 0x10DB0A38)); // s_OverlayMutex + + void* pMaterialSystem = *(void**)((char*)sEngineModule + 0x14C675B0); + + switch (pOverlay->m_Type) + { + 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; + } + LeaveCriticalSection((LPCRITICAL_SECTION)((char*)sEngineModule + 0x10DB0A38)); +} + +ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", DebugOverlay, ConVar, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + RenderLine = module.Offset(0x192A70).As<RenderLineType>(); + RenderBox = module.Offset(0x192520).As<RenderBoxType>(); + RenderWireframeBox = module.Offset(0x193DA0).As<RenderBoxType>(); + sEngineModule = reinterpret_cast<HMODULE>(module.m_nAddress); + + // not in g_pCVar->FindVar by this point for whatever reason, so have to get from memory + ConVar* Cvar_enable_debug_overlays = module.Offset(0x10DB0990).As<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/NorthstarDLL/client/demofixes.cpp b/NorthstarDLL/client/demofixes.cpp new file mode 100644 index 00000000..5fb49918 --- /dev/null +++ b/NorthstarDLL/client/demofixes.cpp @@ -0,0 +1,26 @@ +#include "pch.h" +#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 = R2::g_pCVar->FindVar("demo_enabledemos"); + Cvar_demo_enableDemos->m_pszDefaultValue = "1"; + Cvar_demo_enableDemos->SetValue(true); + + ConVar* Cvar_demo_writeLocalFile = R2::g_pCVar->FindVar("demo_writeLocalFile"); + Cvar_demo_writeLocalFile->m_pszDefaultValue = "1"; + Cvar_demo_writeLocalFile->SetValue(true); + + ConVar* Cvar_demo_autoRecord = R2::g_pCVar->FindVar("demo_autoRecord"); + Cvar_demo_autoRecord->m_pszDefaultValue = "0"; + Cvar_demo_autoRecord->SetValue(false); +} diff --git a/NorthstarDLL/client/diskvmtfixes.cpp b/NorthstarDLL/client/diskvmtfixes.cpp new file mode 100644 index 00000000..cd762c8a --- /dev/null +++ b/NorthstarDLL/client/diskvmtfixes.cpp @@ -0,0 +1,16 @@ +#include "pch.h" + +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/NorthstarDLL/client/languagehooks.cpp b/NorthstarDLL/client/languagehooks.cpp new file mode 100644 index 00000000..1a46633c --- /dev/null +++ b/NorthstarDLL/client/languagehooks.cpp @@ -0,0 +1,116 @@ +#include "pch.h" +#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 (Tier0::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/NorthstarDLL/client/latencyflex.cpp b/NorthstarDLL/client/latencyflex.cpp new file mode 100644 index 00000000..a1ef72ca --- /dev/null +++ b/NorthstarDLL/client/latencyflex.cpp @@ -0,0 +1,44 @@ +#include "pch.h" +#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/NorthstarDLL/client/localchatwriter.cpp b/NorthstarDLL/client/localchatwriter.cpp new file mode 100644 index 00000000..efa7eeee --- /dev/null +++ b/NorthstarDLL/client/localchatwriter.cpp @@ -0,0 +1,450 @@ +#include "pch.h" +#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).As<CGameSettings**>(); + gChatFadeLength = module.Offset(0x11BAB78).As<CGameFloatVar**>(); + gChatFadeSustain = module.Offset(0x11BAC08).As<CGameFloatVar**>(); + CHudChat::allHuds = module.Offset(0x11BA9E8).As<CHudChat**>(); + + ConvertANSIToUnicode = module.Offset(0x7339A0).As<ConvertANSIToUnicodeType>(); +} diff --git a/NorthstarDLL/client/localchatwriter.h b/NorthstarDLL/client/localchatwriter.h new file mode 100644 index 00000000..8bb1fb2f --- /dev/null +++ b/NorthstarDLL/client/localchatwriter.h @@ -0,0 +1,65 @@ +#pragma once +#include "pch.h" +#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/NorthstarDLL/client/modlocalisation.cpp b/NorthstarDLL/client/modlocalisation.cpp new file mode 100644 index 00000000..430e3a5f --- /dev/null +++ b/NorthstarDLL/client/modlocalisation.cpp @@ -0,0 +1,35 @@ +#include "pch.h" +#include "mods/modmanager.h" + +AUTOHOOK_INIT() + +// clang-format off +AUTOHOOK(AddLocalisationFile, localize.dll + 0x6D80, +bool, __fastcall, (void* pVguiLocalize, const char* path, const char* pathId, char unknown)) +// clang-format on +{ + static bool bLoadModLocalisationFiles = true; + bool ret = AddLocalisationFile(pVguiLocalize, path, pathId, unknown); + + if (ret) + spdlog::info("Loaded localisation file {} successfully", path); + + if (!bLoadModLocalisationFiles) + return ret; + + bLoadModLocalisationFiles = false; + + for (Mod mod : g_pModManager->m_LoadedMods) + if (mod.m_bEnabled) + for (std::string& localisationFile : mod.LocalisationFiles) + AddLocalisationFile(pVguiLocalize, localisationFile.c_str(), pathId, unknown); + + bLoadModLocalisationFiles = true; + + return ret; +} + +ON_DLL_LOAD_CLIENT("localize.dll", Localize, (CModule module)) +{ + AUTOHOOK_DISPATCH() +} diff --git a/NorthstarDLL/client/r2client.cpp b/NorthstarDLL/client/r2client.cpp new file mode 100644 index 00000000..2e7bd564 --- /dev/null +++ b/NorthstarDLL/client/r2client.cpp @@ -0,0 +1,20 @@ +#include "pch.h" +#include "r2client.h" + +using namespace R2; + +// use the R2 namespace for game funcs +namespace R2 +{ + char* g_pLocalPlayerUserID; + char* g_pLocalPlayerOriginToken; + GetBaseLocalClientType GetBaseLocalClient; +} // namespace R2 + +ON_DLL_LOAD("engine.dll", R2EngineClient, (CModule module)) +{ + g_pLocalPlayerUserID = module.Offset(0x13F8E688).As<char*>(); + g_pLocalPlayerOriginToken = module.Offset(0x13979C80).As<char*>(); + + GetBaseLocalClient = module.Offset(0x78200).As<GetBaseLocalClientType>(); +} diff --git a/NorthstarDLL/client/r2client.h b/NorthstarDLL/client/r2client.h new file mode 100644 index 00000000..64ed6c61 --- /dev/null +++ b/NorthstarDLL/client/r2client.h @@ -0,0 +1,11 @@ +#pragma once + +// use the R2 namespace for game funcs +namespace R2 +{ + extern char* g_pLocalPlayerUserID; + extern char* g_pLocalPlayerOriginToken; + + typedef void* (*GetBaseLocalClientType)(); + extern GetBaseLocalClientType GetBaseLocalClient; +} // namespace R2 |