aboutsummaryrefslogtreecommitdiff
path: root/NorthstarDLL/client
diff options
context:
space:
mode:
authorEmma Miler <emma.pi@protonmail.com>2022-12-19 19:32:16 +0100
committerGitHub <noreply@github.com>2022-12-19 19:32:16 +0100
commite04f3b36accccb590a2d51b4829256b9964ac3fd (patch)
tree20ee30c82e6f53e6e772be2e1b9613eebca12bf3 /NorthstarDLL/client
parent33f18a735986dcd136bf8ba70ad8331306c28227 (diff)
downloadNorthstarLauncher-e04f3b36accccb590a2d51b4829256b9964ac3fd.tar.gz
NorthstarLauncher-e04f3b36accccb590a2d51b4829256b9964ac3fd.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.cpp507
-rw-r--r--NorthstarDLL/client/audio.h46
-rw-r--r--NorthstarDLL/client/chatcommand.cpp36
-rw-r--r--NorthstarDLL/client/clientauthhooks.cpp64
-rw-r--r--NorthstarDLL/client/clientruihooks.cpp24
-rw-r--r--NorthstarDLL/client/clientvideooverrides.cpp42
-rw-r--r--NorthstarDLL/client/debugoverlay.cpp141
-rw-r--r--NorthstarDLL/client/demofixes.cpp26
-rw-r--r--NorthstarDLL/client/diskvmtfixes.cpp16
-rw-r--r--NorthstarDLL/client/languagehooks.cpp116
-rw-r--r--NorthstarDLL/client/latencyflex.cpp44
-rw-r--r--NorthstarDLL/client/localchatwriter.cpp450
-rw-r--r--NorthstarDLL/client/localchatwriter.h65
-rw-r--r--NorthstarDLL/client/modlocalisation.cpp35
-rw-r--r--NorthstarDLL/client/r2client.cpp20
-rw-r--r--NorthstarDLL/client/r2client.h11
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