big refactor (#171)

* use in-file macros rather than global funcs for registering dll load callbacks

* move more things to macros

* fix debug crashes

* move sqvm funcs to sq managers

* get rid of context file

* refactor some squirrel stuff and ingame compilation error message

* move tier0 and playlist funcs to namespaces

* uiscript_reset concommand: don't loop forever if compilation fails

* improve showing console for ui script compile errors

* standardise concommand func naming in c++

* use lambdas for dll load callbacks so intellisense shits itself less

* use cvar change callbacks for unescaping ns_server_name and ns_server_desc

* add proper helpstrings to masterserver cvars

* add cvar help and find

* allow parsing of convar flags from string

* normalise mod fs paths to be lowercase

* move hoststate to its own file and add host_init hooks

* better IsFlagSet def

* replace files in ReadFromCache

* rename g_ModManager to g_pModManager

* formatting changes

* make cvar print work on dedi, move demo fix stuff, add findflags

* add proper map autocompletes and maps command

* formatting changes

* separate gameutils into multiple r2 headers

* Update keyvalues.cpp

* move sqvm funcs into wrappers in the manager class

* remove unnecessary header files

* lots of cleanup and starting moving to new hooking macros

* update more stuff to new hook macros

* rename project folder (:tf: commit log)

* fix up postbuild commands to use relative dir

* almost fully replaced hooking lib

* completely remove old hooking

* add nsprefix because i forgot to include it

* move exploit prevention and limits code out of serverauthentication, and have actual defs for CBasePlayer

* use modular ServerPresence system for registering servers

* add new memory lib

* accidentally pushed broke code oops

* lots of stuff idk

* implement some more prs

* improve rpakfilesystem

* fix line endings on vcxproj

* Revert "fix line endings on vcxproj"

This reverts commit 4ff7d022d2602c2dba37beba8b8df735cf5cd7d9.

* add more prs

* i swear i committed these how are they not there

* Add ability to load Datatables from files (#238)

* first version of kinda working custom datatables

* Fix copy error

* Finish custom datatables

* Fix Merge

* Fix line endings

* Add fallback to rpak when ns_prefere_datatable_from_disk is true

* fix typo

* Bug fixess

* Fix Function Registration hook

* Set convar value

* Fix Client and Ui VM

* enable server auth with ms agian

* Add Filters

* FIx unused import

* Merge remote-tracking branch 'upsteam/bobs-big-refactor-pr' into datatables

Co-authored-by: RoyalBlue1 <realEmail@veryRealURL.com>

* Add some changes from main to refactor (#243)

* Add PR template

* Update CI folder location

* Delete startup args txt files

* Fix line endings (hopefully) (#244)

* Fix line endings (hopefully)

* Fix more line endings

* Update refactor (#250)

* Add PR template

* Update CI folder location

* Delete startup args txt files

* Add editorconfig file (#246)

* Add editorconfig file

It's a cross-editor compatible config file that defines certain editor
behaviour (e.g. adding/removing newline at end of file)

It is supported by major editors like Visual Studio (Code) and by
version control  providers like GitHub.

Should end the constant adding/removing of final newline in PRs

* More settings

- unicode by default
- trim newlines
- use tabs for indentation (ugh)

* Ignore folder rename (#245)

* Hot reload banlist on player join (#233)

* added banlist hotreload

* fix formatting

* didnt append, cleared whole file oopsie

* unfuckedunban not rewriting file

* fixed not checking for new line

Co-authored-by: ScureX <47725553+ScureX@users.noreply.github.com>

* Refactor cleanup (#256)

* Fix indentation

* Fix path in clang-format command in readme

* Refactor cleanup (some formatting fixes) (#257)

* Fix some formatting

* More formatting fixes

* add scriptdatatable.cpp rewrite

* Some formatting fixes (#260)

* More formatting stuff (#261)

* various formatting changes and fixes

* Fix changed icon (#264)

* clang format, fix issues with server registration and rpak loading

* fix more formatting

* update postbuild step

* set launcher directory and error on fail creating log files

* change some stuff in exploitfixes

* only unrestrict dev commands when commandline flag is present

* fix issues with cvar flag commit

* fixup command flags better and reformat

* bring up to date with main

* fixup formatting

* improve cvar flag fixup and remove temp thing from findflags

* set serverfilter better

* avoid ptr decay when setting auth token

* add more entity functions

* Fix the MS server registration issues. (#285)

* Port ms presence reporter to std::async

* Fix crash due to std::optional being assigned nullptr.

* Fix formatting.

* Wait 20 seconds if MS returns DUPLICATE_SERVER.

* Change PERSISTENCE_MAX_SIZE to fix player authentication (#287)

The size check added in the refactor was incorrect:

- 56306: expected pdata size based on the pdef
- 512: allowance for trailing junk (r2 adds 137 bytes of trailing junk)
- 100: for some wiggle room

Co-Authored-By: pg9182 <96569817+pg9182@users.noreply.github.com>

* change miscserverscript to use actual entity arguments rather than
player index jank

* Fix token clearing hook (#290)

A certain someone forgot to put an `0x` in front of their hex number, meaning the offset is wrong.
This would cause token to be leaked again

Co-authored-by: Maya <malte.hoermeyer@web.de>
Co-authored-by: RoyalBlue1 <realEmail@veryRealURL.com>
Co-authored-by: GeckoEidechse <40122905+GeckoEidechse@users.noreply.github.com>
Co-authored-by: ScureX <47725553+ScureX@users.noreply.github.com>
Co-authored-by: Erlite <ys.aameziane@gmail.com>
Co-authored-by: Emma Miler <emma.pi@protonmail.com>
Co-authored-by: pg9182 <96569817+pg9182@users.noreply.github.com>
This commit is contained in:
BobTheBob 2022-10-17 23:26:07 +01:00 committed by GitHub
parent dc0934d29c
commit 841881af9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
155 changed files with 11504 additions and 9865 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,15 @@
#include "pch.h"
#include "audio.h"
#include "dedicated.h"
#include "convar.h"
#include "rapidjson/error/en.h"
#include <fstream>
#include <iostream>
#include <sstream>
#include <random>
#include "convar.h"
AUTOHOOK_INIT()
extern "C"
{
@ -229,10 +231,8 @@ EventOverrideData::EventOverrideData(const std::string& data, const fs::path& pa
}
// read from after the header first to preserve the empty header, then read the header last
wavStream.seekg(sizeof(EMPTY_WAVE), std::ios::beg);
wavStream.read(reinterpret_cast<char*>(&data[sizeof(EMPTY_WAVE)]), fileSize - sizeof(EMPTY_WAVE));
wavStream.seekg(0, std::ios::beg);
wavStream.read(reinterpret_cast<char*>(data), sizeof(EMPTY_WAVE));
wavStream.read(reinterpret_cast<char*>(data), fileSize);
wavStream.close();
spdlog::info("Finished async read of audio sample {}", pathString);
@ -315,6 +315,7 @@ void CustomAudioManager::ClearAudioOverrides()
{
// stop all miles sounds beforehand
// miles_stop_all
MilesStopAll();
// this is cancer but it works
@ -323,15 +324,12 @@ void CustomAudioManager::ClearAudioOverrides()
// 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(g_CustomAudioManager.m_loadingMutex);
std::unique_lock lock(m_loadingMutex);
m_loadedAudioOverrides.clear();
m_loadedAudioOverridesRegex.clear();
}
typedef bool (*LoadSampleMetadata_Type)(void* sample, void* audioBuffer, unsigned int audioBufferLength, int audioType);
LoadSampleMetadata_Type LoadSampleMetadata_Original;
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);
@ -368,6 +366,26 @@ bool ShouldPlayAudioEvent(const char* eventName, const std::shared_ptr<EventOver
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(
@ -398,7 +416,7 @@ bool __declspec(noinline) __fastcall LoadSampleMetadata_Internal(
if (!overrideData)
// not found either
return LoadSampleMetadata_Original(sample, audioBuffer, audioBufferLength, audioType);
return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType);
else
{
// cache found pattern to improve performance
@ -412,7 +430,7 @@ bool __declspec(noinline) __fastcall LoadSampleMetadata_Internal(
overrideData = iter->second;
if (!ShouldPlayAudioEvent(eventName, overrideData))
return LoadSampleMetadata_Original(sample, audioBuffer, audioBufferLength, audioType);
return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType);
void* data = 0;
unsigned int dataLength = 0;
@ -454,7 +472,7 @@ bool __declspec(noinline) __fastcall LoadSampleMetadata_Internal(
if (!data)
{
spdlog::warn("Could not fetch override sample data for event {}! Using original data instead.", eventName);
return LoadSampleMetadata_Original(sample, audioBuffer, audioBufferLength, audioType);
return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType);
}
audioBuffer = data;
@ -465,51 +483,25 @@ bool __declspec(noinline) __fastcall LoadSampleMetadata_Internal(
*(unsigned int*)((uintptr_t)sample + 0xF0) = audioBufferLength;
// 64 - Auto-detect sample type
bool res = LoadSampleMetadata_Original(sample, audioBuffer, audioBufferLength, 64);
bool res = LoadSampleMetadata(sample, audioBuffer, audioBufferLength, 64);
if (!res)
spdlog::error("LoadSampleMetadata failed! The game will crash :(");
return res;
}
// 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.
bool __fastcall LoadSampleMetadata_Hook(void* sample, void* audioBuffer, unsigned int audioBufferLength, int audioType)
{
uintptr_t parentEvent = (uintptr_t)Audio_GetParentEvent();
// Raw source, used for voice data only
if (audioType == 0)
return LoadSampleMetadata_Original(sample, audioBuffer, audioBufferLength, audioType);
return LoadSampleMetadata_Internal(parentEvent, sample, audioBuffer, audioBufferLength, audioType);
}
typedef bool (*MilesLog_Type)(int level, const char* string);
MilesLog_Type MilesLog_Original;
void __fastcall MilesLog_Hook(int level, const char* string)
// clang-format off
AUTOHOOK(MilesLog, client.dll + 0x57DAD0,
void, __fastcall, (int level, const char* string))
// clang-format on
{
spdlog::info("[MSS] {} - {}", level, string);
}
void InitialiseMilesAudioHooks(HMODULE baseAddress)
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, "");
if (IsDedicatedServer())
return;
uintptr_t milesAudioBase = (uintptr_t)GetModuleHandleA("mileswin64.dll");
if (!milesAudioBase)
return spdlog::error("miles audio not found :terror:");
HookEnabler hook;
ENABLER_CREATEHOOK(
hook, (char*)milesAudioBase + 0xF110, &LoadSampleMetadata_Hook, reinterpret_cast<LPVOID*>(&LoadSampleMetadata_Original));
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x57DAD0, &MilesLog_Hook, reinterpret_cast<LPVOID*>(&MilesLog_Original));
MilesStopAll = (MilesStopAll_Type)((char*)baseAddress + 0x580850);
MilesStopAll = module.Offset(0x580850).As<MilesStopAll_Type>();
}

View File

@ -5,8 +5,6 @@
#include <regex>
#include <shared_mutex>
namespace fs = std::filesystem;
enum class AudioSelectionStrategy
{
INVALID = -1,
@ -46,5 +44,3 @@ class CustomAudioManager
};
extern CustomAudioManager g_CustomAudioManager;
void InitialiseMilesAudioHooks(HMODULE baseAddress);

View File

@ -1,26 +1,28 @@
#pragma once
#include "pch.h"
#include "bansystem.h"
#include "serverauthentication.h"
#include "maxplayers.h"
#include "concommand.h"
#include "miscserverscript.h"
#include <filesystem>
#include "r2server.h"
#include "r2engine.h"
#include "nsprefix.h"
#include <ctime>
#include <filesystem>
const char* BANLIST_PATH_SUFFIX = "/banlist.txt";
const char BANLIST_COMMENT_CHAR = '#';
ServerBanSystem* g_ServerBanSystem;
ServerBanSystem* g_pBanSystem;
void ServerBanSystem::OpenBanlist()
{
std::ifstream enabledModsStream(GetNorthstarPrefix() + "/banlist.txt");
std::stringstream enabledModsStringStream;
std::ifstream banlistStream(GetNorthstarPrefix() + "/banlist.txt");
if (!enabledModsStream.fail())
if (!banlistStream.fail())
{
std::string line;
while (std::getline(enabledModsStream, line))
while (std::getline(banlistStream, line))
{
// ignore line if first char is # or line is empty
if (line == "" || line.front() == BANLIST_COMMENT_CHAR)
@ -41,7 +43,7 @@ void ServerBanSystem::OpenBanlist()
m_vBannedUids.push_back(strtoull(uid.c_str(), nullptr, 10));
}
enabledModsStream.close();
banlistStream.close();
}
// open write stream for banlist // dont do this to allow for all time access
@ -182,15 +184,14 @@ void ConCommand_ban(const CCommand& args)
if (args.ArgC() < 2)
return;
// assuming maxplayers 32
for (int i = 0; i < 32; i++)
for (int i = 0; i < R2::GetMaxPlayers(); i++)
{
void* player = GetPlayerByIndex(i);
R2::CBaseClient* player = &R2::g_pClientArray[i];
if (!strcmp((char*)player + 0x16, args.Arg(1)) || !strcmp((char*)player + 0xF500, args.Arg(1)))
if (!strcmp(player->m_Name, args.Arg(1)) || !strcmp(player->m_UID, args.Arg(1)))
{
g_ServerBanSystem->BanUID(strtoull((char*)player + 0xF500, nullptr, 10));
CBaseClient__Disconnect(player, 1, "Banned from server");
g_pBanSystem->BanUID(strtoull(player->m_UID, nullptr, 10));
R2::CBaseClient__Disconnect(player, 1, "Banned from server");
break;
}
}
@ -202,20 +203,20 @@ void ConCommand_unban(const CCommand& args)
return;
// assumedly the player being unbanned here wasn't already connected, so don't need to iterate over players or anything
g_ServerBanSystem->UnbanUID(strtoull(args.Arg(1), nullptr, 10));
g_pBanSystem->UnbanUID(strtoull(args.Arg(1), nullptr, 10));
}
void ConCommand_clearbanlist(const CCommand& args)
{
g_ServerBanSystem->ClearBanlist();
g_pBanSystem->ClearBanlist();
}
void InitialiseBanSystem(HMODULE baseAddress)
ON_DLL_LOAD_RELIESON("engine.dll", BanSystem, ConCommand, (CModule module))
{
g_ServerBanSystem = new ServerBanSystem;
g_ServerBanSystem->OpenBanlist();
g_pBanSystem = new ServerBanSystem;
g_pBanSystem->OpenBanlist();
RegisterConCommand("ban", ConCommand_ban, "bans a given player by uid or name", FCVAR_GAMEDLL);
RegisterConCommand("unban", ConCommand_unban, "unbans a given player by uid", FCVAR_NONE);
RegisterConCommand("clearbanlist", ConCommand_clearbanlist, "clears all uids on the banlist", FCVAR_NONE);
RegisterConCommand("unban", ConCommand_unban, "unbans a given player by uid", FCVAR_GAMEDLL);
RegisterConCommand("clearbanlist", ConCommand_clearbanlist, "clears all uids on the banlist", FCVAR_GAMEDLL);
}

View File

@ -16,6 +16,4 @@ class ServerBanSystem
bool IsUIDAllowed(uint64_t uid);
};
extern ServerBanSystem* g_ServerBanSystem;
void InitialiseBanSystem(HMODULE baseAddress);
extern ServerBanSystem* g_pBanSystem;

View File

@ -1,19 +1,19 @@
#include "pch.h"
#include "buildainfile.h"
#include "convar.h"
#include "hookutils.h"
#include "hoststate.h"
#include "r2engine.h"
#include <fstream>
#include <filesystem>
#include "nsmem.h"
namespace fs = std::filesystem;
AUTOHOOK_INIT()
const int AINET_VERSION_NUMBER = 57;
const int AINET_SCRIPT_VERSION_NUMBER = 21;
const int MAP_VERSION_TEMP = 30;
const int PLACEHOLDER_CRC = 0;
const int MAX_HULLS = 5;
#pragma pack(push, 1)
struct CAI_NodeLink
{
short srcId;
@ -24,6 +24,7 @@ struct CAI_NodeLink
char unk2[5];
int64_t flags;
};
#pragma pack(pop)
#pragma pack(push, 1)
struct CAI_NodeLinkDisk
@ -33,7 +34,9 @@ struct CAI_NodeLinkDisk
char unk0;
bool hulls[MAX_HULLS];
};
#pragma pack(pop)
#pragma pack(push, 1)
struct CAI_Node
{
int index; // not present on disk
@ -62,6 +65,7 @@ struct CAI_Node
char unk9[8]; // padding until next bit
char unk10[8]; // should match up to unk6 on disk
};
#pragma pack(pop)
// the way CAI_Nodes are represented in on-disk ain files
#pragma pack(push, 1)
@ -81,7 +85,9 @@ struct CAI_NodeDisk
short unk5;
char unk6[8];
}; // total size of 68 bytes
#pragma pack(pop)
#pragma pack(push, 1)
struct UnkNodeStruct0
{
int index;
@ -106,10 +112,12 @@ struct UnkNodeStruct0
char pad4[132];
char unk5;
};
#pragma pack(pop)
int* pUnkStruct0Count;
UnkNodeStruct0*** pppUnkNodeStruct0s;
#pragma pack(push, 1)
struct UnkLinkStruct1
{
short unk0;
@ -119,10 +127,12 @@ struct UnkLinkStruct1
char unk4;
char unk5;
};
#pragma pack(pop)
int* pUnkLinkStruct1Count;
UnkLinkStruct1*** pppUnkStruct1s;
#pragma pack(push, 1)
struct CAI_ScriptNode
{
float x;
@ -130,7 +140,9 @@ struct CAI_ScriptNode
float z;
uint64_t scriptdata;
};
#pragma pack(pop)
#pragma pack(push, 1)
struct CAI_Network
{
// +0
@ -160,16 +172,16 @@ struct CAI_Network
// +84176
CAI_Node** nodes;
};
#pragma pack(pop)
char** pUnkServerMapversionGlobal;
char* pMapName;
ConVar* Cvar_ns_ai_dumpAINfileFromLoad;
void DumpAINInfo(CAI_Network* aiNetwork)
{
fs::path writePath("r2/maps/graphs");
writePath /= pMapName;
fs::path writePath(fmt::format("{}/maps/graphs", R2::g_pModName));
writePath /= R2::g_pHostState->m_levelName;
writePath += ".ain";
// dump from memory
@ -349,20 +361,20 @@ void DumpAINInfo(CAI_Network* aiNetwork)
writeStream.close();
}
typedef void (*CAI_NetworkBuilder__BuildType)(void* builder, CAI_Network* aiNetwork, void* unknown);
CAI_NetworkBuilder__BuildType CAI_NetworkBuilder__Build;
void CAI_NetworkBuilder__BuildHook(void* builder, CAI_Network* aiNetwork, void* unknown)
// clang-format off
AUTOHOOK(CAI_NetworkBuilder__Build, server.dll + 0x385E20,
void, __fastcall, (void* builder, CAI_Network* aiNetwork, void* unknown))
// clang-format on
{
CAI_NetworkBuilder__Build(builder, aiNetwork, unknown);
DumpAINInfo(aiNetwork);
}
typedef void (*LoadAINFileType)(void* aimanager, void* buf, const char* filename);
LoadAINFileType LoadAINFile;
void LoadAINFileHook(void* aimanager, void* buf, const char* filename)
// clang-format off
AUTOHOOK(LoadAINFile, server.dll + 0x3933A0,
void, __fastcall, (void* aimanager, void* buf, const char* filename))
// clang-format on
{
LoadAINFile(aimanager, buf, filename);
@ -373,28 +385,16 @@ void LoadAINFileHook(void* aimanager, void* buf, const char* filename)
}
}
void InitialiseBuildAINFileHooks(HMODULE baseAddress)
ON_DLL_LOAD("server.dll", BuildAINFile, (CModule module))
{
AUTOHOOK_DISPATCH()
Cvar_ns_ai_dumpAINfileFromLoad = new ConVar(
"ns_ai_dumpAINfileFromLoad", "0", FCVAR_NONE, "For debugging: whether we should dump ain data for ains loaded from disk");
HookEnabler hook;
ENABLER_CREATEHOOK(
hook, (char*)baseAddress + 0x385E20, &CAI_NetworkBuilder__BuildHook, reinterpret_cast<LPVOID*>(&CAI_NetworkBuilder__Build));
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x3933A0, &LoadAINFileHook, reinterpret_cast<LPVOID*>(&LoadAINFile));
pUnkStruct0Count = (int*)((char*)baseAddress + 0x1063BF8);
pppUnkNodeStruct0s = (UnkNodeStruct0***)((char*)baseAddress + 0x1063BE0);
pUnkLinkStruct1Count = (int*)((char*)baseAddress + 0x1063AA8);
pppUnkStruct1s = (UnkLinkStruct1***)((char*)baseAddress + 0x1063A90);
pUnkServerMapversionGlobal = (char**)((char*)baseAddress + 0xBFBE08);
pMapName = (char*)baseAddress + 0x1053370;
uintptr_t base = (uintptr_t)baseAddress;
// remove a check that prevents a logging function in link generation from working
// due to the sheer amount of logging this is a massive perf hit to generation, but spewlog_enable 0 exists so whatever
NSMem::NOP(base + 0x3889B6, 6);
NSMem::NOP(base + 0x3889BF, 6);
pUnkStruct0Count = module.Offset(0x1063BF8).As<int*>();
pppUnkNodeStruct0s = module.Offset(0x1063BE0).As<UnkNodeStruct0***>();
pUnkLinkStruct1Count = module.Offset(0x1063AA8).As<int*>();
pppUnkStruct1s = module.Offset(0x1063A90).As<UnkLinkStruct1***>();
pUnkServerMapversionGlobal = module.Offset(0xBFBE08).As<char**>();
}

View File

@ -1,3 +0,0 @@
#pragma once
void InitialiseBuildAINFileHooks(HMODULE baseAddress);

View File

@ -1,13 +1,11 @@
#include "pch.h"
#include "convar.h"
#include "concommand.h"
#include "chatcommand.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
typedef void(__fastcall* ClientSayTextType)(void* a1, const char* message, __int64 isIngameChat, bool isTeamChat);
ClientSayTextType ClientSayText;
void(__fastcall* ClientSayText)(void* a1, const char* message, uint64_t isIngameChat, bool isTeamChat);
void ConCommand_say(const CCommand& args)
{
@ -29,9 +27,9 @@ void ConCommand_log(const CCommand& args)
}
}
void InitialiseChatCommands(HMODULE baseAddress)
ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", ClientChatCommand, ConCommand, (CModule module))
{
ClientSayText = (ClientSayTextType)((char*)baseAddress + 0x54780);
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);

View File

@ -1,3 +0,0 @@
#pragma once
void InitialiseChatCommands(HMODULE baseAddress);

View File

@ -1,12 +1,9 @@
#include "pch.h"
#include "clientauthhooks.h"
#include "hookutils.h"
#include "gameutils.h"
#include "masterserver.h"
#include "convar.h"
#include "r2client.h"
typedef void (*AuthWithStryderType)(void* a1);
AuthWithStryderType AuthWithStryder;
AUTOHOOK_INIT()
ConVar* Cvar_ns_has_agreed_to_send_token;
@ -15,51 +12,53 @@ const int NOT_DECIDED_TO_SEND_TOKEN = 0;
const int AGREED_TO_SEND_TOKEN = 1;
const int DISAGREED_TO_SEND_TOKEN = 2;
typedef char* (*Auth3PTokenType)();
Auth3PTokenType Auth3PToken;
char* token_location = 0x0;
void AuthWithStryderHook(void* a1)
// 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_MasterServerManager->m_bOriginAuthWithMasterServerDone && Cvar_ns_has_agreed_to_send_token->GetInt() != DISAGREED_TO_SEND_TOKEN)
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_MasterServerManager->m_bOriginAuthWithMasterServerInProgress)
g_MasterServerManager->AuthenticateOriginWithMasterServer(g_LocalPlayerUserID, g_LocalPlayerOriginToken);
!g_pMasterServerManager->m_bOriginAuthWithMasterServerInProgress)
g_pMasterServerManager->AuthenticateOriginWithMasterServer(R2::g_pLocalPlayerUserID, R2::g_pLocalPlayerOriginToken);
// invalidate key so auth will fail
*g_LocalPlayerOriginToken = 0;
*R2::g_pLocalPlayerOriginToken = 0;
}
AuthWithStryder(a1);
}
char* Auth3PTokenHook()
char* p3PToken;
// clang-format off
AUTOHOOK(Auth3PToken, engine.dll + 0x183760,
char*, __fastcall, ())
// clang-format on
{
if (g_MasterServerManager->m_sOwnClientAuthToken[0] != 0)
if (g_pMasterServerManager->m_sOwnClientAuthToken[0])
{
memset(token_location, 0x0, 1024);
strcpy(token_location, "Protocol 3: Protect the Pilot");
memset(p3PToken, 0x0, 1024);
strcpy(p3PToken, "Protocol 3: Protect the Pilot");
}
return Auth3PToken();
}
void InitialiseClientAuthHooks(HMODULE baseAddress)
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");
HookEnabler hook;
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x1843A0, &AuthWithStryderHook, reinterpret_cast<LPVOID*>(&AuthWithStryder));
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x183760, &Auth3PTokenHook, reinterpret_cast<LPVOID*>(&Auth3PToken));
token_location = (char*)baseAddress + 0x13979D80;
}

View File

@ -1,2 +0,0 @@
#pragma once
void InitialiseClientAuthHooks(HMODULE baseAddress);

View File

@ -1,29 +1,22 @@
#include "pch.h"
#include "clientchathooks.h"
#include <rapidjson/document.h>
#include "squirrel.h"
#include "serverchathooks.h"
#include "localchatwriter.h"
typedef void(__fastcall* CHudChat__AddGameLineType)(void* self, const char* message, int fromPlayerId, bool isteam, bool isdead);
CHudChat__AddGameLineType CHudChat__AddGameLine;
#include <rapidjson/document.h>
struct ChatTags
{
bool whisper;
bool team;
bool dead;
};
AUTOHOOK_INIT()
static void CHudChat__AddGameLineHook(void* self, const char* message, int inboxId, bool isTeam, bool isDead)
// clang-format off
AUTOHOOK(CHudChat__AddGameLine, client.dll + 0x22E580,
void, __fastcall, (void* self, const char* message, int inboxId, bool isTeam, bool isDead))
// clang-format on
{
// This hook is called for each HUD, but we only want our logic to run once.
if (self != *CHudChat::allHuds)
{
return;
}
if (g_ClientSquirrelManager->setupfunc("CHudChat_ProcessMessageStartThread") != SQRESULT_ERROR)
if (g_pSquirrel<ScriptContext::CLIENT>->setupfunc("CHudChat_ProcessMessageStartThread") != SQRESULT_ERROR)
{
int senderId = inboxId & CUSTOM_MESSAGE_INDEX_MASK;
bool isAnonymous = senderId == 0;
@ -38,58 +31,53 @@ static void CHudChat__AddGameLineHook(void* self, const char* message, int inbox
payload = message + 1;
}
g_ClientSquirrelManager->pusharg((int)senderId - 1);
g_ClientSquirrelManager->pusharg(payload);
g_ClientSquirrelManager->pusharg(isTeam);
g_ClientSquirrelManager->pusharg(isDead);
g_ClientSquirrelManager->pusharg(type);
g_ClientSquirrelManager->call(5);
g_pSquirrel<ScriptContext::CLIENT>->pushinteger(g_pSquirrel<ScriptContext::CLIENT>->m_pSQVM->sqvm, (int)senderId - 1);
g_pSquirrel<ScriptContext::CLIENT>->pushstring(g_pSquirrel<ScriptContext::CLIENT>->m_pSQVM->sqvm, payload);
g_pSquirrel<ScriptContext::CLIENT>->pushbool(g_pSquirrel<ScriptContext::CLIENT>->m_pSQVM->sqvm, isTeam);
g_pSquirrel<ScriptContext::CLIENT>->pushbool(g_pSquirrel<ScriptContext::CLIENT>->m_pSQVM->sqvm, isDead);
g_pSquirrel<ScriptContext::CLIENT>->pushinteger(g_pSquirrel<ScriptContext::CLIENT>->m_pSQVM->sqvm, type);
g_pSquirrel<ScriptContext::CLIENT>->call(g_pSquirrel<ScriptContext::CLIENT>->m_pSQVM->sqvm, 5);
}
else
{
for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next)
{
CHudChat__AddGameLine(hud, message, inboxId, isTeam, isDead);
}
}
}
// void NSChatWrite( int context, string str )
static SQRESULT SQ_ChatWrite(void* sqvm)
SQRESULT SQ_ChatWrite(HSquirrelVM* sqvm)
{
int context = ClientSq_getinteger(sqvm, 1);
const char* str = ClientSq_getstring(sqvm, 2);
int context = g_pSquirrel<ScriptContext::CLIENT>->getinteger(sqvm, 1);
const char* str = g_pSquirrel<ScriptContext::CLIENT>->getstring(sqvm, 2);
LocalChatWriter((LocalChatWriter::Context)context).Write(str);
return SQRESULT_NOTNULL;
return SQRESULT_NULL;
}
// void NSChatWriteRaw( int context, string str )
static SQRESULT SQ_ChatWriteRaw(void* sqvm)
SQRESULT SQ_ChatWriteRaw(HSquirrelVM* sqvm)
{
int context = ClientSq_getinteger(sqvm, 1);
const char* str = ClientSq_getstring(sqvm, 2);
int context = g_pSquirrel<ScriptContext::CLIENT>->getinteger(sqvm, 1);
const char* str = g_pSquirrel<ScriptContext::CLIENT>->getstring(sqvm, 2);
LocalChatWriter((LocalChatWriter::Context)context).InsertText(str);
return SQRESULT_NOTNULL;
return SQRESULT_NULL;
}
// void NSChatWriteLine( int context, string str )
static SQRESULT SQ_ChatWriteLine(void* sqvm)
SQRESULT SQ_ChatWriteLine(HSquirrelVM* sqvm)
{
int context = ClientSq_getinteger(sqvm, 1);
const char* str = ClientSq_getstring(sqvm, 2);
int context = g_pSquirrel<ScriptContext::CLIENT>->getinteger(sqvm, 1);
const char* str = g_pSquirrel<ScriptContext::CLIENT>->getstring(sqvm, 2);
LocalChatWriter((LocalChatWriter::Context)context).WriteLine(str);
return SQRESULT_NOTNULL;
return SQRESULT_NULL;
}
void InitialiseClientChatHooks(HMODULE baseAddress)
ON_DLL_LOAD_CLIENT_RELIESON("client.dll", ClientChatHooks, ClientSquirrel, (CModule module))
{
HookEnabler hook;
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x22E580, &CHudChat__AddGameLineHook, reinterpret_cast<LPVOID*>(&CHudChat__AddGameLine));
AUTOHOOK_DISPATCH()
g_ClientSquirrelManager->AddFuncRegistration("void", "NSChatWrite", "int context, string text", "", SQ_ChatWrite);
g_ClientSquirrelManager->AddFuncRegistration("void", "NSChatWriteRaw", "int context, string text", "", SQ_ChatWriteRaw);
g_ClientSquirrelManager->AddFuncRegistration("void", "NSChatWriteLine", "int context, string text", "", SQ_ChatWriteLine);
g_pSquirrel<ScriptContext::CLIENT>->AddFuncRegistration("void", "NSChatWrite", "int context, string text", "", SQ_ChatWrite);
g_pSquirrel<ScriptContext::CLIENT>->AddFuncRegistration("void", "NSChatWriteRaw", "int context, string text", "", SQ_ChatWriteRaw);
g_pSquirrel<ScriptContext::CLIENT>->AddFuncRegistration("void", "NSChatWriteLine", "int context, string text", "", SQ_ChatWriteLine);
}

View File

@ -1,5 +0,0 @@
#pragma once
#include "pch.h"
#include "serverchathooks.h"
void InitialiseClientChatHooks(HMODULE baseAddress);

View File

@ -1,13 +1,14 @@
#include "pch.h"
#include "clientruihooks.h"
#include "convar.h"
AUTOHOOK_INIT()
ConVar* Cvar_rui_drawEnable;
typedef char (*DrawRUIFuncType)(void* a1, float* a2);
DrawRUIFuncType DrawRUIFunc;
char DrawRUIFuncHook(void* a1, float* a2)
// clang-format off
AUTOHOOK(DrawRUIFunc, engine.dll + 0xFC500,
bool, __fastcall, (void* a1, float* a2))
// clang-format on
{
if (!Cvar_rui_drawEnable->GetBool())
return 0;
@ -15,10 +16,9 @@ char DrawRUIFuncHook(void* a1, float* a2)
return DrawRUIFunc(a1, a2);
}
void InitialiseEngineClientRUIHooks(HMODULE baseAddress)
ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", RUI, ConVar, (CModule module))
{
Cvar_rui_drawEnable = new ConVar("rui_drawEnable", "1", FCVAR_CLIENTDLL, "Controls whether RUI should be drawn");
AUTOHOOK_DISPATCH()
HookEnabler hook;
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0xFC500, &DrawRUIFuncHook, reinterpret_cast<LPVOID*>(&DrawRUIFunc));
Cvar_rui_drawEnable = new ConVar("rui_drawEnable", "1", FCVAR_CLIENTDLL, "Controls whether RUI should be drawn");
}

View File

@ -1,2 +0,0 @@
#pragma once
void InitialiseEngineClientRUIHooks(HMODULE baseAddress);

View File

@ -1,21 +1,21 @@
#include "pch.h"
#include "clientvideooverrides.h"
#include "modmanager.h"
#include "nsmem.h"
typedef void* (*BinkOpenType)(const char* path, uint32_t flags);
BinkOpenType BinkOpen;
AUTOHOOK_INIT()
void* BinkOpenHook(const char* path, uint32_t flags)
// 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_ModManager->m_loadedMods)
for (Mod& mod : g_pModManager->m_LoadedMods)
{
if (!mod.Enabled)
if (!mod.m_bEnabled)
continue;
if (std::find(mod.BinkVideos.begin(), mod.BinkVideos.end(), filename) != mod.BinkVideos.end())
@ -25,23 +25,18 @@ void* BinkOpenHook(const char* path, uint32_t flags)
if (fileOwner)
{
// create new path
fs::path binkPath(fileOwner->ModDirectory / "media" / filename);
fs::path binkPath(fileOwner->m_ModDirectory / "media" / filename);
return BinkOpen(binkPath.string().c_str(), flags);
}
else
return BinkOpen(path, flags);
}
void InitialiseEngineClientVideoOverrides(HMODULE baseAddress)
ON_DLL_LOAD_CLIENT("client.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
NSMem::NOP((uintptr_t)baseAddress + 0x459AD, 6);
HookEnabler hook;
ENABLER_CREATEHOOK(
hook,
reinterpret_cast<void*>(GetProcAddress(GetModuleHandleA("bink2w64.dll"), "BinkOpen")),
&BinkOpenHook,
reinterpret_cast<LPVOID*>(&BinkOpen));
module.Offset(0x459AD).NOP(6);
}

View File

@ -1,2 +0,0 @@
#pragma once
void InitialiseEngineClientVideoOverrides(HMODULE baseAddress);

View File

@ -1,28 +1,9 @@
#include "pch.h"
#include "concommand.h"
#include "gameutils.h"
#include "misccommands.h"
#include <iostream>
typedef void (*ConCommandConstructorType)(
ConCommand* newCommand, const char* name, void (*callback)(const CCommand&), const char* helpString, int flags, void* parent);
ConCommandConstructorType conCommandConstructor;
void RegisterConCommand(const char* name, void (*callback)(const CCommand&), const char* helpString, int flags)
{
spdlog::info("Registering ConCommand {}", name);
// no need to free this ever really, it should exist as long as game does
ConCommand* newCommand = new ConCommand;
conCommandConstructor(newCommand, name, callback, helpString, flags, nullptr);
}
void InitialiseConCommands(HMODULE baseAddress)
{
conCommandConstructor = (ConCommandConstructorType)((char*)baseAddress + 0x415F60);
AddMiscConCommands();
}
//-----------------------------------------------------------------------------
// Purpose: Returns true if this is a command
// Output : bool
@ -57,7 +38,7 @@ bool ConCommandBase::IsRegistered(void) const
//-----------------------------------------------------------------------------
bool ConCommandBase::IsFlagSet(int nFlags) const
{
return false; // !TODO: Returning false on every query? (original implementation in Northstar before ConCommandBase refactor)
return m_nFlags & nFlags;
}
//-----------------------------------------------------------------------------
@ -138,3 +119,33 @@ char* ConCommandBase::CopyString(const char* szFrom) const
}
return szTo;
}
typedef void (*ConCommandConstructorType)(
ConCommand* newCommand, const char* name, FnCommandCallback_t callback, const char* helpString, int flags, void* parent);
ConCommandConstructorType ConCommandConstructor;
void RegisterConCommand(const char* name, FnCommandCallback_t callback, const char* helpString, int flags)
{
spdlog::info("Registering ConCommand {}", name);
// no need to free this ever really, it should exist as long as game does
ConCommand* newCommand = new ConCommand;
ConCommandConstructor(newCommand, name, callback, helpString, flags, nullptr);
}
void RegisterConCommand(
const char* name, FnCommandCallback_t callback, const char* helpString, int flags, FnCommandCompletionCallback completionCallback)
{
spdlog::info("Registering ConCommand {}", name);
// no need to free this ever really, it should exist as long as game does
ConCommand* newCommand = new ConCommand;
ConCommandConstructor(newCommand, name, callback, helpString, flags, nullptr);
newCommand->m_pCompletionCallback = completionCallback;
}
ON_DLL_LOAD("engine.dll", ConCommand, (CModule module))
{
ConCommandConstructor = module.Offset(0x415F60).As<ConCommandConstructorType>();
AddMiscConCommands();
}

View File

@ -72,6 +72,20 @@ inline const char* CCommand::operator[](int nIndex) const
return Arg(nIndex);
}
//-----------------------------------------------------------------------------
// Called when a ConCommand needs to execute
//-----------------------------------------------------------------------------
typedef void (*FnCommandCallback_t)(const CCommand& command);
#define COMMAND_COMPLETION_MAXITEMS 64
#define COMMAND_COMPLETION_ITEM_LENGTH 128
//-----------------------------------------------------------------------------
// Returns 0 to COMMAND_COMPLETION_MAXITEMS worth of completion strings
//-----------------------------------------------------------------------------
typedef int (*__fastcall FnCommandCompletionCallback)(
const char* partial, char commands[COMMAND_COMPLETION_MAXITEMS][COMMAND_COMPLETION_ITEM_LENGTH]);
// From r5reloaded
class ConCommandBase
{
@ -113,8 +127,8 @@ class ConCommand : public ConCommandBase
void Init(void);
bool IsCommand(void) const;
void* m_pCommandCallback {}; // 0x0040 <- starts from 0x40 since we inherit ConCommandBase.
void* m_pCompletionCallback {}; // 0x0048 <- defaults to sub_180417410 ('xor eax, eax').
FnCommandCallback_t m_pCommandCallback {}; // 0x0040 <- starts from 0x40 since we inherit ConCommandBase.
FnCommandCompletionCallback m_pCompletionCallback {}; // 0x0048 <- defaults to sub_180417410 ('xor eax, eax').
int m_nCallbackFlags {}; // 0x0050
char pad_0054[4]; // 0x0054
int unk0; // 0x0058
@ -122,6 +136,5 @@ class ConCommand : public ConCommandBase
}; // Size: 0x0060
void RegisterConCommand(const char* name, void (*callback)(const CCommand&), const char* helpString, int flags);
void InitialiseConCommands(HMODULE baseAddress);
#define MAKE_CONCMD(name, helpStr, flags, fn) RegisterConCommand(name, fn, helpStr, flags);
void RegisterConCommand(
const char* name, void (*callback)(const CCommand&), const char* helpString, int flags, FnCommandCompletionCallback completionCallback);

View File

@ -1,14 +0,0 @@
#include "pch.h"
#include "context.h"
const char* GetContextName(ScriptContext context)
{
if (context == ScriptContext::CLIENT)
return "CLIENT";
else if (context == ScriptContext::SERVER)
return "SERVER";
else if (context == ScriptContext::UI)
return "UI";
return "";
}

View File

@ -1,11 +0,0 @@
#pragma once
enum class ScriptContext : int
{
SERVER,
CLIENT,
UI,
NONE
};
const char* GetContextName(ScriptContext context);

View File

@ -1,13 +1,11 @@
#include <float.h>
#include "pch.h"
#include "bits.h"
#include "cvar.h"
#include "convar.h"
#include "hookutils.h"
#include "gameutils.h"
#include "sourceinterface.h"
#include <float.h>
typedef void (*ConVarRegisterType)(
ConVar* pConVar,
const char* pszName,
@ -27,25 +25,19 @@ ConVarMallocType conVarMalloc;
void* g_pConVar_Vtable = nullptr;
void* g_pIConVar_Vtable = nullptr;
typedef bool (*CvarIsFlagSetType)(ConVar* self, int flags);
CvarIsFlagSetType CvarIsFlagSet;
//-----------------------------------------------------------------------------
// Purpose: ConVar interface initialization
//-----------------------------------------------------------------------------
void InitialiseConVars(HMODULE baseAddress)
ON_DLL_LOAD("engine.dll", ConVar, (CModule module))
{
conVarMalloc = (ConVarMallocType)((char*)baseAddress + 0x415C20);
conVarRegister = (ConVarRegisterType)((char*)baseAddress + 0x417230);
conVarMalloc = module.Offset(0x415C20).As<ConVarMallocType>();
conVarRegister = module.Offset(0x417230).As<ConVarRegisterType>();
g_pConVar_Vtable = (char*)baseAddress + 0x67FD28;
g_pIConVar_Vtable = (char*)baseAddress + 0x67FDC8;
g_pConVar_Vtable = module.Offset(0x67FD28);
g_pIConVar_Vtable = module.Offset(0x67FDC8);
g_pCVarInterface = new SourceInterface<CCvar>("vstdlib.dll", "VEngineCvar007");
g_pCVar = *g_pCVarInterface;
HookEnabler hook;
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x417FA0, &ConVar::IsFlagSet, reinterpret_cast<LPVOID*>(&CvarIsFlagSet));
R2::g_pCVarInterface = new SourceInterface<CCvar>("vstdlib.dll", "VEngineCvar007");
R2::g_pCVar = *R2::g_pCVarInterface;
}
//-----------------------------------------------------------------------------
@ -74,7 +66,7 @@ ConVar::ConVar(
float fMin,
bool bMax,
float fMax,
void* pCallback)
FnChangeCallback_t pCallback)
{
spdlog::info("Registering Convar {}", pszName);
@ -476,16 +468,12 @@ bool ConVar::IsCommand(void) const
//-----------------------------------------------------------------------------
// Purpose: Test each ConVar query before setting the value.
// Input : *pConVar - nFlags
// Input : nFlags
// Output : False if change is permitted, true if not.
//-----------------------------------------------------------------------------
bool ConVar::IsFlagSet(ConVar* pConVar, int nFlags)
bool ConVar::IsFlagSet(int nFlags) const
{
// unrestrict FCVAR_DEVELOPMENTONLY and FCVAR_HIDDEN
if (pConVar && (nFlags == FCVAR_DEVELOPMENTONLY || nFlags == FCVAR_HIDDEN))
return false;
return CvarIsFlagSet(pConVar, nFlags);
return m_ConCommandBase.m_nFlags & nFlags;
}
//-----------------------------------------------------------------------------

View File

@ -58,15 +58,58 @@
// ClientCommand/NET_StringCmd/CBaseClientState::ProcessStringCmd.
#define FCVAR_SERVER_CANNOT_QUERY \
(1 << 29) // If this is set, then the server is not allowed to query this cvar's value (via IServerPluginHelpers::StartQueryCvarValue).
// !!!NOTE!!! : this is likely incorrect, there are multiple concommands that the vanilla game registers with this flag that 100% should not
// be remotely executable i.e. multiple commands that only exist on client (screenshot, joystick_initialize) we now use
// FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS in all places this flag was previously used
#define FCVAR_CLIENTCMD_CAN_EXECUTE \
(1 << 30) // IVEngineClient::ClientCmd is allowed to execute this command.
// Note: IVEngineClient::ClientCmd_Unrestricted can run any client command.
#define FCVAR_ACCESSIBLE_FROM_THREADS (1 << 25) // used as a debugging tool necessary to check material system thread convars
// TODO: could be cool to repurpose these for northstar use somehow?
// #define FCVAR_AVAILABLE (1<<26)
// #define FCVAR_AVAILABLE (1<<27)
// #define FCVAR_AVAILABLE (1<<31)
// flag => string stuff
const std::multimap<int, const char*> g_PrintCommandFlags = {
{FCVAR_UNREGISTERED, "UNREGISTERED"},
{FCVAR_DEVELOPMENTONLY, "DEVELOPMENTONLY"},
{FCVAR_GAMEDLL, "GAMEDLL"},
{FCVAR_CLIENTDLL, "CLIENTDLL"},
{FCVAR_HIDDEN, "HIDDEN"},
{FCVAR_PROTECTED, "PROTECTED"},
{FCVAR_SPONLY, "SPONLY"},
{FCVAR_ARCHIVE, "ARCHIVE"},
{FCVAR_NOTIFY, "NOTIFY"},
{FCVAR_USERINFO, "USERINFO"},
// TODO: PRINTABLEONLY and GAMEDLL_FOR_REMOTE_CLIENTS are both 1<<10, one is for vars and one is for commands
// this fucking sucks i think
{FCVAR_PRINTABLEONLY, "PRINTABLEONLY"},
{FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS, "GAMEDLL_FOR_REMOTE_CLIENTS"},
{FCVAR_UNLOGGED, "UNLOGGED"},
{FCVAR_NEVER_AS_STRING, "NEVER_AS_STRING"},
{FCVAR_REPLICATED, "REPLICATED"},
{FCVAR_CHEAT, "CHEAT"},
{FCVAR_SS, "SS"},
{FCVAR_DEMO, "DEMO"},
{FCVAR_DONTRECORD, "DONTRECORD"},
{FCVAR_SS_ADDED, "SS_ADDED"},
{FCVAR_RELEASE, "RELEASE"},
{FCVAR_RELOAD_MATERIALS, "RELOAD_MATERIALS"},
{FCVAR_RELOAD_TEXTURES, "RELOAD_TEXTURES"},
{FCVAR_NOT_CONNECTED, "NOT_CONNECTED"},
{FCVAR_MATERIAL_SYSTEM_THREAD, "MATERIAL_SYSTEM_THREAD"},
{FCVAR_ARCHIVE_PLAYERPROFILE, "ARCHIVE_PLAYERPROFILE"},
{FCVAR_SERVER_CAN_EXECUTE, "SERVER_CAN_EXECUTE"},
{FCVAR_SERVER_CANNOT_QUERY, "SERVER_CANNOT_QUERY"},
{FCVAR_CLIENTCMD_CAN_EXECUTE, "UNKNOWN"},
{FCVAR_ACCESSIBLE_FROM_THREADS, "ACCESSIBLE_FROM_THREADS"}};
//-----------------------------------------------------------------------------
// Forward declarations
//-----------------------------------------------------------------------------
@ -74,6 +117,8 @@ class ConCommandBase;
class ConCommand;
class ConVar;
typedef void (*FnChangeCallback_t)(ConVar* var, const char* pOldValue, float flOldValue);
//-----------------------------------------------------------------------------
// Purpose: A console variable
//-----------------------------------------------------------------------------
@ -91,7 +136,7 @@ class ConVar
float fMin,
bool bMax,
float fMax,
void* pCallback);
FnChangeCallback_t pCallback);
~ConVar(void);
const char* GetBaseName(void) const;
@ -125,7 +170,7 @@ class ConVar
bool IsRegistered(void) const;
bool IsCommand(void) const;
static bool IsFlagSet(ConVar* pConVar, int nFlags);
bool IsFlagSet(int nFlags) const;
struct CVValue_t
{
@ -145,5 +190,3 @@ class ConVar
void* m_pMalloc {}; // 0x0070
char m_pPad80[10] {}; // 0x0080
}; // Size: 0x0080
void InitialiseConVars(HMODULE baseAddress);

View File

@ -0,0 +1,216 @@
#include "pch.h"
#include "crashhandler.h"
#include "dedicated.h"
#include "nsprefix.h"
#include <minidumpapiset.h>
HANDLE hExceptionFilter;
long __stdcall ExceptionFilter(EXCEPTION_POINTERS* exceptionInfo)
{
static bool logged = false;
if (logged)
return EXCEPTION_CONTINUE_SEARCH;
if (!IsDebuggerPresent())
{
const DWORD exceptionCode = exceptionInfo->ExceptionRecord->ExceptionCode;
if (exceptionCode != EXCEPTION_ACCESS_VIOLATION && exceptionCode != EXCEPTION_ARRAY_BOUNDS_EXCEEDED &&
exceptionCode != EXCEPTION_DATATYPE_MISALIGNMENT && exceptionCode != EXCEPTION_FLT_DENORMAL_OPERAND &&
exceptionCode != EXCEPTION_FLT_DIVIDE_BY_ZERO && exceptionCode != EXCEPTION_FLT_INEXACT_RESULT &&
exceptionCode != EXCEPTION_FLT_INVALID_OPERATION && exceptionCode != EXCEPTION_FLT_OVERFLOW &&
exceptionCode != EXCEPTION_FLT_STACK_CHECK && exceptionCode != EXCEPTION_FLT_UNDERFLOW &&
exceptionCode != EXCEPTION_ILLEGAL_INSTRUCTION && exceptionCode != EXCEPTION_IN_PAGE_ERROR &&
exceptionCode != EXCEPTION_INT_DIVIDE_BY_ZERO && exceptionCode != EXCEPTION_INT_OVERFLOW &&
exceptionCode != EXCEPTION_INVALID_DISPOSITION && exceptionCode != EXCEPTION_NONCONTINUABLE_EXCEPTION &&
exceptionCode != EXCEPTION_PRIV_INSTRUCTION && exceptionCode != EXCEPTION_STACK_OVERFLOW)
return EXCEPTION_CONTINUE_SEARCH;
std::stringstream exceptionCause;
exceptionCause << "Cause: ";
switch (exceptionCode)
{
case EXCEPTION_ACCESS_VIOLATION:
case EXCEPTION_IN_PAGE_ERROR:
{
exceptionCause << "Access Violation" << std::endl;
auto exceptionInfo0 = exceptionInfo->ExceptionRecord->ExceptionInformation[0];
auto exceptionInfo1 = exceptionInfo->ExceptionRecord->ExceptionInformation[1];
if (!exceptionInfo0)
exceptionCause << "Attempted to read from: 0x" << (void*)exceptionInfo1;
else if (exceptionInfo0 == 1)
exceptionCause << "Attempted to write to: 0x" << (void*)exceptionInfo1;
else if (exceptionInfo0 == 8)
exceptionCause << "Data Execution Prevention (DEP) at: 0x" << (void*)std::hex << exceptionInfo1;
else
exceptionCause << "Unknown access violation at: 0x" << (void*)exceptionInfo1;
break;
}
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
exceptionCause << "Array bounds exceeded";
break;
case EXCEPTION_DATATYPE_MISALIGNMENT:
exceptionCause << "Datatype misalignment";
break;
case EXCEPTION_FLT_DENORMAL_OPERAND:
exceptionCause << "Denormal operand";
break;
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
exceptionCause << "Divide by zero (float)";
break;
case EXCEPTION_INT_DIVIDE_BY_ZERO:
exceptionCause << "Divide by zero (int)";
break;
case EXCEPTION_FLT_INEXACT_RESULT:
exceptionCause << "Inexact result";
break;
case EXCEPTION_FLT_INVALID_OPERATION:
exceptionCause << "Invalid operation";
break;
case EXCEPTION_FLT_OVERFLOW:
case EXCEPTION_INT_OVERFLOW:
exceptionCause << "Numeric overflow";
break;
case EXCEPTION_FLT_UNDERFLOW:
exceptionCause << "Numeric underflow";
break;
case EXCEPTION_FLT_STACK_CHECK:
exceptionCause << "Stack check";
break;
case EXCEPTION_ILLEGAL_INSTRUCTION:
exceptionCause << "Illegal instruction";
break;
case EXCEPTION_INVALID_DISPOSITION:
exceptionCause << "Invalid disposition";
break;
case EXCEPTION_NONCONTINUABLE_EXCEPTION:
exceptionCause << "Noncontinuable exception";
break;
case EXCEPTION_PRIV_INSTRUCTION:
exceptionCause << "Priviledged instruction";
break;
case EXCEPTION_STACK_OVERFLOW:
exceptionCause << "Stack overflow";
break;
default:
exceptionCause << "Unknown";
break;
}
void* exceptionAddress = exceptionInfo->ExceptionRecord->ExceptionAddress;
HMODULE crashedModuleHandle;
GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, static_cast<LPCSTR>(exceptionAddress), &crashedModuleHandle);
MODULEINFO crashedModuleInfo;
GetModuleInformation(GetCurrentProcess(), crashedModuleHandle, &crashedModuleInfo, sizeof(crashedModuleInfo));
char crashedModuleFullName[MAX_PATH];
GetModuleFileNameExA(GetCurrentProcess(), crashedModuleHandle, crashedModuleFullName, MAX_PATH);
char* crashedModuleName = strrchr(crashedModuleFullName, '\\') + 1;
DWORD64 crashedModuleOffset = ((DWORD64)exceptionAddress) - ((DWORD64)crashedModuleInfo.lpBaseOfDll);
CONTEXT* exceptionContext = exceptionInfo->ContextRecord;
spdlog::error("Northstar has crashed! a minidump has been written and exception info is available below:");
spdlog::error(exceptionCause.str());
spdlog::error("At: {} + {}", crashedModuleName, (void*)crashedModuleOffset);
PVOID framesToCapture[62];
int frames = RtlCaptureStackBackTrace(0, 62, framesToCapture, NULL);
for (int i = 0; i < frames; i++)
{
HMODULE backtraceModuleHandle;
GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, static_cast<LPCSTR>(framesToCapture[i]), &backtraceModuleHandle);
char backtraceModuleFullName[MAX_PATH];
GetModuleFileNameExA(GetCurrentProcess(), backtraceModuleHandle, backtraceModuleFullName, MAX_PATH);
char* backtraceModuleName = strrchr(backtraceModuleFullName, '\\') + 1;
void* actualAddress = (void*)framesToCapture[i];
void* relativeAddress = (void*)(uintptr_t(actualAddress) - uintptr_t(backtraceModuleHandle));
spdlog::error(" {} + {} ({})", backtraceModuleName, relativeAddress, actualAddress);
}
spdlog::error("RAX: 0x{0:x}", exceptionContext->Rax);
spdlog::error("RBX: 0x{0:x}", exceptionContext->Rbx);
spdlog::error("RCX: 0x{0:x}", exceptionContext->Rcx);
spdlog::error("RDX: 0x{0:x}", exceptionContext->Rdx);
spdlog::error("RSI: 0x{0:x}", exceptionContext->Rsi);
spdlog::error("RDI: 0x{0:x}", exceptionContext->Rdi);
spdlog::error("RBP: 0x{0:x}", exceptionContext->Rbp);
spdlog::error("RSP: 0x{0:x}", exceptionContext->Rsp);
spdlog::error("R8: 0x{0:x}", exceptionContext->R8);
spdlog::error("R9: 0x{0:x}", exceptionContext->R9);
spdlog::error("R10: 0x{0:x}", exceptionContext->R10);
spdlog::error("R11: 0x{0:x}", exceptionContext->R11);
spdlog::error("R12: 0x{0:x}", exceptionContext->R12);
spdlog::error("R13: 0x{0:x}", exceptionContext->R13);
spdlog::error("R14: 0x{0:x}", exceptionContext->R14);
spdlog::error("R15: 0x{0:x}", exceptionContext->R15);
time_t time = std::time(nullptr);
tm currentTime = *std::localtime(&time);
std::stringstream stream;
stream << std::put_time(&currentTime, (GetNorthstarPrefix() + "/logs/nsdump%Y-%m-%d %H-%M-%S.dmp").c_str());
auto hMinidumpFile = CreateFileA(stream.str().c_str(), GENERIC_WRITE, FILE_SHARE_READ, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
if (hMinidumpFile)
{
MINIDUMP_EXCEPTION_INFORMATION dumpExceptionInfo;
dumpExceptionInfo.ThreadId = GetCurrentThreadId();
dumpExceptionInfo.ExceptionPointers = exceptionInfo;
dumpExceptionInfo.ClientPointers = false;
MiniDumpWriteDump(
GetCurrentProcess(),
GetCurrentProcessId(),
hMinidumpFile,
MINIDUMP_TYPE(MiniDumpWithIndirectlyReferencedMemory | MiniDumpScanMemory),
&dumpExceptionInfo,
nullptr,
nullptr);
CloseHandle(hMinidumpFile);
}
else
spdlog::error("Failed to write minidump file {}!", stream.str());
if (!IsDedicatedServer())
MessageBoxA(
0, "Northstar has crashed! Crash info can be found in R2Northstar/logs", "Northstar has crashed!", MB_ICONERROR | MB_OK);
}
logged = true;
return EXCEPTION_EXECUTE_HANDLER;
}
BOOL WINAPI ConsoleHandlerRoutine(DWORD eventCode)
{
switch (eventCode)
{
case CTRL_CLOSE_EVENT:
// User closed console, shut everything down
spdlog::info("Exiting due to console close...");
RemoveCrashHandler();
exit(EXIT_SUCCESS);
return FALSE;
}
return TRUE;
}
void InitialiseCrashHandler()
{
hExceptionFilter = AddVectoredExceptionHandler(TRUE, ExceptionFilter);
SetConsoleCtrlHandler(ConsoleHandlerRoutine, true);
}
void RemoveCrashHandler()
{
RemoveVectoredExceptionHandler(hExceptionFilter);
}

View File

@ -0,0 +1,4 @@
#pragma once
void InitialiseCrashHandler();
void RemoveCrashHandler();

View File

@ -23,5 +23,9 @@ std::unordered_map<std::string, ConCommandBase*> CCvar::DumpToMap()
return allConVars;
}
SourceInterface<CCvar>* g_pCVarInterface;
CCvar* g_pCVar;
// use the R2 namespace for game funcs
namespace R2
{
SourceInterface<CCvar>* g_pCVarInterface;
CCvar* g_pCVar;
} // namespace R2

View File

@ -35,5 +35,9 @@ class CCvar
std::unordered_map<std::string, ConCommandBase*> DumpToMap();
};
extern SourceInterface<CCvar>* g_pCVarInterface;
extern CCvar* g_pCVar;
// use the R2 namespace for game funcs
namespace R2
{
extern SourceInterface<CCvar>* g_pCVarInterface;
extern CCvar* g_pCVar;
} // namespace R2

View File

@ -1,17 +1,9 @@
#include "pch.h"
#include "debugoverlay.h"
#include "dedicated.h"
#include "cvar.h"
#include "vector.h"
struct Vector3
{
float x, y, z;
};
struct QAngle
{
float x, y, z, w;
};
AUTOHOOK_INIT()
enum OverlayType_t
{
@ -40,7 +32,7 @@ struct OverlayBase_t
int m_nServerCount; // Latch server count, too
float m_flEndTime; // When does this box go away
OverlayBase_t* m_pNextOverlay;
__int64 m_pUnk;
void* m_pUnk;
};
struct OverlayLine_t : public OverlayBase_t
@ -76,37 +68,18 @@ struct OverlayBox_t : public OverlayBase_t
int a;
};
// this is in cvar.h, don't need it here
/*class Color
{
public:
Color(int r, int g, int b, int a)
{
_color[0] = (unsigned char)r;
_color[1] = (unsigned char)g;
_color[2] = (unsigned char)b;
_color[3] = (unsigned char)a;
}
private:
unsigned char _color[4];
};*/
static HMODULE sEngineModule;
typedef void (*DrawOverlayType)(OverlayBase_t* a1);
DrawOverlayType DrawOverlay;
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;
// engine.dll+0xABCB0
void __fastcall DrawOverlayHook(OverlayBase_t* pOverlay)
// clang-format off
AUTOHOOK(DrawOverlay, engine.dll + 0xABCB0,
void, __fastcall, (OverlayBase_t * pOverlay))
// clang-format on
{
EnterCriticalSection((LPCRITICAL_SECTION)((char*)sEngineModule + 0x10DB0A38)); // s_OverlayMutex
@ -151,24 +124,17 @@ void __fastcall DrawOverlayHook(OverlayBase_t* pOverlay)
LeaveCriticalSection((LPCRITICAL_SECTION)((char*)sEngineModule + 0x10DB0A38));
}
void InitialiseDebugOverlay(HMODULE baseAddress)
ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", DebugOverlay, ConVar, (CModule module))
{
if (IsDedicatedServer())
return;
AUTOHOOK_DISPATCH()
HookEnabler hook;
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0xABCB0, &DrawOverlayHook, reinterpret_cast<LPVOID*>(&DrawOverlay));
RenderLine = reinterpret_cast<RenderLineType>((char*)baseAddress + 0x192A70);
RenderBox = reinterpret_cast<RenderBoxType>((char*)baseAddress + 0x192520);
RenderWireframeBox = reinterpret_cast<RenderBoxType>((char*)baseAddress + 0x193DA0);
sEngineModule = baseAddress;
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 = (ConVar*)((char*)baseAddress + 0x10DB0990);
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);

View File

@ -1,3 +0,0 @@
#pragma once
void InitialiseDebugOverlay(HMODULE baseAddress);

View File

@ -1,9 +1,16 @@
#include "pch.h"
#include "dedicated.h"
#include "hookutils.h"
#include "gameutils.h"
#include "tier0.h"
#include "playlist.h"
#include "r2engine.h"
#include "hoststate.h"
#include "serverauthentication.h"
#include "masterserver.h"
#include "printcommand.h"
AUTOHOOK_INIT()
using namespace R2;
bool IsDedicatedServer()
{
@ -33,32 +40,23 @@ void Sys_Printf(CDedicatedExports* dedicated, const char* msg)
spdlog::info("[DEDICATED SERVER] {}", msg);
}
typedef void (*CHostState__InitType)(CHostState* self);
void RunServer(CDedicatedExports* dedicated)
{
spdlog::info("CDedicatedExports::RunServer(): starting");
spdlog::info(CommandLine()->GetCmdLine());
spdlog::info(Tier0::CommandLine()->GetCmdLine());
// initialise engine
g_pEngine->Frame();
// add +map if not present
// don't manually execute this from cbuf as users may have it in their startup args anyway, easier just to run from stuffcmds if present
if (!CommandLine()->CheckParm("+map"))
CommandLine()->AppendParm("+map", g_pCVar->FindVar("match_defaultMap")->GetString());
if (!Tier0::CommandLine()->CheckParm("+map"))
Tier0::CommandLine()->AppendParm("+map", g_pCVar->FindVar("match_defaultMap")->GetString());
// run server autoexec and re-run commandline
Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec autoexec_ns_server", cmd_source_t::kCommandSrcCode);
// re-run commandline
Cbuf_AddText(Cbuf_GetCurrentPlayer(), "stuffcmds", cmd_source_t::kCommandSrcCode);
Cbuf_Execute();
// ensure playlist initialises right, if we've not explicitly called setplaylist
SetCurrentPlaylist(GetCurrentPlaylistName());
// note: we no longer manually set map and hoststate to start server in g_pHostState, we just use +map which seems to initialise stuff
// better
// get tickinterval
ConVar* Cvar_base_tickinterval_mp = g_pCVar->FindVar("base_tickinterval_mp");
@ -66,43 +64,31 @@ void RunServer(CDedicatedExports* dedicated)
double frameTitle = 0;
while (g_pEngine->m_nQuitting == EngineQuitState::QUIT_NOTQUITTING)
{
double frameStart = Plat_FloatTime();
double frameStart = Tier0::Plat_FloatTime();
g_pEngine->Frame();
// only update the title after at least 500ms since the last update
if ((frameStart - frameTitle) > 0.5)
{
frameTitle = frameStart;
// this way of getting playercount/maxplayers honestly really sucks, but not got any other methods of doing it rn
const char* maxPlayers = GetCurrentPlaylistVar("max_players", false);
if (!maxPlayers)
maxPlayers = "6";
SetConsoleTitleA(fmt::format(
"{} - {} {}/{} players ({})",
g_MasterServerManager->m_sUnicodeServerName,
g_pHostState->m_levelName,
g_ServerAuthenticationManager->m_additionalPlayerData.size(),
maxPlayers,
GetCurrentPlaylistName())
.c_str());
}
std::this_thread::sleep_for(std::chrono::duration<double, std::ratio<1>>(
Cvar_base_tickinterval_mp->GetFloat() - fmin(Plat_FloatTime() - frameStart, 0.25)));
Cvar_base_tickinterval_mp->GetFloat() - fmin(Tier0::Plat_FloatTime() - frameStart, 0.25)));
}
}
typedef bool (*IsGameActiveWindowType)();
IsGameActiveWindowType IsGameActiveWindow;
bool IsGameActiveWindowHook()
// use server presence to update window title
class DedicatedConsoleServerPresence : public ServerPresenceReporter
{
return true;
}
void ReportPresence(const ServerPresence* pServerPresence) override
{
SetConsoleTitleA(fmt::format(
"{} - {} {}/{} players ({})",
pServerPresence->m_sServerName,
pServerPresence->m_MapName,
pServerPresence->m_iPlayerCount,
pServerPresence->m_iMaxPlayers,
pServerPresence->m_PlaylistName)
.c_str());
}
};
HANDLE consoleInputThreadHandle = NULL;
DWORD WINAPI ConsoleInputThread(PVOID pThreadParameter)
{
while (!g_pEngine || !g_pHostState || g_pHostState->m_iCurrentState != HostState_t::HS_RUN)
@ -121,139 +107,106 @@ DWORD WINAPI ConsoleInputThread(PVOID pThreadParameter)
{
input += "\n";
Cbuf_AddText(Cbuf_GetCurrentPlayer(), input.c_str(), cmd_source_t::kCommandSrcCode);
TryPrintCvarHelpForCommand(input.c_str()); // this needs to be done on main thread, unstable in this one
}
}
return 0;
}
#include "nsmem.h"
void InitialiseDedicated(HMODULE engineAddress)
// clang-format off
AUTOHOOK(IsGameActiveWindow, engine.dll + 0x1CDC80,
bool,, ())
// clang-format on
{
return true;
}
ON_DLL_LOAD_DEDI_RELIESON("engine.dll", DedicatedServer, ServerPresence, (CModule module))
{
spdlog::info("InitialiseDedicated");
uintptr_t ea = (uintptr_t)engineAddress;
AUTOHOOK_DISPATCH_MODULE("engine.dll")
{
// Host_Init
// prevent a particle init that relies on client dll
NSMem::NOP(ea + 0x156799, 5);
}
// Host_Init
// prevent a particle init that relies on client dll
module.Offset(0x156799).NOP(5);
// Host_Init
// don't call Key_Init to avoid loading some extra rsons from rpak (will be necessary to boot if we ever wanna disable rpaks entirely on
// dedi)
module.Offset(0x1565B0).NOP(5);
{
// CModAppSystemGroup::Create
// force the engine into dedicated mode by changing the first comparison to IsServerOnly to an assignment
auto ptr = ea + 0x1C4EBD;
MemoryAddress base = module.Offset(0x1C4EBD);
// cmp => mov
NSMem::BytePatch(ptr + 1, "C6 87");
base.Offset(1).Patch("C6 87");
// 00 => 01
NSMem::BytePatch(ptr + 7, "01");
base.Offset(7).Patch("01");
}
{
// Some init that i'm not sure of that crashes
// nop the call to it
NSMem::NOP(ea + 0x156A63, 5);
}
// Some init that i'm not sure of that crashes
// nop the call to it
module.Offset(0x156A63).NOP(5);
{
// runframeserver
// nop some access violations
NSMem::NOP(ea + 0x159819, 17);
}
// runframeserver
// nop some access violations
module.Offset(0x159819).NOP(17);
{
NSMem::NOP(ea + 0x156B4C, 7);
module.Offset(0x156B4C).NOP(7);
// previously patched these, took me a couple weeks to figure out they were the issue
// removing these will mess up register state when this function is over, so we'll write HS_RUN to the wrong address
// so uhh, don't do that
// NSMem::NOP(ea + 0x156B4C + 7, 8);
// previously patched these, took me a couple weeks to figure out they were the issue
// removing these will mess up register state when this function is over, so we'll write HS_RUN to the wrong address
// so uhh, don't do that
// NSMem::NOP(ea + 0x156B4C + 7, 8);
module.Offset(0x156B4C).Offset(15).NOP(9);
NSMem::NOP(ea + 0x156B4C + 15, 9);
}
// HostState_State_NewGame
// nop an access violation
module.Offset(0xB934C).NOP(9);
{
// HostState_State_NewGame
// nop an access violation
NSMem::NOP(ea + 0xB934C, 9);
}
// CEngineAPI::Connect
// remove call to Shader_Connect
module.Offset(0x1C4D7D).NOP(5);
{
// CEngineAPI::Connect
// remove call to Shader_Connect
NSMem::NOP(ea + 0x1C4D7D, 5);
}
// Host_Init
// remove call to ui loading stuff
module.Offset(0x156595).NOP(5);
// currently does not work, crashes stuff, likely gotta keep this here
//{
// // CEngineAPI::Connect
// // remove calls to register ui rpak asset types
// NSMem::NOP(ea + 0x1C4E07, 5);
//}
// some function that gets called from RunFrameServer
// nop a function that makes requests to stryder, this will eventually access violation if left alone and isn't necessary anyway
module.Offset(0x15A0BB).NOP(5);
{
// Host_Init
// remove call to ui loading stuff
NSMem::NOP(ea + 0x156595, 5);
}
// RunFrameServer
// nop a function that access violations
module.Offset(0x159BF3).NOP(5);
{
// some function that gets called from RunFrameServer
// nop a function that makes requests to stryder, this will eventually access violation if left alone and isn't necessary anyway
NSMem::NOP(ea + 0x15A0BB, 5);
}
// func that checks if origin is inited
// always return 1
module.Offset(0x183B70).Patch("B0 01 C3"); // mov al,01 ret
{
// RunFrameServer
// nop a function that access violations
NSMem::NOP(ea + 0x159BF3, 5);
}
// HostState_State_ChangeLevel
// nop clientinterface call
module.Offset(0x1552ED).NOP(16);
{
// func that checks if origin is inited
// always return 1
NSMem::BytePatch(
ea + 0x183B70,
{
0xB0,
0x01, // mov al,01
0xC3 // ret
});
}
// HostState_State_ChangeLevel
// nop clientinterface call
module.Offset(0x155363).NOP(16);
{
// HostState_State_ChangeLevel
// nop clientinterface call
NSMem::NOP(ea + 0x1552ED, 16);
}
{
// HostState_State_ChangeLevel
// nop clientinterface call
NSMem::NOP(ea + 0x155363, 16);
}
// note: previously had DisableDedicatedWindowCreation patches here, but removing those rn since they're all shit and unstable and bad
// and such check commit history if any are needed for reimplementation
{
// IVideoMode::CreateGameWindow
// nop call to ShowWindow
NSMem::NOP(ea + 0x1CD146, 5);
}
// IVideoMode::CreateGameWindow
// nop call to ShowWindow
module.Offset(0x1CD146).NOP(5);
CDedicatedExports* dedicatedExports = new CDedicatedExports;
dedicatedExports->vtable = dedicatedExports;
dedicatedExports->Sys_Printf = Sys_Printf;
dedicatedExports->RunServer = RunServer;
CDedicatedExports** exports = (CDedicatedExports**)((char*)engineAddress + 0x13F0B668);
*exports = dedicatedExports;
HookEnabler hook;
ENABLER_CREATEHOOK(hook, (char*)engineAddress + 0x1CDC80, &IsGameActiveWindowHook, reinterpret_cast<LPVOID*>(&IsGameActiveWindow));
*module.Offset(0x13F0B668).As<CDedicatedExports**>() = dedicatedExports;
// extra potential patches:
// nop engine.dll+1c67d1 and +1c67d8 to skip videomode creategamewindow
@ -265,15 +218,19 @@ void InitialiseDedicated(HMODULE engineAddress)
// make sure it still gets registered
// add cmdline args that are good for dedi
CommandLine()->AppendParm("-nomenuvid", 0);
CommandLine()->AppendParm("-nosound", 0);
CommandLine()->AppendParm("-windowed", 0);
CommandLine()->AppendParm("-nomessagebox", 0);
CommandLine()->AppendParm("+host_preload_shaders", "0");
CommandLine()->AppendParm("+net_usesocketsforloopback", "1");
Tier0::CommandLine()->AppendParm("-nomenuvid", 0);
Tier0::CommandLine()->AppendParm("-nosound", 0);
Tier0::CommandLine()->AppendParm("-windowed", 0);
Tier0::CommandLine()->AppendParm("-nomessagebox", 0);
Tier0::CommandLine()->AppendParm("+host_preload_shaders", "0");
Tier0::CommandLine()->AppendParm("+net_usesocketsforloopback", "1");
// use presence reporter for console title
DedicatedConsoleServerPresence* presenceReporter = new DedicatedConsoleServerPresence;
g_pServerPresence->AddPresenceReporter(presenceReporter);
// Disable Quick Edit mode to reduce chance of user unintentionally hanging their server by selecting something.
if (!CommandLine()->CheckParm("-bringbackquickedit"))
if (!Tier0::CommandLine()->CheckParm("-bringbackquickedit"))
{
HANDLE stdIn = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode = 0;
@ -295,35 +252,42 @@ void InitialiseDedicated(HMODULE engineAddress)
spdlog::info("Quick Edit enabled by user request");
// create console input thread
if (!CommandLine()->CheckParm("-noconsoleinput"))
if (!Tier0::CommandLine()->CheckParm("-noconsoleinput"))
consoleInputThreadHandle = CreateThread(0, 0, ConsoleInputThread, 0, 0, NULL);
else
spdlog::info("Console input disabled by user request");
}
void InitialiseDedicatedOrigin(HMODULE baseAddress)
ON_DLL_LOAD_DEDI("tier0.dll", DedicatedServerOrigin, (CModule module))
{
// disable origin on dedicated
// for any big ea lawyers, this can't be used to play the game without origin, game will throw a fit if you try to do anything without
// an origin id as a client for dedi it's fine though, game doesn't care if origin is disabled as long as there's only a server
NSMem::BytePatch(
(uintptr_t)GetProcAddress(GetModuleHandleA("tier0.dll"), "Tier0_InitOrigin"),
{
0xC3 // ret
});
module.GetExport("Tier0_InitOrigin").Patch("C3");
}
typedef void (*PrintFatalSquirrelErrorType)(void* sqvm);
PrintFatalSquirrelErrorType PrintFatalSquirrelError;
void PrintFatalSquirrelErrorHook(void* sqvm)
// clang-format off
AUTOHOOK(PrintSquirrelError, server.dll + 0x794D0,
void, __fastcall, (void* sqvm))
// clang-format on
{
PrintFatalSquirrelError(sqvm);
g_pEngine->m_nQuitting = EngineQuitState::QUIT_TODESKTOP;
PrintSquirrelError(sqvm);
// close dedicated server if a fatal error is hit
static ConVar* Cvar_fatal_script_errors = g_pCVar->FindVar("fatal_script_errors");
if (Cvar_fatal_script_errors->GetBool())
abort();
}
void InitialiseDedicatedServerGameDLL(HMODULE baseAddress)
ON_DLL_LOAD_DEDI("server.dll", DedicatedServerGameDLL, (CModule module))
{
HookEnabler hook;
ENABLER_CREATEHOOK(hook, baseAddress + 0x794D0, &PrintFatalSquirrelErrorHook, reinterpret_cast<LPVOID*>(&PrintFatalSquirrelError));
AUTOHOOK_DISPATCH_MODULE(server.dll)
if (Tier0::CommandLine()->CheckParm("-nopakdedi"))
{
module.Offset(0x6BA350).Patch("C3"); // dont load skins.rson from rpak if we don't have rpaks, as loading it will cause a crash
module.Offset(0x6BA300).Patch(
"B8 C8 00 00 00 C3"); // return 200 as the number of skins from server.dll + 6BA300, this is the normal value read from
// skins.rson and should be updated when we need it more modular
}
}

View File

@ -1,7 +1,3 @@
#pragma once
bool IsDedicatedServer();
void InitialiseDedicated(HMODULE moduleAddress);
void InitialiseDedicatedOrigin(HMODULE baseAddress);
void InitialiseDedicatedServerGameDLL(HMODULE baseAddress);

View File

@ -1,11 +1,12 @@
#include "pch.h"
#include "dedicated.h"
#include "dedicatedmaterialsystem.h"
#include "hookutils.h"
#include "gameutils.h"
#include "nsmem.h"
#include "tier0.h"
typedef HRESULT (*__stdcall D3D11CreateDeviceType)(
AUTOHOOK_INIT()
// clang-format off
AUTOHOOK(D3D11CreateDevice, materialsystem_dx11.dll + 0xD9A0E,
HRESULT, __stdcall, (
void* pAdapter,
int DriverType,
HMODULE Software,
@ -15,108 +16,26 @@ typedef HRESULT (*__stdcall D3D11CreateDeviceType)(
UINT SDKVersion,
void** ppDevice,
int* pFeatureLevel,
void** ppImmediateContext);
D3D11CreateDeviceType D3D11CreateDevice;
HRESULT __stdcall D3D11CreateDeviceHook(
void* pAdapter,
int DriverType,
HMODULE Software,
UINT Flags,
int* pFeatureLevels,
UINT FeatureLevels,
UINT SDKVersion,
void** ppDevice,
int* pFeatureLevel,
void** ppImmediateContext)
void** ppImmediateContext))
// clang-format on
{
// note: this is super duper temp pretty much just messing around with it
// does run surprisingly well on dedi for a software driver tho if you ignore the +1gb ram usage at times, seems like dedi doesn't
// really call gpu much even with renderthread still being a thing will be using this hook for actual d3d stubbing and stuff later
// atm, i think the play might be to run d3d in software, and then just stub out any calls that allocate memory/use alot of resources
// (e.g. createtexture and that sorta thing)
// note: this has been succeeded by the d3d11 and gfsdk stubs, and is only being kept around for posterity and as a fallback option
if (CommandLine()->CheckParm("-softwared3d11"))
if (Tier0::CommandLine()->CheckParm("-softwared3d11"))
DriverType = 5; // D3D_DRIVER_TYPE_WARP
return D3D11CreateDevice(
pAdapter, DriverType, Software, Flags, pFeatureLevels, FeatureLevels, SDKVersion, ppDevice, pFeatureLevel, ppImmediateContext);
}
void InitialiseDedicatedMaterialSystem(HMODULE baseAddress)
ON_DLL_LOAD_DEDI("materialsystem_dx11.dll", DedicatedServerMaterialSystem, (CModule module))
{
HookEnabler hook;
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0xD9A0E, &D3D11CreateDeviceHook, reinterpret_cast<LPVOID*>(&D3D11CreateDevice));
AUTOHOOK_DISPATCH()
// not using these for now since they're related to nopping renderthread/gamewindow i.e. very hard
//{
// // function that launches renderthread
// char* ptr = (char*)baseAddress + 0x87047;
// TempReadWrite rw(ptr);
//
// // make it not launch renderthread
// *ptr = (char)0x90;
// *(ptr + 1) = (char)0x90;
// *(ptr + 2) = (char)0x90;
// *(ptr + 3) = (char)0x90;
// *(ptr + 4) = (char)0x90;
// *(ptr + 5) = (char)0x90;
//}
//
//{
// // some function that waits on renderthread job
// char* ptr = (char*)baseAddress + 0x87d00;
// TempReadWrite rw(ptr);
//
// // return immediately
// *ptr = (char)0xC3;
//}
{
// CMaterialSystem::FindMaterial
// make the game always use the error material
NSMem::BytePatch((uintptr_t)baseAddress + 0x5F0F1, {0xE9, 0x34, 0x03, 0x00});
}
// previously had DisableDedicatedWindowCreation stuff here, removing for now since shit and unstable
// check commit history if needed
}
typedef void* (*PakLoadAPI__LoadRpakType)(char* filename, void* unknown, int flags);
PakLoadAPI__LoadRpakType PakLoadAPI__LoadRpak;
void* PakLoadAPI__LoadRpakHook(char* filename, void* unknown, int flags)
{
spdlog::info("PakLoadAPI__LoadRpakHook {}", filename);
// on dedi, don't load any paks that aren't required
if (strncmp(filename, "common", 6))
return 0;
return PakLoadAPI__LoadRpak(filename, unknown, flags);
}
typedef void* (*PakLoadAPI__LoadRpak2Type)(char* filename, void* unknown, int flags, void* callback, void* callback2);
PakLoadAPI__LoadRpak2Type PakLoadAPI__LoadRpak2;
void* PakLoadAPI__LoadRpak2Hook(char* filename, void* unknown, int flags, void* callback, void* callback2)
{
spdlog::info("PakLoadAPI__LoadRpak2Hook {}", filename);
// on dedi, don't load any paks that aren't required
if (strncmp(filename, "common", 6))
return 0;
return PakLoadAPI__LoadRpak2(filename, unknown, flags, callback, callback2);
}
void InitialiseDedicatedRtechGame(HMODULE baseAddress)
{
baseAddress = GetModuleHandleA("rtech_game.dll");
HookEnabler hook;
// unfortunately this is unstable, seems to freeze when changing maps
// ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0xB0F0, &PakLoadAPI__LoadRpakHook, reinterpret_cast<LPVOID*>(&PakLoadAPI__LoadRpak));
// ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0xB170, &PakLoadAPI__LoadRpak2Hook, reinterpret_cast<LPVOID*>(&PakLoadAPI__LoadRpak2));
// CMaterialSystem::FindMaterial
// make the game always use the error material
module.Offset(0x5F0F1).Patch("E9 34 03 00");
}

View File

@ -1,3 +0,0 @@
#pragma once
void InitialiseDedicatedMaterialSystem(HMODULE baseAddress);
void InitialiseDedicatedRtechGame(HMODULE baseAddress);

View File

@ -0,0 +1,26 @@
#include "pch.h"
#include "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);
}

View File

@ -1,63 +1,24 @@
#include "pch.h"
#include "hooks.h"
#include "main.h"
#include "squirrel.h"
#include "dedicated.h"
#include "dedicatedmaterialsystem.h"
#include "sourceconsole.h"
#include "logging.h"
#include "concommand.h"
#include "modmanager.h"
#include "filesystem.h"
#include "serverauthentication.h"
#include "scriptmodmenu.h"
#include "scriptserverbrowser.h"
#include "keyvalues.h"
#include "masterserver.h"
#include "gameutils.h"
#include "chatcommand.h"
#include "modlocalisation.h"
#include "playlist.h"
#include "miscserverscript.h"
#include "clientauthhooks.h"
#include "latencyflex.h"
#include "scriptbrowserhooks.h"
#include "scriptmainmenupromos.h"
#include "miscclientfixes.h"
#include "miscserverfixes.h"
#include "rpakfilesystem.h"
#include "bansystem.h"
#include "crashhandler.h"
#include "memalloc.h"
#include "maxplayers.h"
#include "languagehooks.h"
#include "audio.h"
#include "buildainfile.h"
#include "nsprefix.h"
#include "serverchathooks.h"
#include "clientchathooks.h"
#include "localchatwriter.h"
#include "scriptservertoclientstringcommand.h"
#include "plugin_abi.h"
#include "plugins.h"
#include "debugoverlay.h"
#include "clientvideooverrides.h"
#include "clientruihooks.h"
#include <string.h>
#include "version.h"
#include "pch.h"
#include "scriptutility.h"
#include "rapidjson/document.h"
#include "rapidjson/stringbuffer.h"
#include "rapidjson/writer.h"
#include "rapidjson/error/en.h"
#include "exploitfixes.h"
#include "scriptjson.h"
#include <string.h>
#include <filesystem>
typedef void (*initPluginFuncPtr)(void* (*getPluginObject)(PluginObject));
bool initialised = false;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
@ -72,18 +33,6 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserv
return TRUE;
}
void WaitForDebugger(HMODULE baseAddress)
{
// earlier waitfordebugger call than is in vanilla, just so we can debug stuff a little easier
if (CommandLine()->CheckParm("-waitfordebugger"))
{
spdlog::info("waiting for debugger...");
while (!IsDebuggerPresent())
Sleep(100);
}
}
void freeLibrary(HMODULE hLib)
{
if (!FreeLibrary(hLib))
@ -190,15 +139,13 @@ bool LoadPlugins()
bool InitialiseNorthstar()
{
if (initialised)
{
// spdlog::warn("Called InitialiseNorthstar more than once!"); // it's actually 100% fine for that to happen
static bool bInitialised = false;
if (bInitialised)
return false;
}
initialised = true;
bInitialised = true;
parseConfigurables();
InitialiseNorthstarPrefix();
InitialiseVersion();
// Fix some users' failure to connect to respawn datacenters
@ -206,6 +153,7 @@ bool InitialiseNorthstar()
curl_global_init_mem(CURL_GLOBAL_DEFAULT, _malloc_base, _free_base, _realloc_base, _strdup_base, _calloc_base);
InitialiseCrashHandler();
InitialiseLogging();
InstallInitialHooks();
CreateLogFiles();
@ -213,94 +161,6 @@ bool InitialiseNorthstar()
// Write launcher version to log
spdlog::info("NorthstarLauncher version: {}", version);
InitialiseInterfaceCreationHooks();
AddDllLoadCallback("tier0.dll", InitialiseTier0GameUtilFunctions);
AddDllLoadCallback("engine.dll", WaitForDebugger);
AddDllLoadCallback("engine.dll", InitialiseEngineGameUtilFunctions);
AddDllLoadCallback("server.dll", InitialiseServerGameUtilFunctions);
// dedi patches
{
AddDllLoadCallbackForDedicatedServer("tier0.dll", InitialiseDedicatedOrigin);
AddDllLoadCallbackForDedicatedServer("engine.dll", InitialiseDedicated);
AddDllLoadCallbackForDedicatedServer("server.dll", InitialiseDedicatedServerGameDLL);
AddDllLoadCallbackForDedicatedServer("materialsystem_dx11.dll", InitialiseDedicatedMaterialSystem);
// this fucking sucks, but seemingly we somehow load after rtech_game???? unsure how, but because of this we have to apply patches
// here, not on rtech_game load
AddDllLoadCallbackForDedicatedServer("engine.dll", InitialiseDedicatedRtechGame);
}
AddDllLoadCallback("engine.dll", InitialiseConVars);
AddDllLoadCallback("engine.dll", InitialiseConCommands);
// client-exclusive patches
{
AddDllLoadCallbackForClient("tier0.dll", InitialiseTier0LanguageHooks);
AddDllLoadCallbackForClient("client.dll", InitialiseClientSquirrel);
AddDllLoadCallbackForClient("client.dll", InitialiseSourceConsole);
AddDllLoadCallbackForClient("engine.dll", InitialiseChatCommands);
AddDllLoadCallbackForClient("client.dll", InitialiseScriptModMenu);
AddDllLoadCallbackForClient("client.dll", InitialiseScriptServerBrowser);
AddDllLoadCallbackForClient("localize.dll", InitialiseModLocalisation);
AddDllLoadCallbackForClient("engine.dll", InitialiseClientAuthHooks);
AddDllLoadCallbackForClient("client.dll", InitialiseLatencyFleX);
AddDllLoadCallbackForClient("engine.dll", InitialiseScriptExternalBrowserHooks);
AddDllLoadCallbackForClient("client.dll", InitialiseScriptMainMenuPromos);
AddDllLoadCallbackForClient("client.dll", InitialiseMiscClientFixes);
AddDllLoadCallbackForClient("client.dll", InitialiseClientPrintHooks);
AddDllLoadCallbackForClient("client.dll", InitialisePluginCommands);
AddDllLoadCallbackForClient("client.dll", InitialiseClientChatHooks);
AddDllLoadCallbackForClient("client.dll", InitialiseLocalChatWriter);
AddDllLoadCallbackForClient("client.dll", InitialiseScriptServerToClientStringCommands);
AddDllLoadCallbackForClient("engine.dll", InitialiseEngineClientVideoOverrides);
AddDllLoadCallbackForClient("engine.dll", InitialiseEngineClientRUIHooks);
AddDllLoadCallbackForClient("engine.dll", InitialiseDebugOverlay);
AddDllLoadCallbackForClient("client.dll", InitialiseClientSquirrelJson);
AddDllLoadCallbackForClient("client.dll", InitialiseClientSquirrelUtilityFunctions);
// audio hooks
AddDllLoadCallbackForClient("client.dll", InitialiseMilesAudioHooks);
}
AddDllLoadCallback("engine.dll", InitialiseEngineSpewFuncHooks);
AddDllLoadCallback("server.dll", InitialiseServerSquirrel);
AddDllLoadCallback("engine.dll", InitialiseBanSystem);
AddDllLoadCallback("engine.dll", InitialiseServerAuthentication);
AddDllLoadCallback("engine.dll", InitialiseSharedMasterServer);
AddDllLoadCallback("server.dll", InitialiseMiscServerScriptCommand);
AddDllLoadCallback("server.dll", InitialiseMiscServerFixes);
AddDllLoadCallback("server.dll", InitialiseBuildAINFileHooks);
AddDllLoadCallback("server.dll", InitialiseServerSquirrelUtilityFunctions);
AddDllLoadCallback("server.dll", InitialiseServerSquirrelJson);
AddDllLoadCallback("engine.dll", InitialisePlaylistHooks);
AddDllLoadCallback("filesystem_stdio.dll", InitialiseFilesystem);
AddDllLoadCallback("engine.dll", InitialiseEngineRpakFilesystem);
AddDllLoadCallback("engine.dll", InitialiseKeyValues);
AddDllLoadCallback("engine.dll", InitialiseServerChatHooks_Engine);
AddDllLoadCallback("server.dll", InitialiseServerChatHooks_Server);
// maxplayers increase
AddDllLoadCallback("engine.dll", InitialiseMaxPlayersOverride_Engine);
AddDllLoadCallback("client.dll", InitialiseMaxPlayersOverride_Client);
AddDllLoadCallback("server.dll", InitialiseMaxPlayersOverride_Server);
// mod manager after everything else
AddDllLoadCallback("engine.dll", InitialiseModManager);
{
// activate multi-module exploitfixes callbacks
constexpr const char* EXPLOITFIXES_MULTICALLBACK_MODS[] = {"client.dll", "engine.dll", "server.dll"};
for (const char* mod : EXPLOITFIXES_MULTICALLBACK_MODS)
AddDllLoadCallback(mod, ExploitFixes::LoadCallback_MultiModule);
// activate exploit fixes later
AddDllLoadCallback("server.dll", ExploitFixes::LoadCallback_Full);
}
// run callbacks for any libraries that are already loaded by now
CallAllPendingDLLLoadCallbacks();

View File

@ -1,52 +1,63 @@
#include "pch.h"
#include "exploitfixes.h"
#include "exploitfixes_utf8parser.h"
#include "nsmem.h"
#include "cvar.h"
#include "gameutils.h"
#include "limits.h"
#include "dedicated.h"
#include "tier0.h"
#include "r2engine.h"
#include "r2client.h"
#include "vector.h"
AUTOHOOK_INIT()
ConVar* Cvar_ns_exploitfixes_log;
ConVar* Cvar_ns_should_log_all_clientcommands;
ConVar* Cvar_sv_cheats;
ConVar* ns_exploitfixes_log;
#define SHOULD_LOG (ns_exploitfixes_log->m_Value.m_nValue > 0)
#define BLOCKED_INFO(s) \
( \
[=]() -> bool \
{ \
if (SHOULD_LOG) \
if (Cvar_ns_exploitfixes_log->GetBool()) \
{ \
std::stringstream stream; \
stream << "exploitfixes.cpp: " << BLOCK_PREFIX << s; \
stream << "ExploitFixes.cpp: " << BLOCK_PREFIX << s; \
spdlog::error(stream.str()); \
} \
return false; \
}())
struct Float3
{
float vals[3];
void MakeValid()
{
for (auto& val : vals)
if (isnan(val))
val = 0;
}
};
#define BLOCK_NETMSG_FUNC(name, pattern) \
KHOOK(name, ("engine.dll", pattern), bool, __fastcall, (void* thisptr, void* buffer)) \
{ \
return false; \
}
// block bad netmessages
// Servers can literally request a screenshot from any client, yeah no
BLOCK_NETMSG_FUNC(CLC_Screenshot_WriteToBuffer, "48 89 5C 24 ? 57 48 83 EC 20 8B 42 10");
BLOCK_NETMSG_FUNC(CLC_Screenshot_ReadFromBuffer, "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 48 83 EC 20 48 8B DA 48 8B 52 38");
// clang-format off
AUTOHOOK(CLC_Screenshot_WriteToBuffer, engine.dll + 0x22AF20,
bool, __fastcall, (void* thisptr, void* buffer)) // 48 89 5C 24 ? 57 48 83 EC 20 8B 42 10
// clang-format on
{
return false;
}
// This is unused ingame and a big exploit vector
BLOCK_NETMSG_FUNC(Base_CmdKeyValues_ReadFromBuffer, "40 55 48 81 EC ? ? ? ? 48 8D 6C 24 ? 48 89 5D 70");
// clang-format off
AUTOHOOK(CLC_Screenshot_ReadFromBuffer, engine.dll + 0x221F00,
bool, __fastcall, (void* thisptr, void* buffer)) // 48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 48 83 EC 20 48 8B DA 48 8B 52 38
// clang-format on
{
return false;
}
KHOOK(CClient_ProcessSetConVar, ("engine.dll", "48 8B D1 48 8B 49 18 48 8B 01 48 FF 60 10"), bool, __fastcall, (void* pMsg))
// This is unused ingame and a big client=>server=>client exploit vector
// clang-format off
AUTOHOOK(Base_CmdKeyValues_ReadFromBuffer, engine.dll + 0x220040,
bool, __fastcall, (void* thisptr, void* buffer)) // 40 55 48 81 EC ? ? ? ? 48 8D 6C 24 ? 48 89 5D 70
// clang-format on
{
return false;
}
// clang-format off
AUTOHOOK(CClient_ProcessSetConVar, engine.dll + 0x75CF0,
bool, __fastcall, (void* pMsg)) // 48 8B D1 48 8B 49 18 48 8B 01 48 FF 60 10
// clang-format on
{
constexpr int ENTRY_STR_LEN = 260;
@ -69,25 +80,12 @@ KHOOK(CClient_ProcessSetConVar, ("engine.dll", "48 8B D1 48 8B 49 18 48 8B 01 48
};
auto msg = (NET_SetConVar*)pMsg;
bool bIsServerFrame = Tier0::ThreadInServerFrameThread();
bool areWeServer;
std::string BLOCK_PREFIX =
std::string {"NET_SetConVar ("} + (bIsServerFrame ? "server" : "client") + "): Blocked dangerous/invalid msg: ";
{
// Figure out of we are the client or the server
// To do this, we utilize the msg's m_pMessageHandler pointer
// m_pMessageHandler points to a virtual class that handles all net messages
// The first virtual table function of our m_pMessageHandler will differ if it is IServerMessageHandler or IClientMessageHandler
void* msgHandlerVTableFirstFunc = **(void****)(msg->m_pMessageHandler);
static auto engineBaseAddress = (uintptr_t)GetModuleHandleA("engine.dll");
auto offset = uintptr_t(msgHandlerVTableFirstFunc) - engineBaseAddress;
constexpr uintptr_t CLIENTSTATE_FIRST_VFUNC_OFFSET = 0x8A15C;
areWeServer = offset != CLIENTSTATE_FIRST_VFUNC_OFFSET;
}
std::string BLOCK_PREFIX = std::string {"NET_SetConVar ("} + (areWeServer ? "server" : "client") + "): Blocked dangerous/invalid msg: ";
if (areWeServer)
if (bIsServerFrame)
{
constexpr int SETCONVAR_SANITY_AMOUNT_LIMIT = 69;
if (msg->m_ConVars_count < 1 || msg->m_ConVars_count > SETCONVAR_SANITY_AMOUNT_LIMIT)
@ -101,9 +99,8 @@ KHOOK(CClient_ProcessSetConVar, ("engine.dll", "48 8B D1 48 8B 49 18 48 8B 01 48
auto entry = msg->m_ConVars + i;
// Safety check for memory access
if (NSMem::IsMemoryReadable(entry, sizeof(*entry)))
if (MemoryAddress(entry).IsMemoryReadable(sizeof(*entry)))
{
// Find null terminators
bool nameValid = false, valValid = false;
for (int i = 0; i < ENTRY_STR_LEN; i++)
@ -117,36 +114,19 @@ KHOOK(CClient_ProcessSetConVar, ("engine.dll", "48 8B D1 48 8B 49 18 48 8B 01 48
if (!nameValid || !valValid)
return BLOCKED_INFO("Missing null terminators");
auto realVar = g_pCVar->FindVar(entry->name);
ConVar* pVar = R2::g_pCVar->FindVar(entry->name);
if (realVar)
if (pVar)
{
memcpy(
entry->name,
realVar->m_ConCommandBase.m_pszName,
strlen(realVar->m_ConCommandBase.m_pszName) + 1); // Force name to match case
pVar->m_ConCommandBase.m_pszName,
strlen(pVar->m_ConCommandBase.m_pszName) + 1); // Force name to match case
bool isValidFlags = true;
if (areWeServer)
{
if (realVar)
isValidFlags = ConVar::IsFlagSet(realVar, FCVAR_USERINFO); // ConVar MUST be userinfo var
}
else
{
// TODO: Should probably have some sanity checks, but can't find any that are consistent
}
if (!isValidFlags)
{
if (!realVar)
{
return BLOCKED_INFO("Invalid flags on nonexistant cvar (how tho???)");
}
else
{
int iFlags = bIsServerFrame ? FCVAR_USERINFO : FCVAR_REPLICATED;
if (!pVar->IsFlagSet(iFlags))
return BLOCKED_INFO(
"Invalid flags (" << std::hex << "0x" << realVar->m_ConCommandBase.m_nFlags << "), var is " << entry->name);
}
"Invalid flags (" << std::hex << "0x" << pVar->m_ConCommandBase.m_nFlags << "), var is " << entry->name);
}
}
else
@ -155,11 +135,14 @@ KHOOK(CClient_ProcessSetConVar, ("engine.dll", "48 8B D1 48 8B 49 18 48 8B 01 48
}
}
return oCClient_ProcessSetConVar(msg);
return CClient_ProcessSetConVar(msg);
}
// Purpose: prevent invalid user CMDs
KHOOK(CClient_ProcessUsercmds, ("engine.dll", "40 55 56 48 83 EC 58"), bool, __fastcall, (void* thisptr, void* pMsg))
// prevent invalid user CMDs
// clang-format off
AUTOHOOK(CClient_ProcessUsercmds, engine.dll + 0x1040F0,
bool, __fastcall, (void* thisptr, void* pMsg)) // 40 55 56 48 83 EC 58
// clang-format on
{
struct CLC_Move
{
@ -186,22 +169,19 @@ KHOOK(CClient_ProcessUsercmds, ("engine.dll", "40 55 56 48 83 EC 58"), bool, __f
return BLOCKED_INFO("Invalid m_nNewCommands (" << msg->m_nNewCommands << ")");
}
constexpr int NUMCMD_SANITY_LIMIT = 16;
if ((msg->m_nNewCommands + msg->m_nBackupCommands) > NUMCMD_SANITY_LIMIT)
{
return BLOCKED_INFO("Command count is too high (new: " << msg->m_nNewCommands << ", backup: " << msg->m_nBackupCommands << ")");
}
if (msg->m_nLength <= 0)
return BLOCKED_INFO("Invalid message length (" << msg->m_nLength << ")");
return oCClient_ProcessUsercmds(thisptr, pMsg);
return CClient_ProcessUsercmds(thisptr, pMsg);
}
KHOOK(ReadUsercmd, ("server.dll", "4C 89 44 24 ? 53 55 56 57"), void, __fastcall, (void* buf, void* pCmd_move, void* pCmd_from))
// clang-format off
AUTOHOOK(ReadUsercmd, server.dll + 0x2603F0,
void, __fastcall, (void* buf, void* pCmd_move, void* pCmd_from)) // 4C 89 44 24 ? 53 55 56 57
// clang-format on
{
// Let normal usercmd read happen first, it's safe
oReadUsercmd(buf, pCmd_move, pCmd_from);
ReadUsercmd(buf, pCmd_move, pCmd_from);
// Now let's make sure the CMD we read isnt messed up to prevent numerous exploits (including server crashing)
struct alignas(4) SV_CUserCmd
@ -209,11 +189,11 @@ KHOOK(ReadUsercmd, ("server.dll", "4C 89 44 24 ? 53 55 56 57"), void, __fastcall
DWORD command_number;
DWORD tick_count;
float command_time;
Float3 worldViewAngles;
Vector3 worldViewAngles;
BYTE gap18[4];
Float3 localViewAngles;
Float3 attackangles;
Float3 move;
Vector3 localViewAngles;
Vector3 attackangles;
Vector3 move;
DWORD buttons;
BYTE impulse;
short weaponselect;
@ -221,8 +201,8 @@ KHOOK(ReadUsercmd, ("server.dll", "4C 89 44 24 ? 53 55 56 57"), void, __fastcall
BYTE gap4C[24];
char headoffset;
BYTE gap65[11];
Float3 cameraPos;
Float3 cameraAngles;
Vector3 cameraPos;
Vector3 cameraAngles;
BYTE gap88[4];
int tickSomething;
DWORD dword90;
@ -237,7 +217,7 @@ KHOOK(ReadUsercmd, ("server.dll", "4C 89 44 24 ? 53 55 56 57"), void, __fastcall
std::string BLOCK_PREFIX =
"ReadUsercmd (command_number delta: " + std::to_string(cmd->command_number - fromCmd->command_number) + "): ";
// Fix invalid player angles
// fix invalid player angles
cmd->worldViewAngles.MakeValid();
cmd->attackangles.MakeValid();
cmd->localViewAngles.MakeValid();
@ -249,7 +229,7 @@ KHOOK(ReadUsercmd, ("server.dll", "4C 89 44 24 ? 53 55 56 57"), void, __fastcall
// Fix invaid movement vector
cmd->move.MakeValid();
if (cmd->tick_count == 0 || cmd->command_time <= 0)
if (cmd->frameTime <= 0 || cmd->tick_count == 0 || cmd->command_time <= 0)
{
BLOCKED_INFO(
"Bogus cmd timing (tick_count: " << cmd->tick_count << ", frameTime: " << cmd->frameTime
@ -260,6 +240,7 @@ KHOOK(ReadUsercmd, ("server.dll", "4C 89 44 24 ? 53 55 56 57"), void, __fastcall
return;
INVALID_CMD:
// Fix any gameplay-affecting cmd properties
// NOTE: Currently tickcount/frametime is set to 0, this ~shouldn't~ cause any problems
cmd->worldViewAngles = cmd->localViewAngles = cmd->attackangles = cmd->cameraAngles = {0, 0, 0};
@ -269,287 +250,209 @@ INVALID_CMD:
cmd->meleetarget = 0;
}
// basically: by default r2 isn't set as a valve mod, meaning that m_bRestrictServerCommands is false
// this is HORRIBLE for security, because it means servers can run arbitrary concommands on clients
// especially since we have script commands this could theoretically be awful
KHOOK(IsValveMod, ("engine.dll", "48 83 EC 28 48 8B 0D ? ? ? ? 48 8D 15 ? ? ? ? E8 ? ? ? ? 85 C0 74 63"), bool, __fastcall, ())
// ensure that GetLocalBaseClient().m_bRestrictServerCommands is set correctly, which the return value of this function controls
// this is IsValveMod in source, but we're making it IsRespawnMod now since valve didn't make this one
// clang-format off
AUTOHOOK(IsRespawnMod, engine.dll + 0x1C6360,
bool, __fastcall, (const char* pModName)) // 48 83 EC 28 48 8B 0D ? ? ? ? 48 8D 15 ? ? ? ? E8 ? ? ? ? 85 C0 74 63
// clang-format on
{
bool result = !CommandLine()->CheckParm("-norestrictservercommands");
spdlog::info("ExploitFixes: Overriding IsValveMod to {}...", result);
return result;
// somewhat temp, store the modname here, since we don't have a proper ptr in engine to it rn
int iSize = strlen(pModName);
R2::g_pModName = new char[iSize + 1];
strcpy(R2::g_pModName, pModName);
return (!strcmp("r2", pModName) || !strcmp("r1", pModName)) && !Tier0::CommandLine()->CheckParm("-norestrictservercommands");
}
// Fix respawn's crappy UTF8 parser so it doesn't crash -_-
// This also means you can launch multiplayer with "communities_enabled 1" and not crash, you're welcome
KHOOK(
CrashFunc_ParseUTF8,
("engine.dll", "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 41 54 41 55 41 56 41 57 48 83 EC 20 8B 1A"),
bool,
__fastcall,
(INT64 * a1, DWORD* a2, char* strData))
// ratelimit stringcmds, and prevent remote clients from calling commands that they shouldn't
bool (*CCommand__Tokenize)(CCommand& self, const char* pCommandString, R2::cmd_source_t commandSource);
// clang-format off
AUTOHOOK(CGameClient__ExecuteStringCommand, engine.dll + 0x1022E0,
bool, __fastcall, (R2::CBaseClient* self, uint32_t unknown, const char* pCommandString))
// clang-format on
{
if (Cvar_ns_should_log_all_clientcommands->GetBool())
spdlog::info("player {} (UID: {}) sent command: \"{}\"", self->m_Name, self->m_UID, pCommandString);
static void* targetRetAddr = NSMem::PatternScan("engine.dll", "84 C0 75 2C 49 8B 16");
#ifdef _MSC_VER
if (_ReturnAddress() == targetRetAddr)
#else
if (__builtin_return_address(0) == targetRetAddr)
#endif
if (!g_pServerLimits->CheckStringCommandLimits(self))
{
if (!ExploitFixes_UTF8Parser::CheckValid(a1, a2, strData))
{
const char* BLOCK_PREFIX = "ParseUTF8 Hook: ";
BLOCKED_INFO("Ignoring potentially-crashing utf8 string");
R2::CBaseClient__Disconnect(self, 1, "Sent too many stringcmd commands");
return false;
}
// verify the command we're trying to execute is FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS, if it's a concommand
char* commandBuf[1040]; // assumedly this is the size of CCommand since we don't have an actual constructor
memset(commandBuf, 0, sizeof(commandBuf));
CCommand tempCommand = *(CCommand*)&commandBuf;
if (!CCommand__Tokenize(tempCommand, pCommandString, R2::cmd_source_t::kCommandSrcCode) || !tempCommand.ArgC())
return false;
ConCommand* command = R2::g_pCVar->FindCommand(tempCommand.Arg(0));
// if the command doesn't exist pass it on to ExecuteStringCommand for script clientcommands and stuff
if (command && !command->IsFlagSet(FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS))
{
// ensure FCVAR_GAMEDLL concommands without FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS can't be executed by remote clients
if (IsDedicatedServer())
return false;
if (strcmp(self->m_UID, R2::g_pLocalPlayerUserID))
return false;
}
}
return oCrashFunc_ParseUTF8(a1, a2, strData);
}
// GetEntByIndex (called by ScriptGetEntByIndex) doesn't check for the index being out of bounds when it's
// above the max entity count. This allows it to be used to crash servers.
typedef void*(__fastcall* GetEntByIndexType)(int idx);
GetEntByIndexType GetEntByIndex;
static void* GetEntByIndexHook(int idx)
{
if (idx >= 0x4000)
// check for and block abusable legacy portal 2 commands
// these aren't actually concommands weirdly enough, they seem to just be hardcoded
if (!Cvar_sv_cheats->GetBool())
{
spdlog::info("GetEntByIndex {} is out of bounds", idx);
return nullptr;
}
return GetEntByIndex(idx);
}
constexpr const char* blockedCommands[] = {
"emit", // Sound-playing exploit (likely for Portal 2 coop devs testing splitscreen sound or something)
// RELOCATED FROM https://github.com/R2Northstar/NorthstarLauncher/commit/25dbf729cfc75107a0fcf0186924b58ecc05214b
// Rewrite of CLZSS::SafeUncompress to fix a vulnerability where malicious compressed payloads could cause the decompressor to try to read
// out of the bounds of the output buffer.
KHOOK(
LZSS_SafeUncompress,
("engine.dll", "48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 48 89 7C 24 ? 33 ED 41 8B F9"),
uint32_t,
__fastcall,
(void* self, const unsigned char* pInput, unsigned char* pOutput, unsigned int unBufSize))
{
static constexpr int LZSS_LOOKSHIFT = 4;
// These both execute a command for every single entity for some reason, nice one valve
"pre_go_to_hub",
"pre_go_to_calibration",
uint32_t totalBytes = 0;
int getCmdByte = 0, cmdByte = 0;
struct lzss_header_t
{
uint32_t id, actualSize;
};
lzss_header_t header = *(lzss_header_t*)pInput;
if (pInput == NULL || header.id != 'SSZL' || header.actualSize == 0 || header.actualSize > unBufSize)
return 0;
pInput += sizeof(lzss_header_t);
for (;;)
{
if (!getCmdByte)
cmdByte = *pInput++;
getCmdByte = (getCmdByte + 1) & 0x07;
if (cmdByte & 0x01)
{
int position = *pInput++ << LZSS_LOOKSHIFT;
position |= (*pInput >> LZSS_LOOKSHIFT);
position += 1;
int count = (*pInput++ & 0x0F) + 1;
if (count == 1)
break;
// Ensure reference chunk exists entirely within our buffer
if (position > totalBytes)
return 0;
totalBytes += count;
if (totalBytes > unBufSize)
return 0;
unsigned char* pSource = pOutput - position;
for (int i = 0; i < count; i++)
*pOutput++ = *pSource++;
}
else
{
totalBytes++;
if (totalBytes > unBufSize)
return 0;
*pOutput++ = *pInput++;
}
cmdByte = cmdByte >> 1;
}
if (totalBytes == header.actualSize)
{
return totalBytes;
}
else
{
return 0;
}
}
//////////////////////////////////////////////////
void DoBytePatches()
{
uintptr_t engineBase = (uintptr_t)GetModuleHandleA("engine.dll");
uintptr_t serverBase = (uintptr_t)GetModuleHandleA("server.dll");
// patches to make commands run from client/ui script still work
// note: this is likely preventable in a nicer way? test prolly
NSMem::BytePatch(engineBase + 0x4FB65, "EB 11");
NSMem::BytePatch(engineBase + 0x4FBAC, "EB 16");
// disconnect concommand
{
uintptr_t addr = engineBase + 0x5ADA2D;
int val = *(int*)addr | FCVAR_SERVER_CAN_EXECUTE;
NSMem::BytePatch(addr, (BYTE*)&val, sizeof(int));
}
{ // Dumb ANTITAMPER patches (they negatively impact performance and security)
constexpr const char* ANTITAMPER_EXPORTS[] = {
"ANTITAMPER_SPOTCHECK_CODEMARKER",
"ANTITAMPER_TESTVALUE_CODEMARKER",
"ANTITAMPER_TRIGGER_CODEMARKER",
"end_movie", // Calls "__MovieFinished" script function, not sure exactly what this does but it certainly isn't needed
"load_recent_checkpoint" // This is the instant-respawn exploit, literally just calls RespawnPlayer()
};
// Prevent thesefrom actually doing anything
for (auto exportName : ANTITAMPER_EXPORTS)
int iCmdLength = strlen(tempCommand.Arg(0));
bool bIsBadCommand = false;
for (auto& blockedCommand : blockedCommands)
{
if (iCmdLength != strlen(blockedCommand))
continue;
auto address = (uintptr_t)GetProcAddress(GetModuleHandleA("server.dll"), exportName);
if (!address)
{
spdlog::warn("Failed to find AntiTamper function export \"{}\"", exportName);
}
else
{
// Just return, none of them have any args or are userpurge
NSMem::BytePatch(address, "C3");
for (int i = 0; tempCommand.Arg(0)[i]; i++)
if (tolower(tempCommand.Arg(0)[i]) != blockedCommand[i])
goto NEXT_COMMAND; // break out of this loop, then go to next command
spdlog::info("Patched AntiTamper function export \"{}\"", exportName);
}
// this is a command we need to block
return false;
NEXT_COMMAND:;
}
}
return CGameClient__ExecuteStringCommand(self, unknown, pCommandString);
}
KHOOK(
SpecialClientCommand,
("server.dll", "48 89 5C 24 ? 48 89 74 24 ? 55 57 41 56 48 8D 6C 24 ? 48 81 EC ? ? ? ? 83 3A 00"),
bool,
__fastcall,
(void* player, CCommand* command))
// prevent clients from crashing servers through overflowing CNetworkStringTableContainer::WriteBaselines
bool bWasWritingStringTableSuccessful;
// clang-format off
AUTOHOOK(CBaseClient__SendServerInfo, engine.dll + 0x104FB0,
void, __fastcall, (void* self))
// clang-format on
{
bWasWritingStringTableSuccessful = true;
CBaseClient__SendServerInfo(self);
if (!bWasWritingStringTableSuccessful)
R2::CBaseClient__Disconnect(
self, 1, "Overflowed CNetworkStringTableContainer::WriteBaselines, try restarting your client and reconnecting");
}
static ConVar* sv_cheats = g_pCVar->FindVar("sv_cheats");
// return null when GetEntByIndex is passed an index >= 0x4000
// this is called from exactly 1 script clientcommand that can be given an arbitrary index, and going above 0x4000 crashes
// clang-format off
AUTOHOOK(GetEntByIndex, server.dll + 0x2A8A50,
void*, __fastcall, (int i))
// clang-format on
{
const int MAX_ENT_IDX = 0x4000;
if (sv_cheats->GetBool())
return oSpecialClientCommand(player, command); // Don't block anything if sv_cheats is on
if (i >= MAX_ENT_IDX)
{
spdlog::warn("GetEntByIndex {} is out of bounds (max {})", i, MAX_ENT_IDX);
return nullptr;
}
// These are mostly from Portal 2 (sigh)
constexpr const char* blockedCommands[] = {
"emit", // Sound-playing exploit (likely for Portal 2 coop devs testing splitscreen sound or something)
// These both execute a command for every single entity for some reason, nice one valve
"pre_go_to_hub",
"pre_go_to_calibration",
"end_movie", // Calls "__MovieFinished" script function, not sure exactly what this does but it certainly isn't needed
"load_recent_checkpoint" // This is the instant-respawn exploit, literally just calls RespawnPlayer()
return GetEntByIndex(i);
}
// clang-format off
AUTOHOOK(CL_CopyExistingEntity, engine.dll + 0x6F940,
bool, __fastcall, (void* a1))
// clang-format on
{
struct CEntityReadInfo
{
BYTE gap[40];
int nNewEntity;
};
if (command->ArgC() > 0)
CEntityReadInfo* pReadInfo = (CEntityReadInfo*)a1;
if (pReadInfo->nNewEntity >= 0x1000 || pReadInfo->nNewEntity < 0)
{
std::string cmdStr = command->Arg(0);
for (char& c : cmdStr)
c = tolower(c);
// Value isn't sanitized in release builds for
// every game powered by the Source Engine 1
// causing read/write outside of array bounds.
// This defect has let to the achievement of a
// full-chain RCE exploit. We hook and perform
// sanity checks for the value of m_nNewEntity
// here to prevent this behavior from happening.
return false;
}
for (const char* blockedCommand : blockedCommands)
return CL_CopyExistingEntity(a1);
}
ON_DLL_LOAD("engine.dll", EngineExploitFixes, (CModule module))
{
AUTOHOOK_DISPATCH_MODULE(engine.dll)
CCommand__Tokenize = module.Offset(0x418380).As<bool (*)(CCommand&, const char*, R2::cmd_source_t)>();
// allow client/ui to run clientcommands despite restricting servercommands
module.Offset(0x4FB65).Patch("EB 11");
module.Offset(0x4FBAC).Patch("EB 16");
// patch to set bWasWritingStringTableSuccessful in CNetworkStringTableContainer::WriteBaselines if it fails
{
MemoryAddress writeAddress(&bWasWritingStringTableSuccessful - module.Offset(0x234EDC).m_nAddress);
MemoryAddress addr = module.Offset(0x234ED2);
addr.Patch("C7 05");
addr.Offset(2).Patch((BYTE*)&writeAddress, sizeof(writeAddress));
addr.Offset(6).Patch("00 00 00 00");
addr.Offset(10).NOP(5);
}
}
ON_DLL_LOAD_RELIESON("server.dll", ServerExploitFixes, ConVar, (CModule module))
{
AUTOHOOK_DISPATCH_MODULE(server.dll)
// ret at the start of CServerGameClients::ClientCommandKeyValues as it has no benefit and is forwarded to client (i.e. security issue)
// this prevents the attack vector of client=>server=>client, however server=>client also has clientside patches
module.Offset(0x153920).Patch("C3");
// Dumb ANTITAMPER patches (they negatively impact performance and security)
constexpr const char* ANTITAMPER_EXPORTS[] = {
"ANTITAMPER_SPOTCHECK_CODEMARKER",
"ANTITAMPER_TESTVALUE_CODEMARKER",
"ANTITAMPER_TRIGGER_CODEMARKER",
};
// Prevent these from actually doing anything
for (auto exportName : ANTITAMPER_EXPORTS)
{
MemoryAddress exportAddr = module.GetExport(exportName);
if (exportAddr)
{
if (cmdStr.find(blockedCommand) != std::string::npos)
{
// Block this command
spdlog::warn("Blocked exploititive client command \"{}\".", cmdStr);
return true;
}
// Just return, none of them have any args or are userpurge
exportAddr.Patch("C3");
spdlog::info("Patched AntiTamper function export \"{}\"", exportName);
}
}
return oSpecialClientCommand(player, command);
}
void SetupKHook(KHook* hook)
{
if (hook->Setup())
{
spdlog::debug("KHook::Setup(): Hooked at {}", hook->targetFuncAddr);
}
else
{
spdlog::critical("\tFAILED to initialize all exploit patches.");
// Force exit
MessageBoxA(0, "FAILED to initialize all exploit patches.", "Northstar", MB_ICONERROR);
exit(0);
}
}
void ExploitFixes::LoadCallback_MultiModule(HMODULE baseAddress)
{
spdlog::info("ExploitFixes::LoadCallback_MultiModule({}) ...", (void*)baseAddress);
int hooksEnabled = 0;
for (auto itr = KHook::_allHooks.begin(); itr != KHook::_allHooks.end(); itr++)
{
auto curHook = *itr;
if (GetModuleHandleA(curHook->targetFunc.moduleName) == baseAddress)
{
SetupKHook(curHook);
itr = KHook::_allHooks.erase(itr); // Prevent repeated initialization
hooksEnabled++;
if (itr == KHook::_allHooks.end())
break;
}
}
spdlog::info("\tEnabled {} hooks.", hooksEnabled);
}
void ExploitFixes::LoadCallback_Full(HMODULE baseAddress)
{
spdlog::info("ExploitFixes::LoadCallback_Full ...");
spdlog::info("\tByte patching...");
DoBytePatches();
for (KHook* hook : KHook::_allHooks)
SetupKHook(hook);
spdlog::info("\tInitialized " + std::to_string(KHook::_allHooks.size()) + " late exploit-patch hooks.");
KHook::_allHooks.clear();
ns_exploitfixes_log =
new ConVar("ns_exploitfixes_log", "1", FCVAR_GAMEDLL, "Whether to log whenever exploitfixes.cpp blocks/corrects something");
g_pCVar->FindCommand("migrateme")->m_nFlags &= ~FCVAR_SERVER_CAN_EXECUTE;
HookEnabler hook;
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x2a8a50, &GetEntByIndexHook, reinterpret_cast<LPVOID*>(&GetEntByIndex));
Cvar_ns_exploitfixes_log =
new ConVar("ns_exploitfixes_log", "1", FCVAR_GAMEDLL, "Whether to log whenever ExploitFixes.cpp blocks/corrects something");
Cvar_ns_should_log_all_clientcommands =
new ConVar("ns_should_log_all_clientcommands", "0", FCVAR_NONE, "Whether to log all clientcommands");
Cvar_sv_cheats = R2::g_pCVar->FindVar("sv_cheats");
}

View File

@ -1,12 +0,0 @@
// KittenPopo's exploit fix hooks, feel free to add more here
#pragma once
#include "pch.h"
namespace ExploitFixes
{
// This callback will be ran muliple times on multiple module loads
void LoadCallback_MultiModule(HMODULE baseAddress);
void LoadCallback_Full(HMODULE unused);
} // namespace ExploitFixes

View File

@ -0,0 +1,79 @@
#include "pch.h"
AUTOHOOK_INIT()
static constexpr int LZSS_LOOKSHIFT = 4;
struct lzss_header_t
{
unsigned int id;
unsigned int actualSize;
};
// Rewrite of CLZSS::SafeUncompress to fix a vulnerability where malicious compressed payloads could cause the decompressor to try to read
// out of the bounds of the output buffer.
// clang-format off
AUTOHOOK(CLZSS__SafeDecompress, engine.dll + 0x432A10,
unsigned int, __fastcall, (void* self, const unsigned char* pInput, unsigned char* pOutput, unsigned int unBufSize))
// clang-format on
{
unsigned int totalBytes = 0;
int getCmdByte = 0;
int cmdByte = 0;
lzss_header_t header = *(lzss_header_t*)pInput;
if (!pInput || !header.actualSize || header.id != 0x53535A4C || header.actualSize > unBufSize)
return 0;
pInput += sizeof(lzss_header_t);
for (;;)
{
if (!getCmdByte)
cmdByte = *pInput++;
getCmdByte = (getCmdByte + 1) & 0x07;
if (cmdByte & 0x01)
{
int position = *pInput++ << LZSS_LOOKSHIFT;
position |= (*pInput >> LZSS_LOOKSHIFT);
position += 1;
int count = (*pInput++ & 0x0F) + 1;
if (count == 1)
break;
// Ensure reference chunk exists entirely within our buffer
if (position > totalBytes)
return 0;
totalBytes += count;
if (totalBytes > unBufSize)
return 0;
unsigned char* pSource = pOutput - position;
for (int i = 0; i < count; i++)
*pOutput++ = *pSource++;
}
else
{
totalBytes++;
if (totalBytes > unBufSize)
return 0;
*pOutput++ = *pInput++;
}
cmdByte = cmdByte >> 1;
}
if (totalBytes != header.actualSize)
return 0;
return totalBytes;
}
ON_DLL_LOAD("engine.dll", ExploitFixes_LZSS, (CModule module))
{
AUTOHOOK_DISPATCH()
}

View File

@ -0,0 +1,200 @@
#include "pch.h"
AUTOHOOK_INIT()
INT64(__fastcall* sub_F1320)(DWORD a1, char* a2);
// Reimplementation of an exploitable UTF decoding function in titanfall
bool __fastcall CheckUTF8Valid(INT64* a1, DWORD* a2, char* strData)
{
DWORD v3; // eax
char* v4; // rbx
char v5; // si
char* _strData; // rdi
char* v7; // rbp
char v11; // al
DWORD v12; // er9
DWORD v13; // ecx
DWORD v14; // edx
DWORD v15; // er8
int v16; // eax
DWORD v17; // er9
int v18; // eax
DWORD v19; // er9
DWORD v20; // ecx
int v21; // eax
int v22; // er9
DWORD v23; // edx
int v24; // eax
int v25; // er9
DWORD v26; // er9
DWORD v27; // er10
DWORD v28; // ecx
DWORD v29; // edx
DWORD v30; // er8
int v31; // eax
DWORD v32; // er10
int v33; // eax
DWORD v34; // er10
DWORD v35; // ecx
int v36; // eax
int v37; // er10
DWORD v38; // edx
int v39; // eax
int v40; // er10
DWORD v41; // er10
INT64 v43; // r8
INT64 v44; // rdx
INT64 v45; // rcx
INT64 v46; // rax
INT64 v47; // rax
char v48; // al
INT64 v49; // r8
INT64 v50; // rdx
INT64 v51; // rcx
INT64 v52; // rax
INT64 v53; // rax
v3 = a2[2];
v4 = (char*)(a1[1] + *a2);
v5 = 0;
_strData = strData;
v7 = &v4[*((UINT16*)a2 + 2)];
if (v3 >= 2)
{
++v4;
--v7;
if (v3 != 2)
{
while (1)
{
if (!MemoryAddress(v4).IsMemoryReadable(1))
return false; // INVALID
v11 = *v4++; // crash potential
if (v11 != 92)
goto LABEL_6;
v11 = *v4++;
if (v11 == 110)
break;
switch (v11)
{
case 't':
v11 = 9;
goto LABEL_6;
case 'r':
v11 = 13;
goto LABEL_6;
case 'b':
v11 = 8;
goto LABEL_6;
case 'f':
v11 = 12;
goto LABEL_6;
}
if (v11 != 117)
goto LABEL_6;
v12 = *v4 | 0x20;
v13 = v4[1] | 0x20;
v14 = v4[2] | 0x20;
v15 = v4[3] | 0x20;
v16 = 87;
if (v12 <= 0x39)
v16 = 48;
v17 = v12 - v16;
v18 = 87;
v19 = v17 << 12;
if (v13 <= 0x39)
v18 = 48;
v20 = v13 - v18;
v21 = 87;
v22 = (v20 << 8) | v19;
if (v14 <= 0x39)
v21 = 48;
v23 = v14 - v21;
v24 = 87;
v25 = (16 * v23) | v22;
if (v15 <= 0x39)
v24 = 48;
v4 += 4;
v26 = (v15 - v24) | v25;
if (v26 - 55296 <= 0x7FF)
{
if (v26 >= 0xDC00)
return true;
if (*v4 != 92 || v4[1] != 117)
return true;
v27 = v4[2] | 0x20;
v28 = v4[3] | 0x20;
v29 = v4[4] | 0x20;
v30 = v4[5] | 0x20;
v31 = 87;
if (v27 <= 0x39)
v31 = 48;
v32 = v27 - v31;
v33 = 87;
v34 = v32 << 12;
if (v28 <= 0x39)
v33 = 48;
v35 = v28 - v33;
v36 = 87;
v37 = (v35 << 8) | v34;
if (v29 <= 0x39)
v36 = 48;
v38 = v29 - v36;
v39 = 87;
v40 = (16 * v38) | v37;
if (v30 <= 0x39)
v39 = 48;
v4 += 6;
v41 = ((v30 - v39) | v40) - 56320;
if (v41 > 0x3FF)
return true;
v26 = v41 | ((v26 - 55296) << 10);
}
_strData += (DWORD)sub_F1320(v26, _strData);
LABEL_7:
if (v4 == v7)
goto LABEL_48;
}
v11 = 10;
LABEL_6:
v5 |= v11;
*_strData++ = v11;
goto LABEL_7;
}
}
LABEL_48:
return true;
}
// prevent utf8 parser from crashing when provided bad data, which can be sent through user-controlled openinvites
// clang-format off
AUTOHOOK(Rson_ParseUTF8, engine.dll + 0xEF670,
bool, __fastcall, (INT64* a1, DWORD* a2, char* strData)) // 48 89 5C 24 ? 48 89 6C 24 ? 48 89 74 24 ? 57 41 54 41 55 41 56 41 57 48 83 EC 20 8B 1A
// clang-format on
{
static void* targetRetAddr = CModule("engine.dll").FindPattern("84 C0 75 2C 49 8B 16");
// only call if we're parsing utf8 data from the network (i.e. communities), otherwise we get perf issues
void* pReturnAddress =
#ifdef _MSC_VER
_ReturnAddress()
#else
__builtin_return_address(0)
#endif
;
if (pReturnAddress == targetRetAddr && !CheckUTF8Valid(a1, a2, strData))
return false;
return Rson_ParseUTF8(a1, a2, strData);
}
ON_DLL_LOAD("engine.dll", EngineExploitFixes_UTF8Parser, (CModule module))
{
AUTOHOOK_DISPATCH()
sub_F1320 = module.FindPattern("83 F9 7F 77 08 88 0A").As<INT64(__fastcall*)(DWORD, char*)>();
}

View File

@ -1,175 +0,0 @@
// Reimplementation of a exploitable UTF decoding function in titanfall
#include "NSMem.h"
#pragma once
namespace ExploitFixes_UTF8Parser
{
bool __fastcall CheckValid(INT64* a1, DWORD* a2, char* strData)
{
static auto sub_F1320 = (INT64(__fastcall*)(DWORD a1, char* a2))NSMem::PatternScan("engine.dll", "83 F9 7F 77 08 88 0A");
DWORD v3; // eax
char* v4; // rbx
char v5; // si
char* _strData; // rdi
char* v7; // rbp
char v11; // al
DWORD v12; // er9
DWORD v13; // ecx
DWORD v14; // edx
DWORD v15; // er8
int v16; // eax
DWORD v17; // er9
int v18; // eax
DWORD v19; // er9
DWORD v20; // ecx
int v21; // eax
int v22; // er9
DWORD v23; // edx
int v24; // eax
int v25; // er9
DWORD v26; // er9
DWORD v27; // er10
DWORD v28; // ecx
DWORD v29; // edx
DWORD v30; // er8
int v31; // eax
DWORD v32; // er10
int v33; // eax
DWORD v34; // er10
DWORD v35; // ecx
int v36; // eax
int v37; // er10
DWORD v38; // edx
int v39; // eax
int v40; // er10
DWORD v41; // er10
INT64 v43; // r8
INT64 v44; // rdx
INT64 v45; // rcx
INT64 v46; // rax
INT64 v47; // rax
char v48; // al
INT64 v49; // r8
INT64 v50; // rdx
INT64 v51; // rcx
INT64 v52; // rax
INT64 v53; // rax
v3 = a2[2];
v4 = (char*)(a1[1] + *a2);
v5 = 0;
_strData = strData;
v7 = &v4[*((UINT16*)a2 + 2)];
if (v3 >= 2)
{
++v4;
--v7;
if (v3 != 2)
{
while (1)
{
if (!NSMem::IsMemoryReadable(v4, 1))
return false; // INVALID
v11 = *v4++; // crash potential
if (v11 != 92)
goto LABEL_6;
v11 = *v4++;
if (v11 == 110)
break;
switch (v11)
{
case 't':
v11 = 9;
goto LABEL_6;
case 'r':
v11 = 13;
goto LABEL_6;
case 'b':
v11 = 8;
goto LABEL_6;
case 'f':
v11 = 12;
goto LABEL_6;
}
if (v11 != 117)
goto LABEL_6;
v12 = *v4 | 0x20;
v13 = v4[1] | 0x20;
v14 = v4[2] | 0x20;
v15 = v4[3] | 0x20;
v16 = 87;
if (v12 <= 0x39)
v16 = 48;
v17 = v12 - v16;
v18 = 87;
v19 = v17 << 12;
if (v13 <= 0x39)
v18 = 48;
v20 = v13 - v18;
v21 = 87;
v22 = (v20 << 8) | v19;
if (v14 <= 0x39)
v21 = 48;
v23 = v14 - v21;
v24 = 87;
v25 = (16 * v23) | v22;
if (v15 <= 0x39)
v24 = 48;
v4 += 4;
v26 = (v15 - v24) | v25;
if (v26 - 55296 <= 0x7FF)
{
if (v26 >= 0xDC00)
return true;
if (*v4 != 92 || v4[1] != 117)
return true;
v27 = v4[2] | 0x20;
v28 = v4[3] | 0x20;
v29 = v4[4] | 0x20;
v30 = v4[5] | 0x20;
v31 = 87;
if (v27 <= 0x39)
v31 = 48;
v32 = v27 - v31;
v33 = 87;
v34 = v32 << 12;
if (v28 <= 0x39)
v33 = 48;
v35 = v28 - v33;
v36 = 87;
v37 = (v35 << 8) | v34;
if (v29 <= 0x39)
v36 = 48;
v38 = v29 - v36;
v39 = 87;
v40 = (16 * v38) | v37;
if (v30 <= 0x39)
v39 = 48;
v4 += 6;
v41 = ((v30 - v39) | v40) - 56320;
if (v41 > 0x3FF)
return true;
v26 = v41 | ((v26 - 55296) << 10);
}
_strData += (DWORD)sub_F1320(v26, _strData);
LABEL_7:
if (v4 == v7)
goto LABEL_48;
}
v11 = 10;
LABEL_6:
v5 |= v11;
*_strData++ = v11;
goto LABEL_7;
}
}
LABEL_48:
return true;
}
} // namespace ExploitFixes_UTF8Parser

View File

@ -1,87 +1,69 @@
#include "pch.h"
#include "filesystem.h"
#include "hooks.h"
#include "hookutils.h"
#include "sourceinterface.h"
#include "modmanager.h"
#include <iostream>
#include <sstream>
// hook forward declares
typedef FileHandle_t (*ReadFileFromVPKType)(VPKData* vpkInfo, __int64* b, char* filename);
ReadFileFromVPKType readFileFromVPK;
FileHandle_t ReadFileFromVPKHook(VPKData* vpkInfo, __int64* b, char* filename);
AUTOHOOK_INIT()
typedef bool (*ReadFromCacheType)(IFileSystem* filesystem, char* path, void* result);
ReadFromCacheType readFromCache;
bool ReadFromCacheHook(IFileSystem* filesystem, char* path, void* result);
using namespace R2;
typedef void (*AddSearchPathType)(IFileSystem* fileSystem, const char* pPath, const char* pathID, SearchPathAdd_t addType);
AddSearchPathType addSearchPathOriginal;
void AddSearchPathHook(IFileSystem* fileSystem, const char* pPath, const char* pathID, SearchPathAdd_t addType);
bool bReadingOriginalFile = false;
std::string sCurrentModPath;
typedef FileHandle_t (*ReadFileFromFilesystemType)(
IFileSystem* filesystem, const char* pPath, const char* pOptions, int64_t a4, uint32_t a5);
ReadFileFromFilesystemType readFileFromFilesystem;
FileHandle_t ReadFileFromFilesystemHook(IFileSystem* filesystem, const char* pPath, const char* pOptions, int64_t a4, uint32_t a5);
ConVar* Cvar_ns_fs_log_reads;
typedef VPKData* (*MountVPKType)(IFileSystem* fileSystem, const char* vpkPath);
MountVPKType mountVPK;
VPKData* MountVPKHook(IFileSystem* fileSystem, const char* vpkPath);
bool readingOriginalFile;
std::string currentModPath;
SourceInterface<IFileSystem>* g_Filesystem;
void InitialiseFilesystem(HMODULE baseAddress)
// use the R2 namespace for game funcs
namespace R2
{
g_Filesystem = new SourceInterface<IFileSystem>("filesystem_stdio.dll", "VFileSystem017");
SourceInterface<IFileSystem>* g_pFilesystem;
// create hooks
HookEnabler hook;
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x5CBA0, &ReadFileFromVPKHook, reinterpret_cast<LPVOID*>(&readFileFromVPK));
ENABLER_CREATEHOOK(
hook,
reinterpret_cast<void*>((*g_Filesystem)->m_vtable->ReadFromCache),
&ReadFromCacheHook,
reinterpret_cast<LPVOID*>(&readFromCache));
ENABLER_CREATEHOOK(
hook,
reinterpret_cast<void*>((*g_Filesystem)->m_vtable->AddSearchPath),
&AddSearchPathHook,
reinterpret_cast<LPVOID*>(&addSearchPathOriginal));
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x15F20, &ReadFileFromFilesystemHook, reinterpret_cast<LPVOID*>(&readFileFromFilesystem));
ENABLER_CREATEHOOK(
hook, reinterpret_cast<void*>((*g_Filesystem)->m_vtable->MountVPK), &MountVPKHook, reinterpret_cast<LPVOID*>(&mountVPK));
}
std::string ReadVPKFile(const char* path)
{
// read scripts.rson file, todo: check if this can be overwritten
FileHandle_t fileHandle = (*g_Filesystem)->m_vtable2->Open(&(*g_Filesystem)->m_vtable2, path, "rb", "GAME", 0);
std::stringstream fileStream;
int bytesRead = 0;
char data[4096];
do
std::string ReadVPKFile(const char* path)
{
bytesRead = (*g_Filesystem)->m_vtable2->Read(&(*g_Filesystem)->m_vtable2, data, (int)std::size(data), fileHandle);
fileStream.write(data, bytesRead);
} while (bytesRead == std::size(data));
// read scripts.rson file, todo: check if this can be overwritten
FileHandle_t fileHandle = (*g_pFilesystem)->m_vtable2->Open(&(*g_pFilesystem)->m_vtable2, path, "rb", "GAME", 0);
(*g_Filesystem)->m_vtable2->Close(*g_Filesystem, fileHandle);
std::stringstream fileStream;
int bytesRead = 0;
char data[4096];
do
{
bytesRead = (*g_pFilesystem)->m_vtable2->Read(&(*g_pFilesystem)->m_vtable2, data, (int)std::size(data), fileHandle);
fileStream.write(data, bytesRead);
} while (bytesRead == std::size(data));
return fileStream.str();
}
(*g_pFilesystem)->m_vtable2->Close(*g_pFilesystem, fileHandle);
std::string ReadVPKOriginalFile(const char* path)
return fileStream.str();
}
std::string ReadVPKOriginalFile(const char* path)
{
// todo: should probably set search path to be g_pModName here also
bReadingOriginalFile = true;
std::string ret = ReadVPKFile(path);
bReadingOriginalFile = false;
return ret;
}
} // namespace R2
// clang-format off
HOOK(AddSearchPathHook, AddSearchPath,
void, __fastcall, (IFileSystem * fileSystem, const char* pPath, const char* pathID, SearchPathAdd_t addType))
// clang-format on
{
readingOriginalFile = true;
std::string ret = ReadVPKFile(path);
readingOriginalFile = false;
AddSearchPath(fileSystem, pPath, pathID, addType);
return ret;
// make sure current mod paths are at head
if (!strcmp(pathID, "GAME") && sCurrentModPath.compare(pPath) && addType == PATH_ADD_TO_HEAD)
{
AddSearchPath(fileSystem, sCurrentModPath.c_str(), "GAME", PATH_ADD_TO_HEAD);
AddSearchPath(fileSystem, GetCompiledAssetsPath().string().c_str(), "GAME", PATH_ADD_TO_HEAD);
}
}
void SetNewModSearchPaths(Mod* mod)
@ -90,96 +72,84 @@ void SetNewModSearchPaths(Mod* mod)
// in the future we could also determine whether the file we're setting paths for needs a mod dir, or compiled assets
if (mod != nullptr)
{
if ((fs::absolute(mod->ModDirectory) / MOD_OVERRIDE_DIR).string().compare(currentModPath))
if ((fs::absolute(mod->m_ModDirectory) / MOD_OVERRIDE_DIR).string().compare(sCurrentModPath))
{
spdlog::info("changing mod search path from {} to {}", currentModPath, mod->ModDirectory.string());
spdlog::info("changing mod search path from {} to {}", sCurrentModPath, mod->m_ModDirectory.string());
addSearchPathOriginal(
&*(*g_Filesystem), (fs::absolute(mod->ModDirectory) / MOD_OVERRIDE_DIR).string().c_str(), "GAME", PATH_ADD_TO_HEAD);
currentModPath = (fs::absolute(mod->ModDirectory) / MOD_OVERRIDE_DIR).string();
AddSearchPath(
&*(*g_pFilesystem), (fs::absolute(mod->m_ModDirectory) / MOD_OVERRIDE_DIR).string().c_str(), "GAME", PATH_ADD_TO_HEAD);
sCurrentModPath = (fs::absolute(mod->m_ModDirectory) / MOD_OVERRIDE_DIR).string();
}
}
else // push compiled to head
addSearchPathOriginal(&*(*g_Filesystem), fs::absolute(GetCompiledAssetsPath()).string().c_str(), "GAME", PATH_ADD_TO_HEAD);
AddSearchPath(&*(*g_pFilesystem), fs::absolute(GetCompiledAssetsPath()).string().c_str(), "GAME", PATH_ADD_TO_HEAD);
}
bool TryReplaceFile(char* path, bool shouldCompile)
bool TryReplaceFile(const char* pPath, bool shouldCompile)
{
if (readingOriginalFile)
if (bReadingOriginalFile)
return false;
if (shouldCompile)
(*g_ModManager).CompileAssetsForFile(path);
g_pModManager->CompileAssetsForFile(pPath);
// idk how efficient the lexically normal check is
// can't just set all /s in path to \, since some paths aren't in writeable memory
auto file = g_ModManager->m_modFiles.find(fs::path(path).lexically_normal().string());
if (file != g_ModManager->m_modFiles.end())
auto file = g_pModManager->m_ModFiles.find(g_pModManager->NormaliseModFilePath(fs::path(pPath)));
if (file != g_pModManager->m_ModFiles.end())
{
SetNewModSearchPaths(file->second.owningMod);
SetNewModSearchPaths(file->second.m_pOwningMod);
return true;
}
return false;
}
FileHandle_t ReadFileFromVPKHook(VPKData* vpkInfo, __int64* b, char* filename)
// force modded files to be read from mods, not cache
// clang-format off
HOOK(ReadFromCacheHook, ReadFromCache,
bool, __fastcall, (IFileSystem * filesystem, char* pPath, void* result))
// clang-format off
{
// move this to a convar at some point when we can read them in native
// spdlog::info("ReadFileFromVPKHook {} {}", filename, vpkInfo->path);
if (TryReplaceFile(pPath, true))
return false;
// there is literally never any reason to compile here, since we'll always compile in ReadFileFromFilesystemHook in the same codepath
// this is called
return ReadFromCache(filesystem, pPath, result);
}
// force modded files to be read from mods, not vpk
// clang-format off
AUTOHOOK(ReadFileFromVPK, filesystem_stdio.dll + 0x5CBA0,
FileHandle_t, __fastcall, (VPKData* vpkInfo, uint64_t* b, char* filename))
// clang-format on
{
// don't compile here because this is only ever called from OpenEx, which already compiles
if (TryReplaceFile(filename, false))
{
*b = -1;
return b;
}
return readFileFromVPK(vpkInfo, b, filename);
return ReadFileFromVPK(vpkInfo, b, filename);
}
bool ReadFromCacheHook(IFileSystem* filesystem, char* path, void* result)
// clang-format off
AUTOHOOK(CBaseFileSystem__OpenEx, filesystem_stdio.dll + 0x15F50,
FileHandle_t, __fastcall, (IFileSystem* filesystem, const char* pPath, const char* pOptions, uint32_t flags, const char* pPathID, char **ppszResolvedFilename))
// clang-format on
{
// move this to a convar at some point when we can read them in native
// spdlog::info("ReadFromCacheHook {}", path);
if (TryReplaceFile(path, true))
return false;
return readFromCache(filesystem, path, result);
TryReplaceFile(pPath, true);
return CBaseFileSystem__OpenEx(filesystem, pPath, pOptions, flags, pPathID, ppszResolvedFilename);
}
void AddSearchPathHook(IFileSystem* fileSystem, const char* pPath, const char* pathID, SearchPathAdd_t addType)
HOOK(MountVPKHook, MountVPK, VPKData*, , (IFileSystem * fileSystem, const char* pVpkPath))
{
addSearchPathOriginal(fileSystem, pPath, pathID, addType);
spdlog::info("MountVPK {}", pVpkPath);
VPKData* ret = MountVPK(fileSystem, pVpkPath);
// make sure current mod paths are at head
if (!strcmp(pathID, "GAME") && currentModPath.compare(pPath) && addType == PATH_ADD_TO_HEAD)
for (Mod mod : g_pModManager->m_LoadedMods)
{
addSearchPathOriginal(fileSystem, currentModPath.c_str(), "GAME", PATH_ADD_TO_HEAD);
addSearchPathOriginal(fileSystem, GetCompiledAssetsPath().string().c_str(), "GAME", PATH_ADD_TO_HEAD);
}
}
FileHandle_t ReadFileFromFilesystemHook(IFileSystem* filesystem, const char* pPath, const char* pOptions, int64_t a4, uint32_t a5)
{
// this isn't super efficient, but it's necessary, since calling addsearchpath in readfilefromvpk doesn't work, possibly refactor later
// it also might be possible to hook functions that are called later, idk look into callstack for ReadFileFromVPK
if (!readingOriginalFile)
TryReplaceFile((char*)pPath, true);
return readFileFromFilesystem(filesystem, pPath, pOptions, a4, a5);
}
VPKData* MountVPKHook(IFileSystem* fileSystem, const char* vpkPath)
{
spdlog::info("MountVPK {}", vpkPath);
VPKData* ret = mountVPK(fileSystem, vpkPath);
for (Mod mod : g_ModManager->m_loadedMods)
{
if (!mod.Enabled)
if (!mod.m_bEnabled)
continue;
for (ModVPKEntry& vpkEntry : mod.Vpks)
@ -189,13 +159,13 @@ VPKData* MountVPKHook(IFileSystem* fileSystem, const char* vpkPath)
{
// resolve vpk name and try to load one with the same name
// todo: we should be unloading these on map unload manually
std::string mapName(fs::path(vpkPath).filename().string());
std::string mapName(fs::path(pVpkPath).filename().string());
std::string modMapName(fs::path(vpkEntry.m_sVpkPath.c_str()).filename().string());
if (mapName.compare(modMapName))
continue;
}
VPKData* loaded = mountVPK(fileSystem, vpkEntry.m_sVpkPath.c_str());
VPKData* loaded = MountVPK(fileSystem, vpkEntry.m_sVpkPath.c_str());
if (!ret) // this is primarily for map vpks and stuff, so the map's vpk is what gets returned from here
ret = loaded;
}
@ -203,3 +173,14 @@ VPKData* MountVPKHook(IFileSystem* fileSystem, const char* vpkPath)
return ret;
}
ON_DLL_LOAD("filesystem_stdio.dll", Filesystem, (CModule module))
{
AUTOHOOK_DISPATCH()
R2::g_pFilesystem = new SourceInterface<IFileSystem>("filesystem_stdio.dll", "VFileSystem017");
AddSearchPathHook.Dispatch((*g_pFilesystem)->m_vtable->AddSearchPath);
ReadFromCacheHook.Dispatch((*g_pFilesystem)->m_vtable->ReadFromCache);
MountVPKHook.Dispatch((*g_pFilesystem)->m_vtable->MountVPK);
}

View File

@ -59,7 +59,8 @@ class IFileSystem
FileHandle_t (*Open)(
IFileSystem::VTable2** fileSystem, const char* pFileName, const char* pOptions, const char* pathID, int64_t unknown);
void (*Close)(IFileSystem* fileSystem, FileHandle_t file);
void* unknown2[6];
long long (*Seek)(IFileSystem::VTable2** fileSystem, FileHandle_t file, long long offset, long long whence);
void* unknown2[5];
bool (*FileExists)(IFileSystem::VTable2** fileSystem, const char* pFileName, const char* pPathID);
};
@ -67,8 +68,11 @@ class IFileSystem
VTable2* m_vtable2;
};
std::string ReadVPKFile(const char* path);
std::string ReadVPKOriginalFile(const char* path);
// use the R2 namespace for game funcs
namespace R2
{
extern SourceInterface<IFileSystem>* g_pFilesystem;
void InitialiseFilesystem(HMODULE baseAddress);
extern SourceInterface<IFileSystem>* g_Filesystem;
std::string ReadVPKFile(const char* path);
std::string ReadVPKOriginalFile(const char* path);
} // namespace R2

View File

@ -1,119 +0,0 @@
#include "pch.h"
#include "convar.h"
#include "gameutils.h"
// memory
IMemAlloc* g_pMemAllocSingleton;
typedef IMemAlloc* (*CreateGlobalMemAllocType)();
CreateGlobalMemAllocType CreateGlobalMemAlloc;
// cmd.h
Cbuf_GetCurrentPlayerType Cbuf_GetCurrentPlayer;
Cbuf_AddTextType Cbuf_AddText;
Cbuf_ExecuteType Cbuf_Execute;
// hoststate stuff
CHostState* g_pHostState;
// cengine stuff
CEngine* g_pEngine;
server_state_t* sv_m_State;
// network stuff
ConVar* Cvar_hostport;
// playlist stuff
GetCurrentPlaylistType GetCurrentPlaylistName;
SetCurrentPlaylistType SetCurrentPlaylist;
SetPlaylistVarOverrideType SetPlaylistVarOverride;
GetCurrentPlaylistVarType GetCurrentPlaylistVar;
// server entity stuff
Server_GetEntityByIndexType Server_GetEntityByIndex;
// auth
char* g_LocalPlayerUserID;
char* g_LocalPlayerOriginToken;
// misc stuff
ErrorType Error;
CommandLineType CommandLine;
Plat_FloatTimeType Plat_FloatTime;
ThreadInServerFrameThreadType ThreadInServerFrameThread;
GetBaseLocalClientType GetBaseLocalClient;
void InitialiseEngineGameUtilFunctions(HMODULE baseAddress)
{
Cbuf_GetCurrentPlayer = (Cbuf_GetCurrentPlayerType)((char*)baseAddress + 0x120630);
Cbuf_AddText = (Cbuf_AddTextType)((char*)baseAddress + 0x1203B0);
Cbuf_Execute = (Cbuf_ExecuteType)((char*)baseAddress + 0x1204B0);
g_pHostState = (CHostState*)((char*)baseAddress + 0x7CF180);
g_pEngine = *(CEngine**)((char*)baseAddress + 0x7D70C8);
sv_m_State = (server_state_t*)((char*)baseAddress + 0x12A53D48);
GetCurrentPlaylistName = (GetCurrentPlaylistType)((char*)baseAddress + 0x18C640);
SetCurrentPlaylist = (SetCurrentPlaylistType)((char*)baseAddress + 0x18EB20);
SetPlaylistVarOverride = (SetPlaylistVarOverrideType)((char*)baseAddress + 0x18ED00);
GetCurrentPlaylistVar = (GetCurrentPlaylistVarType)((char*)baseAddress + 0x18C680);
g_LocalPlayerUserID = (char*)baseAddress + 0x13F8E688;
g_LocalPlayerOriginToken = (char*)baseAddress + 0x13979C80;
GetBaseLocalClient = (GetBaseLocalClientType)((char*)baseAddress + 0x78200);
/* NOTE:
g_pCVar->FindVar("convar_name") now works. These are no longer needed.
You can also itterate over every ConVar using CCVarIteratorInternal
dump the pointers to a vector and access them from there.
Example:
std::vector<ConVar*> g_pAllConVars;
for (auto& map : g_pCVar->DumpToMap())
{
ConVar* pConVar = g_pCVar->FindVar(map.first.c_str());
if (pConVar)
{
g_pAllConVars.push_back(pConVar);
}
}*/
Cvar_hostport = (ConVar*)((char*)baseAddress + 0x13FA6070);
}
void InitialiseServerGameUtilFunctions(HMODULE baseAddress)
{
Server_GetEntityByIndex = (Server_GetEntityByIndexType)((char*)baseAddress + 0xFB820);
}
void InitialiseTier0GameUtilFunctions(HMODULE baseAddress)
{
if (!baseAddress)
{
spdlog::critical("tier0 base address is null, but it should be already loaded");
throw "tier0 base address is null, but it should be already loaded";
}
if (g_pMemAllocSingleton)
return; // seems this function was already called
CreateGlobalMemAlloc = reinterpret_cast<CreateGlobalMemAllocType>(GetProcAddress(baseAddress, "CreateGlobalMemAlloc"));
IMemAlloc** ppMemAllocSingleton = reinterpret_cast<IMemAlloc**>(GetProcAddress(baseAddress, "g_pMemAllocSingleton"));
if (!ppMemAllocSingleton)
{
spdlog::critical("Address of g_pMemAllocSingleton is a null pointer, this should never happen");
throw "Address of g_pMemAllocSingleton is a null pointer, this should never happen";
}
if (!*ppMemAllocSingleton)
{
g_pMemAllocSingleton = CreateGlobalMemAlloc();
*ppMemAllocSingleton = g_pMemAllocSingleton;
spdlog::info("Created new g_pMemAllocSingleton");
}
else
{
g_pMemAllocSingleton = *ppMemAllocSingleton;
}
Error = reinterpret_cast<ErrorType>(GetProcAddress(baseAddress, "Error"));
CommandLine = reinterpret_cast<CommandLineType>(GetProcAddress(baseAddress, "CommandLine"));
Plat_FloatTime = reinterpret_cast<Plat_FloatTimeType>(GetProcAddress(baseAddress, "Plat_FloatTime"));
ThreadInServerFrameThread = reinterpret_cast<ThreadInServerFrameThreadType>(GetProcAddress(baseAddress, "ThreadInServerFrameThread"));
}

View File

@ -1,249 +0,0 @@
#pragma once
#include "convar.h"
// memory
class IMemAlloc
{
public:
struct VTable
{
void* unknown[1]; // alloc debug
void* (*Alloc)(IMemAlloc* memAlloc, size_t nSize);
void* unknown2[1]; // realloc debug
void* (*Realloc)(IMemAlloc* memAlloc, void* pMem, size_t nSize);
void* unknown3[1]; // free #1
void (*Free)(IMemAlloc* memAlloc, void* pMem);
void* unknown4[2]; // nullsubs, maybe CrtSetDbgFlag
size_t (*GetSize)(IMemAlloc* memAlloc, void* pMem);
void* unknown5[9]; // they all do literally nothing
void (*DumpStats)(IMemAlloc* memAlloc);
void (*DumpStatsFileBase)(IMemAlloc* memAlloc, const char* pchFileBase);
void* unknown6[4];
int (*heapchk)(IMemAlloc* memAlloc);
};
VTable* m_vtable;
};
extern IMemAlloc* g_pMemAllocSingleton;
typedef IMemAlloc* (*CreateGlobalMemAllocType)();
extern CreateGlobalMemAllocType CreateGlobalMemAlloc;
// cmd.h
enum class ECommandTarget_t
{
CBUF_FIRST_PLAYER = 0,
CBUF_LAST_PLAYER = 1, // MAX_SPLITSCREEN_CLIENTS - 1, MAX_SPLITSCREEN_CLIENTS = 2
CBUF_SERVER = CBUF_LAST_PLAYER + 1,
CBUF_COUNT,
};
enum class cmd_source_t
{
// Added to the console buffer by gameplay code. Generally unrestricted.
kCommandSrcCode,
// Sent from code via engine->ClientCmd, which is restricted to commands visible
// via FCVAR_CLIENTCMD_CAN_EXECUTE.
kCommandSrcClientCmd,
// Typed in at the console or via a user key-bind. Generally unrestricted, although
// the client will throttle commands sent to the server this way to 16 per second.
kCommandSrcUserInput,
// Came in over a net connection as a clc_stringcmd
// host_client will be valid during this state.
//
// Restricted to FCVAR_GAMEDLL commands (but not convars) and special non-ConCommand
// server commands hardcoded into gameplay code (e.g. "joingame")
kCommandSrcNetClient,
// Received from the server as the client
//
// Restricted to commands with FCVAR_SERVER_CAN_EXECUTE
kCommandSrcNetServer,
// Being played back from a demo file
//
// Not currently restricted by convar flag, but some commands manually ignore calls
// from this source. FIXME: Should be heavily restricted as demo commands can come
// from untrusted sources.
kCommandSrcDemoFile,
// Invalid value used when cleared
kCommandSrcInvalid = -1
};
typedef ECommandTarget_t (*Cbuf_GetCurrentPlayerType)();
extern Cbuf_GetCurrentPlayerType Cbuf_GetCurrentPlayer;
// compared to the defs i've seen, this is missing an arg, it could be nTickInterval or source, not sure, guessing it's source
typedef void (*Cbuf_AddTextType)(ECommandTarget_t eTarget, const char* text, cmd_source_t source);
extern Cbuf_AddTextType Cbuf_AddText;
typedef void (*Cbuf_ExecuteType)();
extern Cbuf_ExecuteType Cbuf_Execute;
// commandline stuff
class CCommandLine
{
public:
// based on the defs in the 2013 source sdk, but for some reason has an extra function (may be another CreateCmdLine overload?)
// these seem to line up with what they should be though
virtual void CreateCmdLine(const char* commandline) {}
virtual void CreateCmdLine(int argc, char** argv) {}
virtual void unknown() {}
virtual const char* GetCmdLine(void) const {}
virtual const char* CheckParm(const char* psz, const char** ppszValue = 0) const {}
virtual void RemoveParm() const {}
virtual void AppendParm(const char* pszParm, const char* pszValues) {}
virtual const char* ParmValue(const char* psz, const char* pDefaultVal = 0) const {}
virtual int ParmValue(const char* psz, int nDefaultVal) const {}
virtual float ParmValue(const char* psz, float flDefaultVal) const {}
virtual int ParmCount() const {}
virtual int FindParm(const char* psz) const {}
virtual const char* GetParm(int nIndex) const {}
virtual void SetParm(int nIndex, char const* pParm) {}
// virtual const char** GetParms() const {}
};
// hoststate stuff
enum HostState_t
{
HS_NEW_GAME = 0,
HS_LOAD_GAME,
HS_CHANGE_LEVEL_SP,
HS_CHANGE_LEVEL_MP,
HS_RUN,
HS_GAME_SHUTDOWN,
HS_SHUTDOWN,
HS_RESTART,
};
struct CHostState
{
public:
HostState_t m_iCurrentState;
HostState_t m_iNextState;
float m_vecLocation[3];
float m_angLocation[3];
char m_levelName[32];
char m_mapGroupName[32];
char m_landmarkName[32];
char m_saveName[32];
float m_flShortFrameTime; // run a few one-tick frames to avoid large timesteps while loading assets
bool m_activeGame;
bool m_bRememberLocation;
bool m_bBackgroundLevel;
bool m_bWaitingForConnection;
bool m_bLetToolsOverrideLoadGameEnts; // During a load game, this tells Foundry to override ents that are selected in Hammer.
bool m_bSplitScreenConnect;
bool m_bGameHasShutDownAndFlushedMemory; // This is false once we load a map into memory, and set to true once the map is unloaded and
// all memory flushed
bool m_bWorkshopMapDownloadPending;
};
extern CHostState* g_pHostState;
// cengine stuff
enum EngineQuitState
{
QUIT_NOTQUITTING = 0,
QUIT_TODESKTOP,
QUIT_RESTART
};
enum EngineState_t
{
DLL_INACTIVE = 0, // no dll
DLL_ACTIVE, // engine is focused
DLL_CLOSE, // closing down dll
DLL_RESTART, // engine is shutting down but will restart right away
DLL_PAUSED, // engine is paused, can become active from this state
};
class CEngine
{
public:
virtual void unknown() {} // unsure if this is where
virtual bool Load(bool dedicated, const char* baseDir) {}
virtual void Unload() {}
virtual void SetNextState(EngineState_t iNextState) {}
virtual EngineState_t GetState() {}
virtual void Frame() {}
virtual double GetFrameTime() {}
virtual float GetCurTime() {}
EngineQuitState m_nQuitting;
EngineState_t m_nDllState;
EngineState_t m_nNextDllState;
double m_flCurrentTime;
float m_flFrameTime;
double m_flPreviousTime;
float m_flFilteredTime;
float m_flMinFrameTime; // Expected duration of a frame, or zero if it is unlimited.
};
extern CEngine* g_pEngine;
enum server_state_t
{
ss_dead = 0, // Dead
ss_loading, // Spawning
ss_active, // Running
ss_paused, // Running, but paused
};
extern server_state_t* sv_m_State;
// network stuff
extern ConVar* Cvar_hostport;
// playlist stuff
typedef const char* (*GetCurrentPlaylistType)();
extern GetCurrentPlaylistType GetCurrentPlaylistName;
typedef void (*SetCurrentPlaylistType)(const char* playlistName);
extern SetCurrentPlaylistType SetCurrentPlaylist;
typedef void (*SetPlaylistVarOverrideType)(const char* varName, const char* value);
extern SetPlaylistVarOverrideType SetPlaylistVarOverride;
typedef char* (*GetCurrentPlaylistVarType)(const char* varName, bool useOverrides);
extern GetCurrentPlaylistVarType GetCurrentPlaylistVar;
// server entity stuff
typedef void* (*Server_GetEntityByIndexType)(int index);
extern Server_GetEntityByIndexType Server_GetEntityByIndex;
// auth
extern char* g_LocalPlayerUserID;
extern char* g_LocalPlayerOriginToken;
// misc stuff
typedef void (*ErrorType)(const char* fmt, ...);
extern ErrorType Error;
typedef CCommandLine* (*CommandLineType)();
extern CommandLineType CommandLine;
typedef double (*Plat_FloatTimeType)();
extern Plat_FloatTimeType Plat_FloatTime;
typedef bool (*ThreadInServerFrameThreadType)();
extern ThreadInServerFrameThreadType ThreadInServerFrameThread;
typedef void* (*GetBaseLocalClientType)();
extern GetBaseLocalClientType GetBaseLocalClient;
void InitialiseEngineGameUtilFunctions(HMODULE baseAddress);
void InitialiseServerGameUtilFunctions(HMODULE baseAddress);
void InitialiseTier0GameUtilFunctions(HMODULE baseAddress);

View File

@ -1,61 +1,193 @@
#include "pch.h"
#include "hooks.h"
#include "hookutils.h"
#include "sigscanning.h"
#include "dedicated.h"
#include <iostream>
#include <wchar.h>
#include <iostream>
#include <vector>
#include <fstream>
#include <sstream>
#include <filesystem>
#include <Psapi.h>
typedef LPSTR (*GetCommandLineAType)();
LPSTR GetCommandLineAHook();
AUTOHOOK_INIT()
typedef HMODULE (*LoadLibraryExAType)(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags);
HMODULE LoadLibraryExAHook(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags);
typedef HMODULE (*LoadLibraryAType)(LPCSTR lpLibFileName);
HMODULE LoadLibraryAHook(LPCSTR lpLibFileName);
typedef HMODULE (*LoadLibraryExWType)(LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags);
HMODULE LoadLibraryExWHook(LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags);
typedef HMODULE (*LoadLibraryWType)(LPCWSTR lpLibFileName);
HMODULE LoadLibraryWHook(LPCWSTR lpLibFileName);
GetCommandLineAType GetCommandLineAOriginal;
LoadLibraryExAType LoadLibraryExAOriginal;
LoadLibraryAType LoadLibraryAOriginal;
LoadLibraryExWType LoadLibraryExWOriginal;
LoadLibraryWType LoadLibraryWOriginal;
void InstallInitialHooks()
// called from the ON_DLL_LOAD macros
__dllLoadCallback::__dllLoadCallback(
eDllLoadCallbackSide side, const std::string dllName, DllLoadCallbackFuncType callback, std::string uniqueStr, std::string reliesOn)
{
if (MH_Initialize() != MH_OK)
spdlog::error("MH_Initialize (minhook initialization) failed");
// parse reliesOn array from string
std::vector<std::string> reliesOnArray;
HookEnabler hook;
ENABLER_CREATEHOOK(
hook, reinterpret_cast<void*>(&GetCommandLineA), &GetCommandLineAHook, reinterpret_cast<LPVOID*>(&GetCommandLineAOriginal));
ENABLER_CREATEHOOK(
hook, reinterpret_cast<void*>(&LoadLibraryExA), &LoadLibraryExAHook, reinterpret_cast<LPVOID*>(&LoadLibraryExAOriginal));
ENABLER_CREATEHOOK(hook, reinterpret_cast<void*>(&LoadLibraryA), &LoadLibraryAHook, reinterpret_cast<LPVOID*>(&LoadLibraryAOriginal));
ENABLER_CREATEHOOK(
hook, reinterpret_cast<void*>(&LoadLibraryExW), &LoadLibraryExWHook, reinterpret_cast<LPVOID*>(&LoadLibraryExWOriginal));
ENABLER_CREATEHOOK(hook, reinterpret_cast<void*>(&LoadLibraryW), &LoadLibraryWHook, reinterpret_cast<LPVOID*>(&LoadLibraryWOriginal));
if (reliesOn.length() && reliesOn[0] != '(')
{
reliesOnArray.push_back(reliesOn);
}
else
{
// follows the format (tag, tag, tag)
std::string sCurrentTag;
for (int i = 1; i < reliesOn.length(); i++)
{
if (!isspace(reliesOn[i]))
{
if (reliesOn[i] == ',' || reliesOn[i] == ')')
{
reliesOnArray.push_back(sCurrentTag);
sCurrentTag = "";
}
else
sCurrentTag += reliesOn[i];
}
}
}
switch (side)
{
case eDllLoadCallbackSide::UNSIDED:
{
AddDllLoadCallback(dllName, callback, uniqueStr, reliesOnArray);
break;
}
case eDllLoadCallbackSide::CLIENT:
{
AddDllLoadCallbackForClient(dllName, callback, uniqueStr, reliesOnArray);
break;
}
case eDllLoadCallbackSide::DEDICATED_SERVER:
{
AddDllLoadCallbackForDedicatedServer(dllName, callback, uniqueStr, reliesOnArray);
break;
}
}
}
LPSTR GetCommandLineAHook()
void __fileAutohook::Dispatch()
{
for (__autohook* hook : hooks)
hook->Dispatch();
}
void __fileAutohook::DispatchForModule(const char* pModuleName)
{
const int iModuleNameLen = strlen(pModuleName);
for (__autohook* hook : hooks)
if ((hook->iAddressResolutionMode == __autohook::OFFSET_STRING && !strncmp(pModuleName, hook->pAddrString, iModuleNameLen)) ||
(hook->iAddressResolutionMode == __autohook::PROCADDRESS && !strcmp(pModuleName, hook->pModuleName)))
hook->Dispatch();
}
ManualHook::ManualHook(const char* funcName, LPVOID func) : pHookFunc(func), ppOrigFunc(nullptr)
{
const int iFuncNameStrlen = strlen(funcName);
pFuncName = new char[iFuncNameStrlen];
memcpy(pFuncName, funcName, iFuncNameStrlen);
}
ManualHook::ManualHook(const char* funcName, LPVOID* orig, LPVOID func) : pHookFunc(func), ppOrigFunc(orig)
{
const int iFuncNameStrlen = strlen(funcName);
pFuncName = new char[iFuncNameStrlen];
memcpy(pFuncName, funcName, iFuncNameStrlen);
}
bool ManualHook::Dispatch(LPVOID addr, LPVOID* orig)
{
if (orig)
ppOrigFunc = orig;
if (MH_CreateHook(addr, pHookFunc, ppOrigFunc) == MH_OK)
{
if (MH_EnableHook(addr) == MH_OK)
{
spdlog::info("Enabling hook {}", pFuncName);
return true;
}
else
spdlog::error("MH_EnableHook failed for function {}", pFuncName);
}
else
spdlog::error("MH_CreateHook failed for function {}", pFuncName);
return false;
}
// dll load callback stuff
// this allows for code to register callbacks to be run as soon as a dll is loaded, mainly to allow for patches to be made on dll load
struct DllLoadCallback
{
std::string dll;
DllLoadCallbackFuncType callback;
std::string tag;
std::vector<std::string> reliesOn;
bool called;
};
// HACK: declaring and initialising this vector at file scope crashes on debug builds due to static initialisation order
// using a static var like this ensures that the vector is initialised lazily when it's used
std::vector<DllLoadCallback>& GetDllLoadCallbacks()
{
static std::vector<DllLoadCallback> vec = std::vector<DllLoadCallback>();
return vec;
}
void AddDllLoadCallback(std::string dll, DllLoadCallbackFuncType callback, std::string tag, std::vector<std::string> reliesOn)
{
DllLoadCallback& callbackStruct = GetDllLoadCallbacks().emplace_back();
callbackStruct.dll = dll;
callbackStruct.callback = callback;
callbackStruct.tag = tag;
callbackStruct.reliesOn = reliesOn;
callbackStruct.called = false;
}
void AddDllLoadCallbackForDedicatedServer(
std::string dll, DllLoadCallbackFuncType callback, std::string tag, std::vector<std::string> reliesOn)
{
if (!IsDedicatedServer())
return;
AddDllLoadCallback(dll, callback, tag, reliesOn);
}
void AddDllLoadCallbackForClient(std::string dll, DllLoadCallbackFuncType callback, std::string tag, std::vector<std::string> reliesOn)
{
if (IsDedicatedServer())
return;
AddDllLoadCallback(dll, callback, tag, reliesOn);
}
void MakeHook(LPVOID pTarget, LPVOID pDetour, void* ppOriginal, const char* pFuncName)
{
char* pStrippedFuncName = (char*)pFuncName;
// strip & char from funcname
if (*pStrippedFuncName == '&')
pStrippedFuncName++;
if (MH_CreateHook(pTarget, pDetour, (LPVOID*)ppOriginal) == MH_OK)
{
if (MH_EnableHook(pTarget) == MH_OK)
spdlog::info("Enabling hook {}", pStrippedFuncName);
else
spdlog::error("MH_EnableHook failed for function {}", pStrippedFuncName);
}
else
spdlog::error("MH_CreateHook failed for function {}", pStrippedFuncName);
}
AUTOHOOK_ABSOLUTEADDR(_GetCommandLineA, GetCommandLineA, LPSTR, WINAPI, ())
{
static char* cmdlineModified;
static char* cmdlineOrg;
if (cmdlineOrg == nullptr || cmdlineModified == nullptr)
{
cmdlineOrg = GetCommandLineAOriginal();
cmdlineOrg = _GetCommandLineA();
bool isDedi = strstr(cmdlineOrg, "-dedicated"); // well, this one has to be a real argument
bool ignoreStartupArgs = strstr(cmdlineOrg, "-nostartupargs");
@ -111,77 +243,86 @@ LPSTR GetCommandLineAHook()
return cmdlineModified;
}
// dll load callback stuff
// this allows for code to register callbacks to be run as soon as a dll is loaded, mainly to allow for patches to be made on dll load
struct DllLoadCallback
{
std::string dll;
DllLoadCallbackFuncType callback;
bool called;
};
std::vector<DllLoadCallback*> dllLoadCallbacks;
void AddDllLoadCallback(std::string dll, DllLoadCallbackFuncType callback)
{
DllLoadCallback* callbackStruct = new DllLoadCallback;
callbackStruct->dll = dll;
callbackStruct->callback = callback;
callbackStruct->called = false;
dllLoadCallbacks.push_back(callbackStruct);
}
void AddDllLoadCallbackForDedicatedServer(std::string dll, DllLoadCallbackFuncType callback)
{
if (!IsDedicatedServer())
return;
DllLoadCallback* callbackStruct = new DllLoadCallback;
callbackStruct->dll = dll;
callbackStruct->callback = callback;
callbackStruct->called = false;
dllLoadCallbacks.push_back(callbackStruct);
}
void AddDllLoadCallbackForClient(std::string dll, DllLoadCallbackFuncType callback)
{
if (IsDedicatedServer())
return;
DllLoadCallback* callbackStruct = new DllLoadCallback;
callbackStruct->dll = dll;
callbackStruct->callback = callback;
callbackStruct->called = false;
dllLoadCallbacks.push_back(callbackStruct);
}
std::vector<std::string> calledTags;
void CallLoadLibraryACallbacks(LPCSTR lpLibFileName, HMODULE moduleAddress)
{
for (auto& callbackStruct : dllLoadCallbacks)
CModule cModule(moduleAddress);
while (true)
{
if (!callbackStruct->called &&
strstr(lpLibFileName + (strlen(lpLibFileName) - callbackStruct->dll.length()), callbackStruct->dll.c_str()) != nullptr)
bool bDoneCalling = true;
for (auto& callbackStruct : GetDllLoadCallbacks())
{
callbackStruct->callback(moduleAddress);
callbackStruct->called = true;
if (!callbackStruct.called && fs::path(lpLibFileName).filename() == fs::path(callbackStruct.dll).filename())
{
bool bShouldContinue = false;
if (!callbackStruct.reliesOn.empty())
{
for (std::string tag : callbackStruct.reliesOn)
{
if (std::find(calledTags.begin(), calledTags.end(), tag) == calledTags.end())
{
bDoneCalling = false;
bShouldContinue = true;
break;
}
}
}
if (bShouldContinue)
continue;
callbackStruct.callback(moduleAddress);
calledTags.push_back(callbackStruct.tag);
callbackStruct.called = true;
}
}
if (bDoneCalling)
break;
}
}
void CallLoadLibraryWCallbacks(LPCWSTR lpLibFileName, HMODULE moduleAddress)
{
for (auto& callbackStruct : dllLoadCallbacks)
CModule cModule(moduleAddress);
while (true)
{
std::wstring wcharStrDll = std::wstring(callbackStruct->dll.begin(), callbackStruct->dll.end());
const wchar_t* callbackDll = wcharStrDll.c_str();
if (!callbackStruct->called && wcsstr(lpLibFileName + (wcslen(lpLibFileName) - wcharStrDll.length()), callbackDll) != nullptr)
bool bDoneCalling = true;
for (auto& callbackStruct : GetDllLoadCallbacks())
{
callbackStruct->callback(moduleAddress);
callbackStruct->called = true;
if (!callbackStruct.called && fs::path(lpLibFileName).filename() == fs::path(callbackStruct.dll).filename())
{
bool bShouldContinue = false;
if (!callbackStruct.reliesOn.empty())
{
for (std::string tag : callbackStruct.reliesOn)
{
if (std::find(calledTags.begin(), calledTags.end(), tag) == calledTags.end())
{
bDoneCalling = false;
bShouldContinue = true;
break;
}
}
}
if (bShouldContinue)
continue;
callbackStruct.callback(moduleAddress);
calledTags.push_back(callbackStruct.tag);
callbackStruct.called = true;
}
}
if (bDoneCalling)
break;
}
}
@ -208,65 +349,78 @@ void CallAllPendingDLLLoadCallbacks()
}
}
HMODULE LoadLibraryExAHook(LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)
// clang-format off
AUTOHOOK_ABSOLUTEADDR(_LoadLibraryExA, LoadLibraryExA,
HMODULE, WINAPI, (LPCSTR lpLibFileName, HANDLE hFile, DWORD dwFlags))
// clang-format on
{
HMODULE moduleAddress;
// replace xinput dll with one that has ASLR
if (!strncmp(lpLibFileName, "XInput1_3.dll", 14))
{
HMODULE moduleAddress = LoadLibraryExAOriginal("XInput9_1_0.dll", hFile, dwFlags);
if (moduleAddress)
{
CallLoadLibraryACallbacks(lpLibFileName, moduleAddress);
}
else
moduleAddress = _LoadLibraryExA("XInput9_1_0.dll", hFile, dwFlags);
if (!moduleAddress)
{
MessageBoxA(0, "Could not find XInput9_1_0.dll", "Northstar", MB_ICONERROR);
exit(-1);
return nullptr;
}
return moduleAddress;
}
else
{
HMODULE moduleAddress = LoadLibraryExAOriginal(lpLibFileName, hFile, dwFlags);
if (moduleAddress)
{
CallLoadLibraryACallbacks(lpLibFileName, moduleAddress);
}
return moduleAddress;
}
}
HMODULE LoadLibraryAHook(LPCSTR lpLibFileName)
{
HMODULE moduleAddress = LoadLibraryAOriginal(lpLibFileName);
moduleAddress = _LoadLibraryExA(lpLibFileName, hFile, dwFlags);
if (moduleAddress)
{
CallLoadLibraryACallbacks(lpLibFileName, moduleAddress);
}
return moduleAddress;
}
HMODULE LoadLibraryExWHook(LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)
// clang-format off
AUTOHOOK_ABSOLUTEADDR(_LoadLibraryA, LoadLibraryA,
HMODULE, WINAPI, (LPCSTR lpLibFileName))
// clang-format on
{
HMODULE moduleAddress = LoadLibraryExWOriginal(lpLibFileName, hFile, dwFlags);
HMODULE moduleAddress = _LoadLibraryA(lpLibFileName);
if (moduleAddress)
{
CallLoadLibraryWCallbacks(lpLibFileName, moduleAddress);
}
CallLoadLibraryACallbacks(lpLibFileName, moduleAddress);
return moduleAddress;
}
HMODULE LoadLibraryWHook(LPCWSTR lpLibFileName)
// clang-format off
AUTOHOOK_ABSOLUTEADDR(_LoadLibraryExW, LoadLibraryExW,
HMODULE, WINAPI, (LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags))
// clang-format on
{
HMODULE moduleAddress = LoadLibraryWOriginal(lpLibFileName);
HMODULE moduleAddress = _LoadLibraryExW(lpLibFileName, hFile, dwFlags);
if (moduleAddress)
{
CallLoadLibraryWCallbacks(lpLibFileName, moduleAddress);
}
return moduleAddress;
}
// clang-format off
AUTOHOOK_ABSOLUTEADDR(_LoadLibraryW, LoadLibraryW,
HMODULE, WINAPI, (LPCWSTR lpLibFileName))
// clang-format on
{
HMODULE moduleAddress = _LoadLibraryW(lpLibFileName);
if (moduleAddress)
CallLoadLibraryWCallbacks(lpLibFileName, moduleAddress);
return moduleAddress;
}
void InstallInitialHooks()
{
if (MH_Initialize() != MH_OK)
spdlog::error("MH_Initialize (minhook initialization) failed");
AUTOHOOK_DISPATCH()
}

View File

@ -1,11 +1,311 @@
#pragma once
#include "memory.h"
#include <string>
#include <iostream>
void InstallInitialHooks();
typedef void (*DllLoadCallbackFuncType)(HMODULE moduleAddress);
void AddDllLoadCallback(std::string dll, DllLoadCallbackFuncType callback);
void AddDllLoadCallbackForDedicatedServer(std::string dll, DllLoadCallbackFuncType callback);
void AddDllLoadCallbackForClient(std::string dll, DllLoadCallbackFuncType callback);
typedef void (*DllLoadCallbackFuncType)(CModule moduleAddress);
void AddDllLoadCallback(std::string dll, DllLoadCallbackFuncType callback, std::string tag = "", std::vector<std::string> reliesOn = {});
void AddDllLoadCallbackForDedicatedServer(
std::string dll, DllLoadCallbackFuncType callback, std::string tag = "", std::vector<std::string> reliesOn = {});
void AddDllLoadCallbackForClient(
std::string dll, DllLoadCallbackFuncType callback, std::string tag = "", std::vector<std::string> reliesOn = {});
void CallAllPendingDLLLoadCallbacks();
// new dll load callback stuff
enum class eDllLoadCallbackSide
{
UNSIDED,
CLIENT,
DEDICATED_SERVER
};
class __dllLoadCallback
{
public:
__dllLoadCallback() = delete;
__dllLoadCallback(
eDllLoadCallbackSide side,
const std::string dllName,
DllLoadCallbackFuncType callback,
std::string uniqueStr,
std::string reliesOn);
};
#define __CONCAT3(x, y, z) x##y##z
#define CONCAT3(x, y, z) __CONCAT3(x, y, z)
#define __CONCAT2(x, y) x##y
#define CONCAT2(x, y) __CONCAT2(x, y)
#define __STR(s) #s
// adds a callback to be called when a given dll is loaded, for creating hooks and such
#define __ON_DLL_LOAD(dllName, side, uniquestr, reliesOn, args) \
void CONCAT2(__dllLoadCallback, uniquestr) args; \
namespace \
{ \
__dllLoadCallback CONCAT2(__dllLoadCallbackInstance, __LINE__)( \
side, dllName, CONCAT2(__dllLoadCallback, uniquestr), __STR(uniquestr), reliesOn); \
} \
void CONCAT2(__dllLoadCallback, uniquestr) args
#define ON_DLL_LOAD(dllName, uniquestr, args) __ON_DLL_LOAD(dllName, eDllLoadCallbackSide::UNSIDED, uniquestr, "", args)
#define ON_DLL_LOAD_RELIESON(dllName, uniquestr, reliesOn, args) \
__ON_DLL_LOAD(dllName, eDllLoadCallbackSide::UNSIDED, uniquestr, __STR(reliesOn), args)
#define ON_DLL_LOAD_CLIENT(dllName, uniquestr, args) __ON_DLL_LOAD(dllName, eDllLoadCallbackSide::CLIENT, uniquestr, "", args)
#define ON_DLL_LOAD_CLIENT_RELIESON(dllName, uniquestr, reliesOn, args) \
__ON_DLL_LOAD(dllName, eDllLoadCallbackSide::CLIENT, uniquestr, __STR(reliesOn), args)
#define ON_DLL_LOAD_DEDI(dllName, uniquestr, args) __ON_DLL_LOAD(dllName, eDllLoadCallbackSide::DEDICATED_SERVER, uniquestr, "", args)
#define ON_DLL_LOAD_DEDI_RELIESON(dllName, uniquestr, reliesOn, args) \
__ON_DLL_LOAD(dllName, eDllLoadCallbackSide::DEDICATED_SERVER, uniquestr, __STR(reliesOn), args)
// new macro hook stuff
class __autohook;
class __fileAutohook
{
public:
std::vector<__autohook*> hooks;
void Dispatch();
void DispatchForModule(const char* pModuleName);
};
// initialise autohooks for this file
#define AUTOHOOK_INIT() \
namespace \
{ \
__fileAutohook __FILEAUTOHOOK; \
}
// dispatch all autohooks in this file
#define AUTOHOOK_DISPATCH() __FILEAUTOHOOK.Dispatch();
#define AUTOHOOK_DISPATCH_MODULE(moduleName) __FILEAUTOHOOK.DispatchForModule(__STR(moduleName));
class __autohook
{
public:
enum AddressResolutionMode
{
OFFSET_STRING, // we're using a string that of the format dllname.dll + offset
ABSOLUTE_ADDR, // we're using an absolute address, we don't need to process it at all
PROCADDRESS // resolve using GetModuleHandle and GetProcAddress
};
char* pFuncName;
LPVOID pHookFunc;
LPVOID* ppOrigFunc;
// address resolution props
AddressResolutionMode iAddressResolutionMode;
char* pAddrString = nullptr; // for OFFSET_STRING
LPVOID iAbsoluteAddress = nullptr; // for ABSOLUTE_ADDR
char* pModuleName; // for PROCADDRESS
char* pProcName; // for PROCADDRESS
public:
__autohook() = delete;
__autohook(__fileAutohook* autohook, const char* funcName, LPVOID absoluteAddress, LPVOID* orig, LPVOID func)
: pHookFunc(func), ppOrigFunc(orig), iAbsoluteAddress(absoluteAddress)
{
iAddressResolutionMode = ABSOLUTE_ADDR;
const int iFuncNameStrlen = strlen(funcName) + 1;
pFuncName = new char[iFuncNameStrlen];
memcpy(pFuncName, funcName, iFuncNameStrlen);
autohook->hooks.push_back(this);
}
__autohook(__fileAutohook* autohook, const char* funcName, const char* addrString, LPVOID* orig, LPVOID func)
: pHookFunc(func), ppOrigFunc(orig)
{
iAddressResolutionMode = OFFSET_STRING;
const int iFuncNameStrlen = strlen(funcName) + 1;
pFuncName = new char[iFuncNameStrlen];
memcpy(pFuncName, funcName, iFuncNameStrlen);
const int iAddrStrlen = strlen(addrString) + 1;
pAddrString = new char[iAddrStrlen];
memcpy(pAddrString, addrString, iAddrStrlen);
autohook->hooks.push_back(this);
}
__autohook(__fileAutohook* autohook, const char* funcName, const char* moduleName, const char* procName, LPVOID* orig, LPVOID func)
: pHookFunc(func), ppOrigFunc(orig)
{
iAddressResolutionMode = PROCADDRESS;
const int iFuncNameStrlen = strlen(funcName) + 1;
pFuncName = new char[iFuncNameStrlen];
memcpy(pFuncName, funcName, iFuncNameStrlen);
const int iModuleNameStrlen = strlen(moduleName) + 1;
pModuleName = new char[iModuleNameStrlen];
memcpy(pModuleName, moduleName, iModuleNameStrlen);
const int iProcNameStrlen = strlen(procName) + 1;
pProcName = new char[iProcNameStrlen];
memcpy(pProcName, procName, iProcNameStrlen);
autohook->hooks.push_back(this);
}
~__autohook()
{
delete[] pFuncName;
if (pAddrString)
delete[] pAddrString;
if (pModuleName)
delete[] pModuleName;
if (pProcName)
delete[] pProcName;
}
void Dispatch()
{
LPVOID targetAddr = nullptr;
// determine the address of the function we're hooking
switch (iAddressResolutionMode)
{
case ABSOLUTE_ADDR:
{
targetAddr = iAbsoluteAddress;
break;
}
case OFFSET_STRING:
{
// in the format server.dll + 0xDEADBEEF
int iDllNameEnd = 0;
for (; !isspace(pAddrString[iDllNameEnd]) && pAddrString[iDllNameEnd] != '+'; iDllNameEnd++)
;
char* pModuleName = new char[iDllNameEnd + 1];
memcpy(pModuleName, pAddrString, iDllNameEnd);
pModuleName[iDllNameEnd] = '\0';
// get the module address
const HMODULE pModuleAddr = GetModuleHandleA(pModuleName);
if (!pModuleAddr)
break;
// get the offset string
uintptr_t iOffset = 0;
int iOffsetBegin = iDllNameEnd;
int iOffsetEnd = strlen(pAddrString);
// seek until we hit the start of the number offset
for (; !(pAddrString[iOffsetBegin] >= '0' && pAddrString[iOffsetBegin] <= '9') && pAddrString[iOffsetBegin]; iOffsetBegin++)
;
bool bIsHex =
pAddrString[iOffsetBegin] == '0' && (pAddrString[iOffsetBegin + 1] == 'X' || pAddrString[iOffsetBegin + 1] == 'x');
if (bIsHex)
iOffset = std::stoi(pAddrString + iOffsetBegin + 2, 0, 16);
else
iOffset = std::stoi(pAddrString + iOffsetBegin);
targetAddr = (LPVOID)((uintptr_t)pModuleAddr + iOffset);
break;
}
case PROCADDRESS:
{
targetAddr = GetProcAddress(GetModuleHandleA(pModuleName), pProcName);
break;
}
}
if (MH_CreateHook(targetAddr, pHookFunc, ppOrigFunc) == MH_OK)
{
if (MH_EnableHook(targetAddr) == MH_OK)
spdlog::info("Enabling hook {}", pFuncName);
else
spdlog::error("MH_EnableHook failed for function {}", pFuncName);
}
else
spdlog::error("MH_CreateHook failed for function {}", pFuncName);
}
};
// hook a function at a given offset from a dll to be dispatched with AUTOHOOK_DISPATCH()
#define AUTOHOOK(name, addrString, type, callingConvention, args) \
type callingConvention CONCAT2(__autohookfunc, name) args; \
namespace \
{ \
type(*callingConvention name) args; \
__autohook CONCAT2(__autohook, __LINE__)( \
&__FILEAUTOHOOK, __STR(name), __STR(addrString), (LPVOID*)&name, (LPVOID)CONCAT2(__autohookfunc, name)); \
} \
type callingConvention CONCAT2(__autohookfunc, name) args
// hook a function at a given absolute constant address to be dispatched with AUTOHOOK_DISPATCH()
#define AUTOHOOK_ABSOLUTEADDR(name, addr, type, callingConvention, args) \
type callingConvention CONCAT2(__autohookfunc, name) args; \
namespace \
{ \
type(*callingConvention name) args; \
__autohook \
CONCAT2(__autohook, __LINE__)(&__FILEAUTOHOOK, __STR(name), addr, (LPVOID*)&name, (LPVOID)CONCAT2(__autohookfunc, name)); \
} \
type callingConvention CONCAT2(__autohookfunc, name) args
// hook a function at a given module and exported function to be dispatched with AUTOHOOK_DISPATCH()
#define AUTOHOOK_PROCADDRESS(name, moduleName, procName, type, callingConvention, args) \
type callingConvention CONCAT2(__autohookfunc, name) args; \
namespace \
{ \
type(*callingConvention name) args; \
__autohook CONCAT2(__autohook, __LINE__)( \
&__FILEAUTOHOOK, __STR(name), __STR(moduleName), __STR(procName), (LPVOID*)&name, (LPVOID)CONCAT2(__autohookfunc, name)); \
} \
type callingConvention CONCAT2(__autohookfunc, name) \
args
class ManualHook
{
public:
char* pFuncName;
LPVOID pHookFunc;
LPVOID* ppOrigFunc;
public:
ManualHook() = delete;
ManualHook(const char* funcName, LPVOID func);
ManualHook(const char* funcName, LPVOID* orig, LPVOID func);
bool Dispatch(LPVOID addr, LPVOID* orig = nullptr);
};
// hook a function to be dispatched manually later
#define HOOK(varName, originalFunc, type, callingConvention, args) \
namespace \
{ \
type(*callingConvention originalFunc) args; \
} \
type callingConvention CONCAT2(__manualhookfunc, varName) args; \
ManualHook varName = ManualHook(__STR(varName), (LPVOID*)&originalFunc, (LPVOID)CONCAT2(__manualhookfunc, varName)); \
type callingConvention CONCAT2(__manualhookfunc, varName) args
#define HOOK_NOORIG(varName, type, callingConvention, args) \
type callingConvention CONCAT2(__manualhookfunc, varName) args; \
ManualHook varName = ManualHook(__STR(varName), (LPVOID)CONCAT2(__manualhookfunc, varName)); \
type callingConvention CONCAT2(__manualhookfunc, varName) \
args
void MakeHook(LPVOID pTarget, LPVOID pDetour, void* ppOriginal, const char* pFuncName = "");
#define MAKEHOOK(pTarget, pDetour, ppOriginal) MakeHook(pTarget, pDetour, ppOriginal, __STR(pDetour))

View File

@ -1,44 +0,0 @@
#include "pch.h"
#include "hookutils.h"
#include <iostream>
void HookEnabler::CreateHook(LPVOID ppTarget, LPVOID ppDetour, LPVOID* ppOriginal, const char* targetName)
{
// the macro for this uses ppTarget's name as targetName, and this typically starts with &
// targetname is used for debug stuff and debug output is nicer if we don't have this
if (*targetName == '&')
targetName++;
if (MH_CreateHook(ppTarget, ppDetour, ppOriginal) == MH_OK)
{
HookTarget* target = new HookTarget;
target->targetAddress = ppTarget;
target->targetName = (char*)targetName;
m_hookTargets.push_back(target);
}
else
{
if (targetName != nullptr)
spdlog::error("MH_CreateHook failed for function {}", targetName);
else
spdlog::error("MH_CreateHook failed for unknown function");
}
}
HookEnabler::~HookEnabler()
{
for (auto& hook : m_hookTargets)
{
if (MH_EnableHook(hook->targetAddress) != MH_OK)
{
if (hook->targetName != nullptr)
spdlog::error("MH_EnableHook failed for function {}", hook->targetName);
else
spdlog::error("MH_EnableHook failed for unknown function");
}
else
spdlog::info("Enabling hook {}", hook->targetName);
}
}

View File

@ -1,23 +0,0 @@
#pragma once
#include <vector>
// Enables all hooks created with the HookEnabler object when it goes out of scope and handles hook errors
class HookEnabler
{
private:
struct HookTarget
{
char* targetName;
LPVOID targetAddress;
};
std::vector<HookTarget*> m_hookTargets;
public:
void CreateHook(LPVOID ppTarget, LPVOID ppDetour, LPVOID* ppOriginal, const char* targetName = nullptr);
~HookEnabler();
};
// macro to call HookEnabler::CreateHook with the hook's name
#define ENABLER_CREATEHOOK(enabler, ppTarget, ppDetour, ppOriginal) \
enabler.CreateHook(ppTarget, reinterpret_cast<void*>(ppDetour), ppOriginal, #ppDetour)

37
NorthstarDLL/host.cpp Normal file
View File

@ -0,0 +1,37 @@
#include "pch.h"
#include "convar.h"
#include "modmanager.h"
#include "printcommand.h"
#include "printmaps.h"
#include "misccommands.h"
#include "r2engine.h"
#include "tier0.h"
AUTOHOOK_INIT()
// clang-format off
AUTOHOOK(Host_Init, engine.dll + 0x155EA0,
void, __fastcall, (bool bDedicated))
// clang-format on
{
spdlog::info("Host_Init()");
Host_Init(bDedicated);
FixupCvarFlags();
// need to initialise these after host_init since they do stuff to preexisting concommands/convars without being client/server specific
InitialiseCommandPrint();
InitialiseMapsPrint();
// client/server autoexecs on necessary platforms
// dedi needs autoexec_ns_server on boot, while non-dedi will run it on on listen server start
if (bDedicated)
R2::Cbuf_AddText(R2::Cbuf_GetCurrentPlayer(), "exec autoexec_ns_server", R2::cmd_source_t::kCommandSrcCode);
else
R2::Cbuf_AddText(R2::Cbuf_GetCurrentPlayer(), "exec autoexec_ns_client", R2::cmd_source_t::kCommandSrcCode);
}
ON_DLL_LOAD("engine.dll", Host_Init, (CModule module))
{
AUTOHOOK_DISPATCH()
}

116
NorthstarDLL/hoststate.cpp Normal file
View File

@ -0,0 +1,116 @@
#include "pch.h"
#include "hoststate.h"
#include "masterserver.h"
#include "serverauthentication.h"
#include "serverpresence.h"
#include "playlist.h"
#include "tier0.h"
#include "r2engine.h"
#include "limits.h"
AUTOHOOK_INIT()
using namespace R2;
// use the R2 namespace for game funcs
namespace R2
{
CHostState* g_pHostState;
} // namespace R2
ConVar* Cvar_hostport;
void ServerStartingOrChangingMap()
{
// net_data_block_enabled is required for sp, force it if we're on an sp map
// sucks for security but just how it be
if (!strncmp(g_pHostState->m_levelName, "sp_", 3))
g_pCVar->FindVar("net_data_block_enabled")->SetValue(true);
}
// clang-format off
AUTOHOOK(CHostState__State_NewGame, engine.dll + 0x16E7D0,
void, __fastcall, (CHostState* self))
// clang-format on
{
spdlog::info("HostState: NewGame");
Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec autoexec_ns_server", cmd_source_t::kCommandSrcCode);
Cbuf_Execute();
// need to do this to ensure we don't go to private match
if (g_pServerAuthentication->m_bNeedLocalAuthForNewgame)
SetCurrentPlaylist("tdm");
// don't require authentication on singleplayer startup
g_pServerAuthentication->m_bRequireClientAuth = strncmp(g_pHostState->m_levelName, "sp_", 3);
ServerStartingOrChangingMap();
double dStartTime = Tier0::Plat_FloatTime();
CHostState__State_NewGame(self);
spdlog::info("loading took {}s", Tier0::Plat_FloatTime() - dStartTime);
// setup server presence
g_pServerPresence->CreatePresence();
g_pServerPresence->SetMap(g_pHostState->m_levelName, true);
g_pServerPresence->SetPlaylist(GetCurrentPlaylistName());
g_pServerPresence->SetPort(Cvar_hostport->GetInt());
g_pServerAuthentication->StartPlayerAuthServer();
g_pServerAuthentication->m_bNeedLocalAuthForNewgame = false;
}
// clang-format off
AUTOHOOK(CHostState__State_ChangeLevelMP, engine.dll + 0x16E520,
void, __fastcall, (CHostState* self))
// clang-format on
{
spdlog::info("HostState: ChangeLevelMP");
ServerStartingOrChangingMap();
double dStartTime = Tier0::Plat_FloatTime();
CHostState__State_ChangeLevelMP(self);
spdlog::info("loading took {}s", Tier0::Plat_FloatTime() - dStartTime);
g_pServerPresence->SetMap(g_pHostState->m_levelName);
}
// clang-format off
AUTOHOOK(CHostState__State_GameShutdown, engine.dll + 0x16E640,
void, __fastcall, (CHostState* self))
// clang-format on
{
spdlog::info("HostState: GameShutdown");
g_pServerPresence->DestroyPresence();
g_pServerAuthentication->StopPlayerAuthServer();
CHostState__State_GameShutdown(self);
}
// clang-format off
AUTOHOOK(CHostState__FrameUpdate, engine.dll + 0x16DB00,
void, __fastcall, (CHostState* self, double flCurrentTime, float flFrameTime))
// clang-format on
{
CHostState__FrameUpdate(self, flCurrentTime, flFrameTime);
if (*R2::g_pServerState == R2::server_state_t::ss_active)
{
// update server presence
g_pServerPresence->RunFrame(flCurrentTime);
// update limits for frame
g_pServerLimits->RunFrame(flCurrentTime, flFrameTime);
}
}
ON_DLL_LOAD_RELIESON("engine.dll", HostState, ConVar, (CModule module))
{
AUTOHOOK_DISPATCH()
g_pHostState = module.Offset(0x7CF180).As<CHostState*>();
Cvar_hostport = module.Offset(0x13FA6070).As<ConVar*>();
}

45
NorthstarDLL/hoststate.h Normal file
View File

@ -0,0 +1,45 @@
#pragma once
// use the R2 namespace for game funxcs
namespace R2
{
enum class HostState_t
{
HS_NEW_GAME = 0,
HS_LOAD_GAME,
HS_CHANGE_LEVEL_SP,
HS_CHANGE_LEVEL_MP,
HS_RUN,
HS_GAME_SHUTDOWN,
HS_SHUTDOWN,
HS_RESTART,
};
struct CHostState
{
public:
HostState_t m_iCurrentState;
HostState_t m_iNextState;
float m_vecLocation[3];
float m_angLocation[3];
char m_levelName[32];
char m_mapGroupName[32];
char m_landmarkName[32];
char m_saveName[32];
float m_flShortFrameTime; // run a few one-tick frames to avoid large timesteps while loading assets
bool m_activeGame;
bool m_bRememberLocation;
bool m_bBackgroundLevel;
bool m_bWaitingForConnection;
bool m_bLetToolsOverrideLoadGameEnts; // During a load game, this tells Foundry to override ents that are selected in Hammer.
bool m_bSplitScreenConnect;
bool m_bGameHasShutDownAndFlushedMemory; // This is false once we load a map into memory, and set to true once the map is unloaded
// and all memory flushed
bool m_bWorkshopMapDownloadPending;
};
extern CHostState* g_pHostState;
} // namespace R2

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,45 +1,16 @@
#include "pch.h"
#include "keyvalues.h"
#include "modmanager.h"
#include "filesystem.h"
#include "hookutils.h"
#include <fstream>
// hook forward defs
typedef char (*KeyValues__LoadFromBufferType)(
void* self, const char* resourceName, const char* pBuffer, void* pFileSystem, void* a5, void* a6, int a7);
KeyValues__LoadFromBufferType KeyValues__LoadFromBuffer;
char KeyValues__LoadFromBufferHook(
void* self, const char* resourceName, const char* pBuffer, void* pFileSystem, void* a5, void* a6, int a7);
void InitialiseKeyValues(HMODULE baseAddress)
{
HookEnabler hook;
ENABLER_CREATEHOOK(
hook, (char*)baseAddress + 0x426C30, &KeyValues__LoadFromBufferHook, reinterpret_cast<LPVOID*>(&KeyValues__LoadFromBuffer));
}
void* savedFilesystemPtr;
char KeyValues__LoadFromBufferHook(void* self, const char* resourceName, const char* pBuffer, void* pFileSystem, void* a5, void* a6, int a7)
{
// this is just to allow playlists to get a valid pFileSystem ptr for kv building, other functions that call this particular overload of
// LoadFromBuffer seem to get called on network stuff exclusively not exactly sure what the address wanted here is, so just taking it
// from a function call that always happens before playlists is loaded
if (pFileSystem != nullptr)
savedFilesystemPtr = pFileSystem;
if (!pFileSystem && !strcmp(resourceName, "playlists"))
pFileSystem = savedFilesystemPtr;
return KeyValues__LoadFromBuffer(self, resourceName, pBuffer, pFileSystem, a5, a6, a7);
}
AUTOHOOK_INIT()
void ModManager::TryBuildKeyValues(const char* filename)
{
spdlog::info("Building KeyValues for file {}", filename);
std::string normalisedPath = fs::path(filename).lexically_normal().string();
std::string normalisedPath = g_pModManager->NormaliseModFilePath(fs::path(filename));
fs::path compiledPath = GetCompiledAssetsPath() / filename;
fs::path compiledDir = compiledPath.parent_path();
fs::create_directories(compiledDir);
@ -54,14 +25,14 @@ void ModManager::TryBuildKeyValues(const char* filename)
// copy over patch kv files, and add #bases to new file, last mods' patches should be applied first
// note: #include should be identical but it's actually just broken, thanks respawn
for (int64_t i = m_loadedMods.size() - 1; i > -1; i--)
for (int64_t i = m_LoadedMods.size() - 1; i > -1; i--)
{
if (!m_loadedMods[i].Enabled)
if (!m_LoadedMods[i].m_bEnabled)
continue;
size_t fileHash = STR_HASH(normalisedPath);
auto modKv = m_loadedMods[i].KeyValues.find(fileHash);
if (modKv != m_loadedMods[i].KeyValues.end())
auto modKv = m_LoadedMods[i].KeyValues.find(fileHash);
if (modKv != m_LoadedMods[i].KeyValues.end())
{
// should result in smth along the lines of #include "mod_patch_5_mp_weapon_car.txt"
@ -76,7 +47,7 @@ void ModManager::TryBuildKeyValues(const char* filename)
fs::remove(compiledDir / patchFilePath);
fs::copy_file(m_loadedMods[i].ModDirectory / "keyvalues" / filename, compiledDir / patchFilePath);
fs::copy_file(m_LoadedMods[i].m_ModDirectory / "keyvalues" / filename, compiledDir / patchFilePath);
}
}
@ -86,7 +57,7 @@ void ModManager::TryBuildKeyValues(const char* filename)
newKvs += "\"\n";
// load original file, so we can parse out the name of the root obj (e.g. WeaponData for weapons)
std::string originalFile = ReadVPKOriginalFile(filename);
std::string originalFile = R2::ReadVPKOriginalFile(filename);
if (!originalFile.length())
{
@ -96,7 +67,6 @@ void ModManager::TryBuildKeyValues(const char* filename)
char rootName[64];
memset(rootName, 0, sizeof(rootName));
rootName[63] = '\0';
// iterate until we hit an ascii char that isn't in a # command or comment to get root obj name
int i = 0;
@ -127,11 +97,36 @@ void ModManager::TryBuildKeyValues(const char* filename)
writeStream.close();
ModOverrideFile overrideFile;
overrideFile.owningMod = nullptr;
overrideFile.path = normalisedPath;
overrideFile.m_pOwningMod = nullptr;
overrideFile.m_Path = normalisedPath;
if (m_modFiles.find(normalisedPath) == m_modFiles.end())
m_modFiles.insert(std::make_pair(normalisedPath, overrideFile));
if (m_ModFiles.find(normalisedPath) == m_ModFiles.end())
m_ModFiles.insert(std::make_pair(normalisedPath, overrideFile));
else
m_modFiles[normalisedPath] = overrideFile;
m_ModFiles[normalisedPath] = overrideFile;
}
// clang-format off
AUTOHOOK(KeyValues__LoadFromBuffer, engine.dll + 0x426C30,
char, __fastcall, (void* self, const char* resourceName, const char* pBuffer, void* pFileSystem, void* a5, void* a6, int a7))
// clang-format on
{
static void* pSavedFilesystemPtr = nullptr;
// this is just to allow playlists to get a valid pFileSystem ptr for kv building, other functions that call this particular overload of
// LoadFromBuffer seem to get called on network stuff exclusively not exactly sure what the address wanted here is, so just taking it
// from a function call that always happens before playlists is loaded
// note: would be better if we could serialize this to disk for playlists, as this method breaks saving playlists in demos
if (pFileSystem != nullptr)
pSavedFilesystemPtr = pFileSystem;
if (!pFileSystem && !strcmp(resourceName, "playlists"))
pFileSystem = pSavedFilesystemPtr;
return KeyValues__LoadFromBuffer(self, resourceName, pBuffer, pFileSystem, a5, a6, a7);
}
ON_DLL_LOAD("engine.dll", KeyValues, (CModule module))
{
AUTOHOOK_DISPATCH()
}

View File

@ -1,3 +0,0 @@
#pragma once
void InitialiseKeyValues(HMODULE baseAddress);

View File

@ -1,18 +1,13 @@
#include "pch.h"
#include "languagehooks.h"
#include "gameutils.h"
#include "tier0.h"
#include <filesystem>
#include <regex>
namespace fs = std::filesystem;
typedef char* (*GetGameLanguageType)();
char* GetGameLanguage();
AUTOHOOK_INIT()
typedef LANGID (*Tier0_DetectDefaultLanguageType)();
GetGameLanguageType GetGameLanguageOriginal;
bool CheckLangAudioExists(char* lang)
{
std::string path {"r2\\sound\\general_"};
@ -52,7 +47,10 @@ std::string GetAnyInstalledAudioLanguage()
return "NO LANGUAGE DETECTED";
}
char* GetGameLanguageHook()
// 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");
@ -60,7 +58,7 @@ char* GetGameLanguageHook()
bool& canOriginDictateLang = *(bool*)((char*)tier0Handle + 0xA9A90);
const char* forcedLanguage;
if (CommandLine()->CheckParm("-language", &forcedLanguage))
if (Tier0::CommandLine()->CheckParm("-language", &forcedLanguage))
{
if (!CheckLangAudioExists((char*)forcedLanguage))
{
@ -79,7 +77,7 @@ char* GetGameLanguageHook()
canOriginDictateLang = true; // let it try
{
auto lang = GetGameLanguageOriginal();
auto lang = GetGameLanguage();
if (!CheckLangAudioExists(lang))
{
if (strcmp(lang, "russian") !=
@ -97,7 +95,7 @@ char* GetGameLanguageHook()
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 = GetGameLanguageOriginal();
auto lang = GetGameLanguage();
spdlog::info("Detected system language: {}", lang);
if (!CheckLangAudioExists(lang))
{
@ -112,8 +110,7 @@ char* GetGameLanguageHook()
return lang;
}
void InitialiseTier0LanguageHooks(HMODULE baseAddress)
ON_DLL_LOAD_CLIENT("tier0.dll", LanguageHooks, (CModule module))
{
HookEnabler hook;
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0xF560, &GetGameLanguageHook, reinterpret_cast<LPVOID*>(&GetGameLanguageOriginal));
AUTOHOOK_DISPATCH()
}

View File

@ -1,3 +0,0 @@
#pragma once
void InitialiseTier0LanguageHooks(HMODULE baseAddress);

View File

@ -1,76 +1,44 @@
#include "pch.h"
#include "latencyflex.h"
#include "hookutils.h"
#include "convar.h"
typedef void (*OnRenderStartType)();
OnRenderStartType OnRenderStart;
AUTOHOOK_INIT()
ConVar* Cvar_r_latencyflex;
HMODULE m_lfxModule {};
typedef void (*PFN_lfx_WaitAndBeginFrame)();
PFN_lfx_WaitAndBeginFrame m_lfx_WaitAndBeginFrame {};
void (*m_winelfx_WaitAndBeginFrame)();
void OnRenderStartHook()
// clang-format off
AUTOHOOK(OnRenderStart, client.dll + 0x1952C0,
void, __fastcall, ())
// clang-format on
{
// Sleep before next frame as needed to reduce latency.
if (Cvar_r_latencyflex->GetInt())
{
if (m_lfx_WaitAndBeginFrame)
{
m_lfx_WaitAndBeginFrame();
}
}
if (Cvar_r_latencyflex->GetBool() && m_winelfx_WaitAndBeginFrame)
m_winelfx_WaitAndBeginFrame();
OnRenderStart();
}
void InitialiseLatencyFleX(HMODULE baseAddress)
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/
const auto lfxModuleName = "latencyflex_layer.dll";
const auto lfxModuleNameFallback = "latencyflex_wine.dll";
auto useFallbackEntrypoints = false;
HMODULE pLfxModule;
// Load LatencyFleX library.
m_lfxModule = ::LoadLibraryA(lfxModuleName);
if (m_lfxModule == nullptr && ::GetLastError() == ERROR_MOD_NOT_FOUND)
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("LFX: Primary LatencyFleX library not found, trying fallback.");
m_lfxModule = ::LoadLibraryA(lfxModuleNameFallback);
if (m_lfxModule == nullptr)
{
if (::GetLastError() == ERROR_MOD_NOT_FOUND)
{
spdlog::info("LFX: Fallback LatencyFleX library not found.");
}
else
{
spdlog::info("LFX: Error loading fallback LatencyFleX library - Code: {}", ::GetLastError());
}
return;
}
useFallbackEntrypoints = true;
}
else if (m_lfxModule == nullptr)
{
spdlog::info("LFX: Error loading primary LatencyFleX library - Code: {}", ::GetLastError());
spdlog::info("Unable to load LatencyFleX library, LatencyFleX disabled.");
return;
}
m_lfx_WaitAndBeginFrame = reinterpret_cast<PFN_lfx_WaitAndBeginFrame>(reinterpret_cast<void*>(
GetProcAddress(m_lfxModule, !useFallbackEntrypoints ? "lfx_WaitAndBeginFrame" : "winelfx_WaitAndBeginFrame")));
spdlog::info("LFX: Initialized.");
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.");
HookEnabler hook;
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x1952C0, &OnRenderStartHook, reinterpret_cast<LPVOID*>(&OnRenderStart));
}

View File

@ -1,2 +0,0 @@
#pragma once
void InitialiseLatencyFleX(HMODULE baseAddress);

299
NorthstarDLL/limits.cpp Normal file
View File

@ -0,0 +1,299 @@
#include "pch.h"
#include "limits.h"
#include "hoststate.h"
#include "r2client.h"
#include "r2engine.h"
#include "r2server.h"
#include "maxplayers.h"
#include "tier0.h"
#include "vector.h"
#include "serverauthentication.h"
AUTOHOOK_INIT()
ServerLimitsManager* g_pServerLimits;
ConVar* Cvar_net_datablock_enabled;
// todo: make this work on higher timescales, also possibly disable when sv_cheats is set
void ServerLimitsManager::RunFrame(double flCurrentTime, float flFrameTime)
{
if (Cvar_sv_antispeedhack_enable->GetBool())
{
// for each player, set their usercmd processing budget for the frame to the last frametime for the server
for (int i = 0; i < R2::GetMaxPlayers(); i++)
{
R2::CBaseClient* player = &R2::g_pClientArray[i];
if (m_PlayerLimitData.find(player) != m_PlayerLimitData.end())
{
PlayerLimitData* pLimitData = &g_pServerLimits->m_PlayerLimitData[player];
if (pLimitData->flFrameUserCmdBudget < 0.016666667 * Cvar_sv_antispeedhack_maxtickbudget->GetFloat())
pLimitData->flFrameUserCmdBudget +=
fmax(flFrameTime, 0.016666667) * g_pServerLimits->Cvar_sv_antispeedhack_budgetincreasemultiplier->GetFloat();
}
}
}
}
void ServerLimitsManager::AddPlayer(R2::CBaseClient* player)
{
PlayerLimitData limitData;
limitData.flFrameUserCmdBudget = 0.016666667 * Cvar_sv_antispeedhack_maxtickbudget->GetFloat();
m_PlayerLimitData.insert(std::make_pair(player, limitData));
}
void ServerLimitsManager::RemovePlayer(R2::CBaseClient* player)
{
if (m_PlayerLimitData.find(player) != m_PlayerLimitData.end())
m_PlayerLimitData.erase(player);
}
bool ServerLimitsManager::CheckStringCommandLimits(R2::CBaseClient* player)
{
if (CVar_sv_quota_stringcmdspersecond->GetInt() != -1)
{
// note: this isn't super perfect, legit clients can trigger it in lobby if they try, mostly good enough tho imo
if (Tier0::Plat_FloatTime() - m_PlayerLimitData[player].lastClientCommandQuotaStart >= 1.0)
{
// reset quota
m_PlayerLimitData[player].lastClientCommandQuotaStart = Tier0::Plat_FloatTime();
m_PlayerLimitData[player].numClientCommandsInQuota = 0;
}
m_PlayerLimitData[player].numClientCommandsInQuota++;
if (m_PlayerLimitData[player].numClientCommandsInQuota > CVar_sv_quota_stringcmdspersecond->GetInt())
{
// too many stringcmds, dc player
return false;
}
}
return true;
}
bool ServerLimitsManager::CheckChatLimits(R2::CBaseClient* player)
{
if (Tier0::Plat_FloatTime() - m_PlayerLimitData[player].lastSayTextLimitStart >= 1.0)
{
m_PlayerLimitData[player].lastSayTextLimitStart = Tier0::Plat_FloatTime();
m_PlayerLimitData[player].sayTextLimitCount = 0;
}
if (m_PlayerLimitData[player].sayTextLimitCount >= Cvar_sv_max_chat_messages_per_sec->GetInt())
return false;
m_PlayerLimitData[player].sayTextLimitCount++;
return true;
}
// clang-format off
AUTOHOOK(CNetChan__ProcessMessages, engine.dll + 0x2140A0,
char, __fastcall, (void* self, void* buf))
// clang-format on
{
enum eNetChanLimitMode
{
NETCHANLIMIT_WARN,
NETCHANLIMIT_KICK
};
double startTime = Tier0::Plat_FloatTime();
char ret = CNetChan__ProcessMessages(self, buf);
// check processing limits, unless we're in a level transition
if (R2::g_pHostState->m_iCurrentState == R2::HostState_t::HS_RUN && Tier0::ThreadInServerFrameThread())
{
// player that sent the message
R2::CBaseClient* sender = *(R2::CBaseClient**)((char*)self + 368);
// if no sender, return
// relatively certain this is fine?
if (!sender || !g_pServerLimits->m_PlayerLimitData.count(sender))
return ret;
// reset every second
if (startTime - g_pServerLimits->m_PlayerLimitData[sender].lastNetChanProcessingLimitStart >= 1.0 ||
g_pServerLimits->m_PlayerLimitData[sender].lastNetChanProcessingLimitStart == -1.0)
{
g_pServerLimits->m_PlayerLimitData[sender].lastNetChanProcessingLimitStart = startTime;
g_pServerLimits->m_PlayerLimitData[sender].netChanProcessingLimitTime = 0.0;
}
g_pServerLimits->m_PlayerLimitData[sender].netChanProcessingLimitTime += (Tier0::Plat_FloatTime() * 1000) - (startTime * 1000);
if (g_pServerLimits->m_PlayerLimitData[sender].netChanProcessingLimitTime >=
g_pServerLimits->Cvar_net_chan_limit_msec_per_sec->GetInt())
{
spdlog::warn(
"Client {} hit netchan processing limit with {}ms of processing time this second (max is {})",
(char*)sender + 0x16,
g_pServerLimits->m_PlayerLimitData[sender].netChanProcessingLimitTime,
g_pServerLimits->Cvar_net_chan_limit_msec_per_sec->GetInt());
// never kick local player
if (g_pServerLimits->Cvar_net_chan_limit_mode->GetInt() != NETCHANLIMIT_WARN && strcmp(R2::g_pLocalPlayerUserID, sender->m_UID))
{
R2::CBaseClient__Disconnect(sender, 1, "Exceeded net channel processing limit");
return false;
}
}
}
return ret;
}
// clang-format off
AUTOHOOK(ProcessConnectionlessPacket, engine.dll + 0x117800,
bool, , (void* a1, R2::netpacket_t* packet))
// clang-format on
{
if (packet->adr.type == R2::NA_IP &&
(!(packet->data[4] == 'N' && Cvar_net_datablock_enabled->GetBool()) || !Cvar_net_datablock_enabled->GetBool()))
{
// bad lookup: optimise later tm
UnconnectedPlayerLimitData* sendData = nullptr;
for (UnconnectedPlayerLimitData& foundSendData : g_pServerLimits->m_UnconnectedPlayerLimitData)
{
if (!memcmp(packet->adr.ip, foundSendData.ip, 16))
{
sendData = &foundSendData;
break;
}
}
if (!sendData)
{
sendData = &g_pServerLimits->m_UnconnectedPlayerLimitData.emplace_back();
memcpy(sendData->ip, packet->adr.ip, 16);
}
if (Tier0::Plat_FloatTime() < sendData->timeoutEnd)
return false;
if (Tier0::Plat_FloatTime() - sendData->lastQuotaStart >= 1.0)
{
sendData->lastQuotaStart = Tier0::Plat_FloatTime();
sendData->packetCount = 0;
}
sendData->packetCount++;
if (sendData->packetCount >= g_pServerLimits->Cvar_sv_querylimit_per_sec->GetInt())
{
spdlog::warn(
"Client went over connectionless ratelimit of {} per sec with packet of type {}",
g_pServerLimits->Cvar_sv_querylimit_per_sec->GetInt(),
packet->data[4]);
// timeout for a minute
sendData->timeoutEnd = Tier0::Plat_FloatTime() + 60.0;
return false;
}
}
return ProcessConnectionlessPacket(a1, packet);
}
// this is weird and i'm not sure if it's correct, so not using for now
/*AUTOHOOK(CBasePlayer__PhysicsSimulate, server.dll + 0x5A6E50, bool, __fastcall, (void* self, int a2, char a3))
{
spdlog::info("CBasePlayer::PhysicsSimulate");
return CBasePlayer__PhysicsSimulate(self, a2, a3);
}*/
struct alignas(4) SV_CUserCmd
{
DWORD command_number;
DWORD tick_count;
float command_time;
Vector3 worldViewAngles;
BYTE gap18[4];
Vector3 localViewAngles;
Vector3 attackangles;
Vector3 move;
DWORD buttons;
BYTE impulse;
short weaponselect;
DWORD meleetarget;
BYTE gap4C[24];
char headoffset;
BYTE gap65[11];
Vector3 cameraPos;
Vector3 cameraAngles;
BYTE gap88[4];
int tickSomething;
DWORD dword90;
DWORD predictedServerEventAck;
DWORD dword98;
float frameTime;
};
// clang-format off
AUTOHOOK(CPlayerMove__RunCommand, server.dll + 0x5B8100,
void, __fastcall, (void* self, R2::CBasePlayer* player, SV_CUserCmd* pUserCmd, uint64_t a4))
// clang-format on
{
if (g_pServerLimits->Cvar_sv_antispeedhack_enable->GetBool())
{
R2::CBaseClient* pClient = &R2::g_pClientArray[player->m_nPlayerIndex - 1];
if (g_pServerLimits->m_PlayerLimitData.find(pClient) != g_pServerLimits->m_PlayerLimitData.end())
{
PlayerLimitData* pLimitData = &g_pServerLimits->m_PlayerLimitData[pClient];
pLimitData->flFrameUserCmdBudget = fmax(0.0, pLimitData->flFrameUserCmdBudget - pUserCmd->frameTime);
if (pLimitData->flFrameUserCmdBudget <= 0.0)
{
spdlog::warn("player {} went over usercmd budget ({})", pClient->m_Name, pLimitData->flFrameUserCmdBudget);
return;
}
// else
// spdlog::info("{}: {}", pClient->m_Name, pLimitData->flFrameUserCmdBudget);
}
}
CPlayerMove__RunCommand(self, player, pUserCmd, a4);
}
ON_DLL_LOAD_RELIESON("engine.dll", ServerLimits, ConVar, (CModule module))
{
AUTOHOOK_DISPATCH_MODULE(engine.dll)
g_pServerLimits = new ServerLimitsManager;
g_pServerLimits->CVar_sv_quota_stringcmdspersecond = new ConVar(
"sv_quota_stringcmdspersecond",
"60",
FCVAR_GAMEDLL,
"How many string commands per second clients are allowed to submit, 0 to disallow all string commands, -1 to disable");
g_pServerLimits->Cvar_net_chan_limit_mode =
new ConVar("net_chan_limit_mode", "0", FCVAR_GAMEDLL, "The mode for netchan processing limits: 0 = warn, 1 = kick");
g_pServerLimits->Cvar_net_chan_limit_msec_per_sec = new ConVar(
"net_chan_limit_msec_per_sec",
"100",
FCVAR_GAMEDLL,
"Netchannel processing is limited to so many milliseconds, abort connection if exceeding budget");
g_pServerLimits->Cvar_sv_querylimit_per_sec = new ConVar("sv_querylimit_per_sec", "15", FCVAR_GAMEDLL, "");
g_pServerLimits->Cvar_sv_max_chat_messages_per_sec = new ConVar("sv_max_chat_messages_per_sec", "5", FCVAR_GAMEDLL, "");
g_pServerLimits->Cvar_sv_antispeedhack_enable =
new ConVar("sv_antispeedhack_enable", "0", FCVAR_NONE, "whether to enable antispeedhack protections");
g_pServerLimits->Cvar_sv_antispeedhack_maxtickbudget = new ConVar(
"sv_antispeedhack_maxtickbudget",
"64",
FCVAR_GAMEDLL,
"Maximum number of client-issued usercmd ticks that can be replayed in packet loss conditions, 0 to allow no restrictions");
g_pServerLimits->Cvar_sv_antispeedhack_budgetincreasemultiplier = new ConVar(
"sv_antispeedhack_budgetincreasemultiplier",
"1.2",
FCVAR_GAMEDLL,
"Increase usercmd processing budget by tickinterval * value per tick");
Cvar_net_datablock_enabled = R2::g_pCVar->FindVar("net_datablock_enabled");
}
ON_DLL_LOAD("server.dll", ServerLimitsServer, (CModule module))
{
AUTOHOOK_DISPATCH_MODULE(server.dll)
}

51
NorthstarDLL/limits.h Normal file
View File

@ -0,0 +1,51 @@
#pragma once
#include "r2engine.h"
#include "convar.h"
#include <unordered_map>
struct PlayerLimitData
{
double lastClientCommandQuotaStart = -1.0;
int numClientCommandsInQuota = 0;
double lastNetChanProcessingLimitStart = -1.0;
double netChanProcessingLimitTime = 0.0;
double lastSayTextLimitStart = -1.0;
int sayTextLimitCount = 0;
float flFrameUserCmdBudget = 0.0;
};
struct UnconnectedPlayerLimitData
{
char ip[16];
double lastQuotaStart = 0.0;
int packetCount = 0;
double timeoutEnd = -1.0;
};
class ServerLimitsManager
{
public:
ConVar* CVar_sv_quota_stringcmdspersecond;
ConVar* Cvar_net_chan_limit_mode;
ConVar* Cvar_net_chan_limit_msec_per_sec;
ConVar* Cvar_sv_querylimit_per_sec;
ConVar* Cvar_sv_max_chat_messages_per_sec;
ConVar* Cvar_sv_antispeedhack_enable;
ConVar* Cvar_sv_antispeedhack_maxtickbudget;
ConVar* Cvar_sv_antispeedhack_budgetincreasemultiplier;
std::unordered_map<R2::CBaseClient*, PlayerLimitData> m_PlayerLimitData;
std::vector<UnconnectedPlayerLimitData> m_UnconnectedPlayerLimitData;
public:
void RunFrame(double flCurrentTime, float flFrameTime);
void AddPlayer(R2::CBaseClient* player);
void RemovePlayer(R2::CBaseClient* player);
bool CheckStringCommandLimits(R2::CBaseClient* player);
bool CheckChatLimits(R2::CBaseClient* player);
};
extern ServerLimitsManager* g_pServerLimits;

View File

@ -37,12 +37,11 @@ class vgui_BaseRichText_vtable
void(__fastcall* SetVerticalScrollbar)(vgui_BaseRichText* self, bool state);
void(__fastcall* SetMaximumCharCount)(vgui_BaseRichText* self, int maxChars);
void(__fastcall* InsertColorChange)(vgui_BaseRichText* self, vgui_Color col);
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, vgui_Color URLTextColor, vgui_Color normalTextColor);
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);
@ -81,25 +80,25 @@ LocalChatWriter::SwatchColor swatchColors[4] = {
LocalChatWriter::NetworkNameColor,
};
vgui_Color darkColors[8] = {
vgui_Color {0, 0, 0, 255},
vgui_Color {205, 49, 49, 255},
vgui_Color {13, 188, 121, 255},
vgui_Color {229, 229, 16, 255},
vgui_Color {36, 114, 200, 255},
vgui_Color {188, 63, 188, 255},
vgui_Color {17, 168, 205, 255},
vgui_Color {229, 229, 229, 255}};
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}};
vgui_Color lightColors[8] = {
vgui_Color {102, 102, 102, 255},
vgui_Color {241, 76, 76, 255},
vgui_Color {35, 209, 139, 255},
vgui_Color {245, 245, 67, 255},
vgui_Color {59, 142, 234, 255},
vgui_Color {214, 112, 214, 255},
vgui_Color {41, 184, 219, 255},
vgui_Color {255, 255, 255, 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
{
@ -144,7 +143,7 @@ class AnsiEscapeParser
LocalChatWriter* m_writer;
Next m_next = Next::ControlType;
vgui_Color m_expandedColor {0, 0, 0, 0};
Color m_expandedColor {0, 0, 0, 0};
Next HandleControlType(unsigned long val)
{
@ -190,7 +189,7 @@ class AnsiEscapeParser
// Next values are r,g,b
if (val == 2)
{
m_expandedColor = {0, 0, 0, 255};
m_expandedColor.SetColor(0, 0, 0, 255);
return Next::ForegroundR;
}
// Next value is 8-bit swatch color
@ -219,13 +218,12 @@ class AnsiEscapeParser
unsigned char blue = code % 6;
unsigned char green = ((code - blue) / 6) % 6;
unsigned char red = (code - blue - (green * 6)) / 36;
m_writer->InsertColorChange(
vgui_Color {(unsigned char)(red * 51), (unsigned char)(green * 51), (unsigned char)(blue * 51), 255});
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(vgui_Color {brightness, brightness, brightness, 255});
m_writer->InsertColorChange(Color {brightness, brightness, brightness, 255});
}
return Next::ControlType;
@ -236,7 +234,7 @@ class AnsiEscapeParser
if (val >= UCHAR_MAX)
return Next::ControlType;
m_expandedColor.r = (unsigned char)val;
m_expandedColor[0] = (unsigned char)val;
return Next::ForegroundG;
}
@ -245,7 +243,7 @@ class AnsiEscapeParser
if (val >= UCHAR_MAX)
return Next::ControlType;
m_expandedColor.g = (unsigned char)val;
m_expandedColor[1] = (unsigned char)val;
return Next::ForegroundB;
}
@ -254,7 +252,7 @@ class AnsiEscapeParser
if (val >= UCHAR_MAX)
return Next::ControlType;
m_expandedColor.b = (unsigned char)val;
m_expandedColor[2] = (unsigned char)val;
m_writer->InsertColorChange(m_expandedColor);
return Next::ControlType;
}
@ -280,12 +278,11 @@ void LocalChatWriter::Write(const char* str)
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(writeBuffer, str, copyChars);
writeBuffer[copyChars] = 0;
strncpy_s(writeBuffer, copyChars + 1, str, copyChars);
InsertText(writeBuffer);
}
@ -320,6 +317,8 @@ void LocalChatWriter::InsertChar(wchar_t ch)
void LocalChatWriter::InsertText(const char* str)
{
spdlog::info(str);
WCHAR messageUnicode[288];
ConvertANSIToUnicode(str, -1, messageUnicode, 274);
@ -347,7 +346,7 @@ void LocalChatWriter::InsertText(const wchar_t* str)
InsertDefaultFade();
}
void LocalChatWriter::InsertColorChange(vgui_Color color)
void LocalChatWriter::InsertColorChange(Color color)
{
for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next)
{
@ -358,20 +357,24 @@ void LocalChatWriter::InsertColorChange(vgui_Color color)
}
}
static vgui_Color GetHudSwatchColor(CHudChat* hud, LocalChatWriter::SwatchColor swatchColor)
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 vgui_Color {0, 0, 0, 0};
return Color(0, 0, 0, 0);
}
void LocalChatWriter::InsertSwatchColorChange(SwatchColor swatchColor)
@ -436,12 +439,12 @@ void LocalChatWriter::InsertDefaultFade()
}
}
void InitialiseLocalChatWriter(HMODULE baseAddress)
ON_DLL_LOAD_CLIENT("client.dll", LocalChatWriter, (CModule module))
{
gGameSettings = (CGameSettings**)((char*)baseAddress + 0x11BAA48);
gChatFadeLength = (CGameFloatVar**)((char*)baseAddress + 0x11BAB78);
gChatFadeSustain = (CGameFloatVar**)((char*)baseAddress + 0x11BAC08);
CHudChat::allHuds = (CHudChat**)((char*)baseAddress + 0x11BA9E8);
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 = (ConvertANSIToUnicodeType)((char*)baseAddress + 0x7339A0);
ConvertANSIToUnicode = module.Offset(0x7339A0).As<ConvertANSIToUnicodeType>();
}

View File

@ -1,13 +1,6 @@
#pragma once
#include "pch.h"
struct vgui_Color
{
unsigned char r;
unsigned char g;
unsigned char b;
unsigned char a;
};
#include "color.h"
class vgui_BaseRichText;
@ -18,10 +11,10 @@ class CHudChat
char unknown1[720];
vgui_Color m_sameTeamColor;
vgui_Color m_enemyTeamColor;
vgui_Color m_mainTextColor;
vgui_Color m_networkNameColor;
Color m_sameTeamColor;
Color m_enemyTeamColor;
Color m_mainTextColor;
Color m_networkNameColor;
char unknown2[12];
@ -61,7 +54,7 @@ class LocalChatWriter
void InsertChar(wchar_t ch);
void InsertText(const char* str);
void InsertText(const wchar_t* str);
void InsertColorChange(vgui_Color color);
void InsertColorChange(Color color);
void InsertSwatchColorChange(SwatchColor color);
private:
@ -70,5 +63,3 @@ class LocalChatWriter
const char* ApplyAnsiEscape(const char* escape);
void InsertDefaultFade();
};
void InitialiseLocalChatWriter(HMODULE baseAddress);

View File

@ -1,258 +1,20 @@
#include "pch.h"
#include "logging.h"
#include "sourceconsole.h"
#include "spdlog/sinks/basic_file_sink.h"
#include "hookutils.h"
#include "dedicated.h"
#include "convar.h"
#include "concommand.h"
#include "nsprefix.h"
#include "bitbuf.h"
#include "tier0.h"
#include "spdlog/sinks/basic_file_sink.h"
#include <iomanip>
#include <sstream>
#include "nsprefix.h"
#include <dbghelp.h>
// This needs to be called after hooks are loaded so we can access the command line args
void CreateLogFiles()
{
if (strstr(GetCommandLineA(), "-disablelogs"))
{
spdlog::default_logger()->set_level(spdlog::level::off);
}
else
{
try
{
// todo: might be good to delete logs that are too old
time_t time = std::time(nullptr);
tm currentTime = *std::localtime(&time);
std::stringstream stream;
stream << std::put_time(&currentTime, (GetNorthstarPrefix() + "/logs/nslog%Y-%m-%d %H-%M-%S.txt").c_str());
spdlog::default_logger()->sinks().push_back(std::make_shared<spdlog::sinks::basic_file_sink_mt>(stream.str(), false));
spdlog::flush_on(spdlog::level::info);
}
catch (...)
{
spdlog::error("Failed creating log file");
MessageBoxA(
0, "Failed creating log file! Make sure the profile directory is writable.", "Northstar Warning", MB_ICONWARNING | MB_OK);
}
}
}
long __stdcall ExceptionFilter(EXCEPTION_POINTERS* exceptionInfo)
{
static bool logged = false;
if (logged)
return EXCEPTION_CONTINUE_SEARCH;
if (!IsDebuggerPresent())
{
const DWORD exceptionCode = exceptionInfo->ExceptionRecord->ExceptionCode;
if (exceptionCode != EXCEPTION_ACCESS_VIOLATION && exceptionCode != EXCEPTION_ARRAY_BOUNDS_EXCEEDED &&
exceptionCode != EXCEPTION_DATATYPE_MISALIGNMENT && exceptionCode != EXCEPTION_FLT_DENORMAL_OPERAND &&
exceptionCode != EXCEPTION_FLT_DIVIDE_BY_ZERO && exceptionCode != EXCEPTION_FLT_INEXACT_RESULT &&
exceptionCode != EXCEPTION_FLT_INVALID_OPERATION && exceptionCode != EXCEPTION_FLT_OVERFLOW &&
exceptionCode != EXCEPTION_FLT_STACK_CHECK && exceptionCode != EXCEPTION_FLT_UNDERFLOW &&
exceptionCode != EXCEPTION_ILLEGAL_INSTRUCTION && exceptionCode != EXCEPTION_IN_PAGE_ERROR &&
exceptionCode != EXCEPTION_INT_DIVIDE_BY_ZERO && exceptionCode != EXCEPTION_INT_OVERFLOW &&
exceptionCode != EXCEPTION_INVALID_DISPOSITION && exceptionCode != EXCEPTION_NONCONTINUABLE_EXCEPTION &&
exceptionCode != EXCEPTION_PRIV_INSTRUCTION && exceptionCode != EXCEPTION_STACK_OVERFLOW)
return EXCEPTION_CONTINUE_SEARCH;
std::stringstream exceptionCause;
exceptionCause << "Cause: ";
switch (exceptionCode)
{
case EXCEPTION_ACCESS_VIOLATION:
case EXCEPTION_IN_PAGE_ERROR:
{
exceptionCause << "Access Violation" << std::endl;
auto exceptionInfo0 = exceptionInfo->ExceptionRecord->ExceptionInformation[0];
auto exceptionInfo1 = exceptionInfo->ExceptionRecord->ExceptionInformation[1];
if (!exceptionInfo0)
exceptionCause << "Attempted to read from: 0x" << (void*)exceptionInfo1;
else if (exceptionInfo0 == 1)
exceptionCause << "Attempted to write to: 0x" << (void*)exceptionInfo1;
else if (exceptionInfo0 == 8)
exceptionCause << "Data Execution Prevention (DEP) at: 0x" << (void*)std::hex << exceptionInfo1;
else
exceptionCause << "Unknown access violation at: 0x" << (void*)exceptionInfo1;
break;
}
case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
exceptionCause << "Array bounds exceeded";
break;
case EXCEPTION_DATATYPE_MISALIGNMENT:
exceptionCause << "Datatype misalignment";
break;
case EXCEPTION_FLT_DENORMAL_OPERAND:
exceptionCause << "Denormal operand";
break;
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
exceptionCause << "Divide by zero (float)";
break;
case EXCEPTION_INT_DIVIDE_BY_ZERO:
exceptionCause << "Divide by zero (int)";
break;
case EXCEPTION_FLT_INEXACT_RESULT:
exceptionCause << "Inexact result";
break;
case EXCEPTION_FLT_INVALID_OPERATION:
exceptionCause << "Invalid operation";
break;
case EXCEPTION_FLT_OVERFLOW:
case EXCEPTION_INT_OVERFLOW:
exceptionCause << "Numeric overflow";
break;
case EXCEPTION_FLT_UNDERFLOW:
exceptionCause << "Numeric underflow";
break;
case EXCEPTION_FLT_STACK_CHECK:
exceptionCause << "Stack check";
break;
case EXCEPTION_ILLEGAL_INSTRUCTION:
exceptionCause << "Illegal instruction";
break;
case EXCEPTION_INVALID_DISPOSITION:
exceptionCause << "Invalid disposition";
break;
case EXCEPTION_NONCONTINUABLE_EXCEPTION:
exceptionCause << "Noncontinuable exception";
break;
case EXCEPTION_PRIV_INSTRUCTION:
exceptionCause << "Priviledged instruction";
break;
case EXCEPTION_STACK_OVERFLOW:
exceptionCause << "Stack overflow";
break;
default:
exceptionCause << "Unknown";
break;
}
void* exceptionAddress = exceptionInfo->ExceptionRecord->ExceptionAddress;
HMODULE crashedModuleHandle;
GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, static_cast<LPCSTR>(exceptionAddress), &crashedModuleHandle);
MODULEINFO crashedModuleInfo;
GetModuleInformation(GetCurrentProcess(), crashedModuleHandle, &crashedModuleInfo, sizeof(crashedModuleInfo));
char crashedModuleFullName[MAX_PATH];
GetModuleFileNameExA(GetCurrentProcess(), crashedModuleHandle, crashedModuleFullName, MAX_PATH);
char* crashedModuleName = strrchr(crashedModuleFullName, '\\') + 1;
DWORD64 crashedModuleOffset = ((DWORD64)exceptionAddress) - ((DWORD64)crashedModuleInfo.lpBaseOfDll);
CONTEXT* exceptionContext = exceptionInfo->ContextRecord;
spdlog::error("Northstar has crashed! a minidump has been written and exception info is available below:");
spdlog::error(exceptionCause.str());
spdlog::error("At: {} + {}", crashedModuleName, (void*)crashedModuleOffset);
PVOID framesToCapture[62];
int frames = RtlCaptureStackBackTrace(0, 62, framesToCapture, NULL);
for (int i = 0; i < frames; i++)
{
HMODULE backtraceModuleHandle;
GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, static_cast<LPCSTR>(framesToCapture[i]), &backtraceModuleHandle);
char backtraceModuleFullName[MAX_PATH];
GetModuleFileNameExA(GetCurrentProcess(), backtraceModuleHandle, backtraceModuleFullName, MAX_PATH);
char* backtraceModuleName = strrchr(backtraceModuleFullName, '\\') + 1;
void* actualAddress = (void*)framesToCapture[i];
void* relativeAddress = (void*)(uintptr_t(actualAddress) - uintptr_t(backtraceModuleHandle));
spdlog::error(" {} + {} ({})", backtraceModuleName, relativeAddress, actualAddress);
}
spdlog::error("RAX: 0x{0:x}", exceptionContext->Rax);
spdlog::error("RBX: 0x{0:x}", exceptionContext->Rbx);
spdlog::error("RCX: 0x{0:x}", exceptionContext->Rcx);
spdlog::error("RDX: 0x{0:x}", exceptionContext->Rdx);
spdlog::error("RSI: 0x{0:x}", exceptionContext->Rsi);
spdlog::error("RDI: 0x{0:x}", exceptionContext->Rdi);
spdlog::error("RBP: 0x{0:x}", exceptionContext->Rbp);
spdlog::error("RSP: 0x{0:x}", exceptionContext->Rsp);
spdlog::error("R8: 0x{0:x}", exceptionContext->R8);
spdlog::error("R9: 0x{0:x}", exceptionContext->R9);
spdlog::error("R10: 0x{0:x}", exceptionContext->R10);
spdlog::error("R11: 0x{0:x}", exceptionContext->R11);
spdlog::error("R12: 0x{0:x}", exceptionContext->R12);
spdlog::error("R13: 0x{0:x}", exceptionContext->R13);
spdlog::error("R14: 0x{0:x}", exceptionContext->R14);
spdlog::error("R15: 0x{0:x}", exceptionContext->R15);
time_t time = std::time(nullptr);
tm currentTime = *std::localtime(&time);
std::stringstream stream;
stream << std::put_time(&currentTime, (GetNorthstarPrefix() + "/logs/nsdump%Y-%m-%d %H-%M-%S.dmp").c_str());
auto hMinidumpFile = CreateFileA(stream.str().c_str(), GENERIC_WRITE, FILE_SHARE_READ, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
if (hMinidumpFile)
{
MINIDUMP_EXCEPTION_INFORMATION dumpExceptionInfo;
dumpExceptionInfo.ThreadId = GetCurrentThreadId();
dumpExceptionInfo.ExceptionPointers = exceptionInfo;
dumpExceptionInfo.ClientPointers = false;
MiniDumpWriteDump(
GetCurrentProcess(),
GetCurrentProcessId(),
hMinidumpFile,
MINIDUMP_TYPE(MiniDumpWithIndirectlyReferencedMemory | MiniDumpScanMemory),
&dumpExceptionInfo,
nullptr,
nullptr);
CloseHandle(hMinidumpFile);
}
else
spdlog::error("Failed to write minidump file {}!", stream.str());
if (!IsDedicatedServer())
MessageBoxA(
0, "Northstar has crashed! Crash info can be found in R2Northstar/logs", "Northstar has crashed!", MB_ICONERROR | MB_OK);
}
logged = true;
return EXCEPTION_EXECUTE_HANDLER;
}
HANDLE hExceptionFilter;
BOOL WINAPI ConsoleHandlerRoutine(DWORD eventCode)
{
switch (eventCode)
{
case CTRL_CLOSE_EVENT:
// User closed console, shut everything down
spdlog::info("Exiting due to console close...");
RemoveVectoredExceptionHandler(hExceptionFilter);
exit(EXIT_SUCCESS);
return FALSE;
}
return TRUE;
}
void InitialiseLogging()
{
hExceptionFilter = AddVectoredExceptionHandler(TRUE, ExceptionFilter);
AllocConsole();
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
spdlog::default_logger()->set_pattern("[%H:%M:%S] [%l] %v");
SetConsoleCtrlHandler(ConsoleHandlerRoutine, true);
}
AUTOHOOK_INIT()
ConVar* Cvar_spewlog_enable;
enum SpewType_t
enum class SpewType_t
{
SPEW_MESSAGE = 0,
@ -264,56 +26,24 @@ enum SpewType_t
SPEW_TYPE_COUNT
};
typedef void (*EngineSpewFuncType)();
EngineSpewFuncType EngineSpewFunc;
const std::unordered_map<SpewType_t, const char*> PrintSpewTypes = {
{SpewType_t::SPEW_MESSAGE, "SPEW_MESSAGE"},
{SpewType_t::SPEW_WARNING, "SPEW_WARNING"},
{SpewType_t::SPEW_ASSERT, "SPEW_ASSERT"},
{SpewType_t::SPEW_ERROR, "SPEW_ERROR"},
{SpewType_t::SPEW_LOG, "SPEW_LOG"}};
void EngineSpewFuncHook(void* engineServer, SpewType_t type, const char* format, va_list args)
// clang-format off
AUTOHOOK(EngineSpewFunc, engine.dll + 0x11CA80,
void, __fastcall, (void* pEngineServer, SpewType_t type, const char* format, va_list args))
// clang-format on
{
if (!Cvar_spewlog_enable->GetBool())
return;
const char* typeStr;
switch (type)
{
case SPEW_MESSAGE:
{
typeStr = "SPEW_MESSAGE";
break;
}
case SPEW_WARNING:
{
typeStr = "SPEW_WARNING";
break;
}
case SPEW_ASSERT:
{
typeStr = "SPEW_ASSERT";
break;
}
case SPEW_ERROR:
{
typeStr = "SPEW_ERROR";
break;
}
case SPEW_LOG:
{
typeStr = "SPEW_LOG";
break;
}
default:
{
typeStr = "SPEW_UNKNOWN";
break;
}
}
const char* typeStr = PrintSpewTypes.at(type);
char formatted[2048] = {0};
bool shouldFormat = true;
bool bShouldFormat = true;
// because titanfall 2 is quite possibly the worst thing to yet exist, it sometimes gives invalid specifiers which will crash
// ttf2sdk had a way to prevent them from crashing but it doesnt work in debug builds
@ -360,19 +90,17 @@ void EngineSpewFuncHook(void* engineServer, SpewType_t type, const char* format,
default:
{
shouldFormat = false;
bShouldFormat = false;
break;
}
}
}
}
if (shouldFormat)
if (bShouldFormat)
vsnprintf(formatted, sizeof(formatted), format, args);
else
{
spdlog::warn("Failed to format {} \"{}\"", typeStr, format);
}
auto endpos = strlen(formatted);
if (formatted[endpos - 1] == '\n')
@ -381,10 +109,11 @@ void EngineSpewFuncHook(void* engineServer, SpewType_t type, const char* format,
spdlog::info("[SERVER {}] {}", typeStr, formatted);
}
typedef void (*Status_ConMsg_Type)(const char* text, ...);
Status_ConMsg_Type Status_ConMsg_Original;
void Status_ConMsg_Hook(const char* text, ...)
// used for printing the output of status
// clang-format off
AUTOHOOK(Status_ConMsg, engine.dll + 0x15ABD0,
void,, (const char* text, ...))
// clang-format on
{
char formatted[2048];
va_list list;
@ -400,10 +129,10 @@ void Status_ConMsg_Hook(const char* text, ...)
spdlog::info(formatted);
}
typedef bool (*CClientState_ProcessPrint_Type)(__int64 thisptr, __int64 msg);
CClientState_ProcessPrint_Type CClientState_ProcessPrint_Original;
bool CClientState_ProcessPrint_Hook(__int64 thisptr, __int64 msg)
// clang-format off
AUTOHOOK(CClientState_ProcessPrint, engine.dll + 0x1A1530,
bool,, (void* thisptr, uintptr_t msg))
// clang-format on
{
char* text = *(char**)(msg + 0x20);
@ -415,32 +144,8 @@ bool CClientState_ProcessPrint_Hook(__int64 thisptr, __int64 msg)
return true;
}
void InitialiseEngineSpewFuncHooks(HMODULE baseAddress)
{
HookEnabler hook;
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x11CA80, EngineSpewFuncHook, reinterpret_cast<LPVOID*>(&EngineSpewFunc));
// Hook print function that status concmd uses to actually print data
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x15ABD0, Status_ConMsg_Hook, reinterpret_cast<LPVOID*>(&Status_ConMsg_Original));
// Hook CClientState::ProcessPrint
ENABLER_CREATEHOOK(
hook,
(char*)baseAddress + 0x1A1530,
CClientState_ProcessPrint_Hook,
reinterpret_cast<LPVOID*>(&CClientState_ProcessPrint_Original));
Cvar_spewlog_enable = new ConVar("spewlog_enable", "1", FCVAR_NONE, "Enables/disables whether the engine spewfunc should be logged");
}
#include "bitbuf.h"
ConVar* Cvar_cl_showtextmsg;
typedef void (*TextMsg_Type)(__int64);
TextMsg_Type TextMsg_Original;
class ICenterPrint
{
public:
@ -453,11 +158,22 @@ class ICenterPrint
virtual void SetTextColor(int r, int g, int b, int a) = 0;
};
ICenterPrint* internalCenterPrint = NULL;
ICenterPrint* pInternalCenterPrint = NULL;
void TextMsgHook(BFRead* msg)
enum class TextMsgPrintType_t
{
int msg_dest = msg->ReadByte();
HUD_PRINTNOTIFY = 1,
HUD_PRINTCONSOLE,
HUD_PRINTTALK,
HUD_PRINTCENTER
};
// clang-format off
AUTOHOOK(TextMsg, client.dll + 0x198710,
void,, (BFRead* msg))
// clang-format on
{
TextMsgPrintType_t msg_dest = (TextMsgPrintType_t)msg->ReadByte();
char text[256];
msg->ReadString(text, sizeof(text));
@ -467,29 +183,86 @@ void TextMsgHook(BFRead* msg)
switch (msg_dest)
{
case 4: // HUD_PRINTCENTER
internalCenterPrint->Print(text);
case TextMsgPrintType_t::HUD_PRINTCENTER:
pInternalCenterPrint->Print(text);
break;
default:
spdlog::warn("Unimplemented TextMsg type {}! printing to console", msg_dest);
[[fallthrough]];
case 2: // HUD_PRINTCONSOLE
case TextMsgPrintType_t::HUD_PRINTCONSOLE:
auto endpos = strlen(text);
if (text[endpos - 1] == '\n')
text[endpos - 1] = '\0'; // cut off repeated newline
spdlog::info(text);
break;
}
}
void InitialiseClientPrintHooks(HMODULE baseAddress)
// clang-format off
AUTOHOOK(ConCommand_echo, engine.dll + 0x123680,
void,, (const CCommand& arg))
// clang-format on
{
HookEnabler hook;
if (arg.ArgC() >= 2)
spdlog::info("[echo] {}", arg.ArgS());
}
internalCenterPrint = (ICenterPrint*)((char*)baseAddress + 0x216E940);
// This needs to be called after hooks are loaded so we can access the command line args
void CreateLogFiles()
{
if (strstr(GetCommandLineA(), "-disablelogs"))
{
spdlog::default_logger()->set_level(spdlog::level::off);
}
else
{
try
{
// todo: might be good to delete logs that are too old
time_t time = std::time(nullptr);
tm currentTime = *std::localtime(&time);
std::stringstream stream;
// "TextMsg" usermessage
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x198710, TextMsgHook, reinterpret_cast<LPVOID*>(&TextMsg_Original));
stream << std::put_time(&currentTime, (GetNorthstarPrefix() + "/logs/nslog%Y-%m-%d %H-%M-%S.txt").c_str());
spdlog::default_logger()->sinks().push_back(std::make_shared<spdlog::sinks::basic_file_sink_mt>(stream.str(), false));
spdlog::flush_on(spdlog::level::info);
}
catch (...)
{
spdlog::error("Failed creating log file!");
MessageBoxA(
0, "Failed creating log file! Make sure the profile directory is writable.", "Northstar Warning", MB_ICONWARNING | MB_OK);
}
}
}
void InitialiseLogging()
{
AllocConsole();
// Bind stdout to receive console output.
// these two lines are responsible for stuff to not show up in the console sometimes, from talking about it on discord
// apparently they were meant to make logging work when using -northstar, however from testing it seems that it doesnt
// work regardless of these two lines
// freopen("CONOUT$", "w", stdout);
// freopen("CONOUT$", "w", stderr);
spdlog::default_logger()->set_pattern("[%H:%M:%S] [%l] %v");
}
ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", EngineSpewFuncHooks, ConVar, (CModule module))
{
AUTOHOOK_DISPATCH_MODULE(engine.dll)
Cvar_spewlog_enable = new ConVar("spewlog_enable", "1", FCVAR_NONE, "Enables/disables whether the engine spewfunc should be logged");
}
ON_DLL_LOAD_CLIENT_RELIESON("client.dll", ClientPrintHooks, ConVar, (CModule module))
{
AUTOHOOK_DISPATCH_MODULE(client.dll)
Cvar_cl_showtextmsg = new ConVar("cl_showtextmsg", "1", FCVAR_NONE, "Enable/disable text messages printing on the screen.");
pInternalCenterPrint = module.Offset(0x216E940).As<ICenterPrint*>();
}

View File

@ -1,7 +1,4 @@
#pragma once
#include "context.h"
void CreateLogFiles();
void InitialiseLogging();
void InitialiseEngineSpewFuncHooks(HMODULE baseAddress);
void InitialiseClientPrintHooks(HMODULE baseAddress);

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,15 @@
#pragma once
#include "convar.h"
#include "serverpresence.h"
#include <winsock2.h>
#include <string>
#include <cstring>
#include <future>
extern ConVar* Cvar_ns_masterserver_hostname;
extern ConVar* Cvar_ns_curl_log_enable;
struct RemoteModInfo
{
public:
@ -82,8 +89,6 @@ class MasterServerManager
char m_sOwnClientAuthToken[33];
std::string m_sOwnModInfoJson;
std::string m_sUnicodeServerName; // Unicode unescaped version of Cvar_ns_auth_servername for support in cjk characters
std::string m_sUnicodeServerDesc; // Unicode unescaped version of Cvar_ns_auth_serverdesc for support in cjk characters
bool m_bOriginAuthWithMasterServerDone = false;
bool m_bOriginAuthWithMasterServerInProgress = false;
@ -97,8 +102,7 @@ class MasterServerManager
bool m_bNewgameAfterSelfAuth = false;
bool m_bScriptAuthenticatingWithGameServer = false;
bool m_bSuccessfullyAuthenticatedWithGameServer = false;
std::string s_authfail_reason {};
std::string m_sAuthFailureReason {};
bool m_bHasPendingConnectionInfo = false;
RemoteServerConnectionInfo m_pendingConnectionInfo;
@ -108,28 +112,76 @@ class MasterServerManager
bool m_bHasMainMenuPromoData = false;
MainMenuPromoData m_sMainMenuPromoData;
private:
void SetCommonHttpClientOptions(CURL* curl);
public:
MasterServerManager();
void ClearServerList();
void RequestServerList();
void RequestMainMenuPromos();
void AuthenticateOriginWithMasterServer(char* uid, char* originToken);
void AuthenticateWithOwnServer(char* uid, char* playerToken);
void AuthenticateWithServer(char* uid, char* playerToken, char* serverId, char* password);
void
AddSelfToServerList(int port, int authPort, char* name, char* description, char* map, char* playlist, int maxPlayers, char* password);
void UpdateServerMapAndPlaylist(char* map, char* playlist, int playerCount);
void UpdateServerPlayerCount(int playerCount);
void WritePlayerPersistentData(char* playerId, char* pdata, size_t pdataSize);
void RemoveSelfFromServerList();
void AuthenticateOriginWithMasterServer(const char* uid, const char* originToken);
void AuthenticateWithOwnServer(const char* uid, const char* playerToken);
void AuthenticateWithServer(const char* uid, const char* playerToken, const char* serverId, const char* password);
void WritePlayerPersistentData(const char* playerId, const char* pdata, size_t pdataSize);
};
std::string unescape_unicode(const std::string& str);
void UpdateServerInfoFromUnicodeToUTF8();
void InitialiseSharedMasterServer(HMODULE baseAddress);
extern MasterServerManager* g_MasterServerManager;
extern MasterServerManager* g_pMasterServerManager;
extern ConVar* Cvar_ns_masterserver_hostname;
extern ConVar* Cvar_ns_server_password;
/** Result returned in the std::future of a MasterServerPresenceReporter::ReportPresence() call. */
enum class MasterServerReportPresenceResult
{
// Adding this server to the MS was successful.
Success,
// We failed to add this server to the MS and should retry.
Failed,
// We failed to add this server to the MS and shouldn't retry.
FailedNoRetry,
// We failed to even reach the MS.
FailedNoConnect,
// We failed to add the server because an existing server with the same ip:port exists.
FailedDuplicateServer,
};
class MasterServerPresenceReporter : public ServerPresenceReporter
{
public:
/** Full data returned in the std::future of a MasterServerPresenceReporter::ReportPresence() call. */
struct ReportPresenceResultData
{
MasterServerReportPresenceResult result;
std::optional<std::string> id;
std::optional<std::string> serverAuthToken;
};
const int MAX_REGISTRATION_ATTEMPTS = 5;
// Called to initialise the master server presence reporter's state.
void CreatePresence(const ServerPresence* pServerPresence) override;
// Run on an internal to either add the server to the MS or update it.
void ReportPresence(const ServerPresence* pServerPresence) override;
// Called when we need to remove the server from the master server.
void DestroyPresence(const ServerPresence* pServerPresence) override;
// Called every frame.
void RunFrame(double flCurrentTime, const ServerPresence* pServerPresence) override;
protected:
// Contains the async logic to add the server to the MS.
void InternalAddServer(const ServerPresence* pServerPresence);
// Contains the async logic to update the server on the MS.
void InternalUpdateServer(const ServerPresence* pServerPresence);
// The future used for InternalAddServer() calls.
std::future<ReportPresenceResultData> addServerFuture;
// The future used for InternalAddServer() calls.
std::future<ReportPresenceResultData> updateServerFuture;
int m_nNumRegistrationAttempts;
double m_fNextAddServerAttemptTime;
};

View File

@ -1,6 +1,8 @@
#include "pch.h"
#include "tier0.h"
#include "maxplayers.h"
#include "gameutils.h"
AUTOHOOK_INIT()
// never set this to anything below 32
#define NEW_MAX_PLAYERS 64
@ -45,50 +47,33 @@ constexpr int Team_PlayerArray_AddedLength = NEW_MAX_PLAYERS - 32;
constexpr int Team_PlayerArray_AddedSize = PAD_NUMBER(Team_PlayerArray_AddedLength * 8, 4);
constexpr int Team_AddedSize = Team_PlayerArray_AddedSize;
#include "nsmem.h"
template <class T> void ChangeOffset(void* addr, unsigned int offset)
bool MaxPlayersIncreaseEnabled()
{
NSMem::BytePatch((uintptr_t)addr, (BYTE*)&offset, sizeof(T));
static bool bMaxPlayersIncreaseEnabled = Tier0::CommandLine()->CheckParm("-experimentalmaxplayersincrease");
return bMaxPlayersIncreaseEnabled;
}
/*
typedef bool(*MatchRecvPropsToSendProps_R_Type)(__int64 lookup, __int64 tableNameBroken, __int64 sendTable, __int64 recvTable);
MatchRecvPropsToSendProps_R_Type MatchRecvPropsToSendProps_R_Original;
bool MatchRecvPropsToSendProps_R_Hook(__int64 lookup, __int64 tableNameBroken, __int64 sendTable, __int64 recvTable)
// should we use R2 for this? not sure
namespace R2 // use R2 namespace for game funcs
{
const char* tableName = *(const char**)(sendTable + 0x118);
int GetMaxPlayers()
{
if (MaxPlayersIncreaseEnabled())
return NEW_MAX_PLAYERS;
spdlog::info("MatchRecvPropsToSendProps_R table name {}", tableName);
bool orig = MatchRecvPropsToSendProps_R_Original(lookup, tableNameBroken, sendTable, recvTable);
return orig;
}
typedef bool(*DataTable_SetupReceiveTableFromSendTable_Type)(__int64 sendTable, bool needsDecoder);
DataTable_SetupReceiveTableFromSendTable_Type DataTable_SetupReceiveTableFromSendTable_Original;
bool DataTable_SetupReceiveTableFromSendTable_Hook(__int64 sendTable, bool needsDecoder)
{
const char* tableName = *(const char**)(sendTable + 0x118);
spdlog::info("DataTable_SetupReceiveTableFromSendTable table name {}", tableName);
if (!strcmp(tableName, "m_bConnected")) {
char f[64];
sprintf_s(f, "%p", sendTable);
MessageBoxA(0, f, "DataTable_SetupReceiveTableFromSendTable", 0);
return 32;
}
} // namespace R2
return DataTable_SetupReceiveTableFromSendTable_Original(sendTable, needsDecoder);
template <class T> void ChangeOffset(MemoryAddress addr, unsigned int offset)
{
addr.Patch((BYTE*)&offset, sizeof(T));
}
*/
typedef void* (*StringTables_CreateStringTable_Type)(
__int64 thisptr, const char* name, int maxentries, int userdatafixedsize, int userdatanetworkbits, int flags);
StringTables_CreateStringTable_Type StringTables_CreateStringTable_Original;
void* StringTables_CreateStringTable_Hook(
__int64 thisptr, const char* name, int maxentries, int userdatafixedsize, int userdatanetworkbits, int flags)
// clang-format off
AUTOHOOK(StringTables_CreateStringTable, engine.dll + 0x22E220,
void*,, (void* thisptr, const char* name, int maxentries, int userdatafixedsize, int userdatanetworkbits, int flags))
// clang-format on
{
// Change the amount of entries to account for a bigger player amount
if (!strcmp(name, "userinfo"))
@ -100,36 +85,33 @@ void* StringTables_CreateStringTable_Hook(
maxentries = maxPlayersPowerOf2;
}
return StringTables_CreateStringTable_Original(thisptr, name, maxentries, userdatafixedsize, userdatanetworkbits, flags);
return StringTables_CreateStringTable(thisptr, name, maxentries, userdatafixedsize, userdatanetworkbits, flags);
}
bool MaxPlayersIncreaseEnabled()
{
return CommandLine() && CommandLine()->CheckParm("-experimentalmaxplayersincrease");
}
void InitialiseMaxPlayersOverride_Engine(HMODULE baseAddress)
ON_DLL_LOAD("engine.dll", MaxPlayersOverride_Engine, (CModule module))
{
if (!MaxPlayersIncreaseEnabled())
return;
AUTOHOOK_DISPATCH_MODULE(engine.dll)
// patch GetPlayerLimits to ignore the boundary limit
ChangeOffset<unsigned char>((char*)baseAddress + 0x116458, 0xEB); // jle => jmp
module.Offset(0x116458).Patch("0xEB"); // jle => jmp
// patch ED_Alloc to change nFirstIndex
ChangeOffset<int>((char*)baseAddress + 0x18F46C + 1, NEW_MAX_PLAYERS + 8 + 1); // original: 41 (sv.GetMaxClients() + 1)
ChangeOffset<int>(module.Offset(0x18F46C + 1), NEW_MAX_PLAYERS + 8 + 1); // original: 41 (sv.GetMaxClients() + 1)
// patch CGameServer::SpawnServer to change GetMaxClients inline
ChangeOffset<int>((char*)baseAddress + 0x119543 + 2, NEW_MAX_PLAYERS + 8 + 1); // original: 41 (sv.GetMaxClients() + 1)
ChangeOffset<int>(module.Offset(0x119543 + 2), NEW_MAX_PLAYERS + 8 + 1); // original: 41 (sv.GetMaxClients() + 1)
// patch CGameServer::SpawnServer to change for loop
ChangeOffset<unsigned char>((char*)baseAddress + 0x11957F + 2, NEW_MAX_PLAYERS); // original: 32
ChangeOffset<unsigned char>(module.Offset(0x11957F + 2), NEW_MAX_PLAYERS); // original: 32
// patch CGameServer::SpawnServer to change for loop (there are two)
ChangeOffset<unsigned char>((char*)baseAddress + 0x119586 + 2, NEW_MAX_PLAYERS + 1); // original: 33 (32 + 1)
ChangeOffset<unsigned char>(module.Offset(0x119586 + 2), NEW_MAX_PLAYERS + 1); // original: 33 (32 + 1)
// patch max players somewhere in CClientState
ChangeOffset<unsigned char>((char*)baseAddress + 0x1A162C + 2, NEW_MAX_PLAYERS - 1); // original: 31 (32 - 1)
ChangeOffset<unsigned char>(module.Offset(0x1A162C + 2), NEW_MAX_PLAYERS - 1); // original: 31 (32 - 1)
// patch max players in userinfo stringtable creation
/*{
@ -142,22 +124,10 @@ void InitialiseMaxPlayersOverride_Engine(HMODULE baseAddress)
// proper fix below
// patch max players in userinfo stringtable creation loop
ChangeOffset<unsigned char>((char*)baseAddress + 0x114C48 + 2, NEW_MAX_PLAYERS); // original: 32
ChangeOffset<unsigned char>(module.Offset(0x114C48 + 2), NEW_MAX_PLAYERS); // original: 32
// do not load prebaked SendTable message list
ChangeOffset<unsigned char>((char*)baseAddress + 0x75859, 0xEB); // jnz -> jmp
HookEnabler hook;
// ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x209000, &MatchRecvPropsToSendProps_R_Hook,
// reinterpret_cast<LPVOID*>(&MatchRecvPropsToSendProps_R_Original)); ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x1FACD0,
// &DataTable_SetupReceiveTableFromSendTable_Hook, reinterpret_cast<LPVOID*>(&DataTable_SetupReceiveTableFromSendTable_Original));
ENABLER_CREATEHOOK(
hook,
(char*)baseAddress + 0x22E220,
&StringTables_CreateStringTable_Hook,
reinterpret_cast<LPVOID*>(&StringTables_CreateStringTable_Original));
module.Offset(0x75859).Patch("EB"); // jnz -> jmp
}
typedef void (*RunUserCmds_Type)(bool a1, float a2);
@ -167,7 +137,10 @@ HMODULE serverBase = 0;
auto RandomIntZeroMax = (__int64(__fastcall*)())0;
// lazy rebuild
void RunUserCmds_Hook(bool a1, float a2)
// clang-format off
AUTOHOOK(RunUserCmds, server.dll + 0x483D10,
void,, (bool a1, float a2))
// clang-format on
{
unsigned char v3; // bl
int v5; // er14
@ -308,162 +281,160 @@ void RunUserCmds_Hook(bool a1, float a2)
}
}
typedef __int64 (*SendPropArray2_Type)(__int64 recvProp, int elements, int flags, const char* name, __int64 proxyFn, unsigned char unk1);
SendPropArray2_Type SendPropArray2_Original;
__int64 __fastcall SendPropArray2_Hook(__int64 recvProp, int elements, int flags, const char* name, __int64 proxyFn, unsigned char unk1)
// clang-format off
AUTOHOOK(SendPropArray2, server.dll + 0x12B130,
__int64, __fastcall, (__int64 recvProp, int elements, int flags, const char* name, __int64 proxyFn, unsigned char unk1))
// clang-format on
{
// Change the amount of elements to account for a bigger player amount
if (!strcmp(name, "\"player_array\""))
elements = NEW_MAX_PLAYERS;
return SendPropArray2_Original(recvProp, elements, flags, name, proxyFn, unk1);
return SendPropArray2(recvProp, elements, flags, name, proxyFn, unk1);
}
void InitialiseMaxPlayersOverride_Server(HMODULE baseAddress)
ON_DLL_LOAD("server.dll", MaxPlayersOverride_Server, (CModule module))
{
if (!MaxPlayersIncreaseEnabled())
return;
AUTOHOOK_DISPATCH_MODULE(server.dll)
// get required data
serverBase = GetModuleHandleA("server.dll");
serverBase = (HMODULE)module.m_nAddress;
RandomIntZeroMax = (decltype(RandomIntZeroMax))(GetProcAddress(GetModuleHandleA("vstdlib.dll"), "RandomIntZeroMax"));
// patch max players amount
ChangeOffset<unsigned char>((char*)baseAddress + 0x9A44D + 3, NEW_MAX_PLAYERS); // 0x20 (32) => 0x80 (128)
ChangeOffset<unsigned char>(module.Offset(0x9A44D + 3), NEW_MAX_PLAYERS); // 0x20 (32) => 0x80 (128)
// patch SpawnGlobalNonRewinding to change forced edict index
ChangeOffset<unsigned char>((char*)baseAddress + 0x2BC403 + 2, NEW_MAX_PLAYERS + 1); // original: 33 (32 + 1)
ChangeOffset<unsigned char>(module.Offset(0x2BC403 + 2), NEW_MAX_PLAYERS + 1); // original: 33 (32 + 1)
constexpr int CPlayerResource_OriginalSize = 4776;
constexpr int CPlayerResource_AddedSize = PlayerResource_TotalSize;
constexpr int CPlayerResource_ModifiedSize = CPlayerResource_OriginalSize + CPlayerResource_AddedSize;
// CPlayerResource class allocation function - allocate a bigger amount to fit all new max player data
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C560A + 1, CPlayerResource_ModifiedSize);
ChangeOffset<unsigned int>(module.Offset(0x5C560A + 1), CPlayerResource_ModifiedSize);
// DT_PlayerResource::m_iPing SendProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C5059 + 2, CPlayerResource_OriginalSize + PlayerResource_Ping_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C50A8 + 2, CPlayerResource_OriginalSize + PlayerResource_Ping_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C50E2 + 4, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0x5C5059 + 2), CPlayerResource_OriginalSize + PlayerResource_Ping_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C50A8 + 2), CPlayerResource_OriginalSize + PlayerResource_Ping_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C50E2 + 4), NEW_MAX_PLAYERS + 1);
// DT_PlayerResource::m_iPing DataMap
ChangeOffset<unsigned int>((char*)baseAddress + 0xB94598, CPlayerResource_OriginalSize + PlayerResource_Ping_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB9459C, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB945C0, PlayerResource_Ping_Size);
ChangeOffset<unsigned int>(module.Offset(0xB94598), CPlayerResource_OriginalSize + PlayerResource_Ping_Start);
ChangeOffset<unsigned short>(module.Offset(0xB9459C), NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned short>(module.Offset(0xB945C0), PlayerResource_Ping_Size);
// DT_PlayerResource::m_iTeam SendProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C5110 + 2, CPlayerResource_OriginalSize + PlayerResource_Team_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C519C + 2, CPlayerResource_OriginalSize + PlayerResource_Team_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C517E + 4, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0x5C5110 + 2), CPlayerResource_OriginalSize + PlayerResource_Team_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C519C + 2), CPlayerResource_OriginalSize + PlayerResource_Team_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C517E + 4), NEW_MAX_PLAYERS + 1);
// DT_PlayerResource::m_iTeam DataMap
ChangeOffset<unsigned int>((char*)baseAddress + 0xB94600, CPlayerResource_OriginalSize + PlayerResource_Team_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB94604, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB94628, PlayerResource_Team_Size);
ChangeOffset<unsigned int>(module.Offset(0xB94600), CPlayerResource_OriginalSize + PlayerResource_Team_Start);
ChangeOffset<unsigned short>(module.Offset(0xB94604), NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned short>(module.Offset(0xB94628), PlayerResource_Team_Size);
// DT_PlayerResource::m_iPRHealth SendProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C51C0 + 2, CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C5204 + 2, CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C523E + 4, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0x5C51C0 + 2), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C5204 + 2), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C523E + 4), NEW_MAX_PLAYERS + 1);
// DT_PlayerResource::m_iPRHealth DataMap
ChangeOffset<unsigned int>((char*)baseAddress + 0xB94668, CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB9466C, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB94690, PlayerResource_PRHealth_Size);
ChangeOffset<unsigned int>(module.Offset(0xB94668), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
ChangeOffset<unsigned short>(module.Offset(0xB9466C), NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned short>(module.Offset(0xB94690), PlayerResource_PRHealth_Size);
// DT_PlayerResource::m_bConnected SendProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C526C + 2, CPlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C52B4 + 2, CPlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C52EE + 4, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0x5C526C + 2), CPlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C52B4 + 2), CPlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C52EE + 4), NEW_MAX_PLAYERS + 1);
// DT_PlayerResource::m_bConnected DataMap
ChangeOffset<unsigned int>((char*)baseAddress + 0xB946D0, CPlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB946D4, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB946F8, PlayerResource_Connected_Size);
ChangeOffset<unsigned int>(module.Offset(0xB946D0), CPlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned short>(module.Offset(0xB946D4), NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned short>(module.Offset(0xB946F8), PlayerResource_Connected_Size);
// DT_PlayerResource::m_bAlive SendProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C5321 + 2, CPlayerResource_OriginalSize + PlayerResource_Alive_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C5364 + 2, CPlayerResource_OriginalSize + PlayerResource_Alive_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C539E + 4, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0x5C5321 + 2), CPlayerResource_OriginalSize + PlayerResource_Alive_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C5364 + 2), CPlayerResource_OriginalSize + PlayerResource_Alive_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C539E + 4), NEW_MAX_PLAYERS + 1);
// DT_PlayerResource::m_bAlive DataMap
ChangeOffset<unsigned int>((char*)baseAddress + 0xB94738, CPlayerResource_OriginalSize + PlayerResource_Alive_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB9473C, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB94760, PlayerResource_Alive_Size);
ChangeOffset<unsigned int>(module.Offset(0xB94738), CPlayerResource_OriginalSize + PlayerResource_Alive_Start);
ChangeOffset<unsigned short>(module.Offset(0xB9473C), NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned short>(module.Offset(0xB94760), PlayerResource_Alive_Size);
// DT_PlayerResource::m_boolStats SendProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C53CC + 2, CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C5414 + 2, CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C544E + 4, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0x5C53CC + 2), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C5414 + 2), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C544E + 4), NEW_MAX_PLAYERS + 1);
// DT_PlayerResource::m_boolStats DataMap
ChangeOffset<unsigned int>((char*)baseAddress + 0xB947A0, CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB947A4, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB947C8, PlayerResource_BoolStats_Size);
ChangeOffset<unsigned int>(module.Offset(0xB947A0), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
ChangeOffset<unsigned short>(module.Offset(0xB947A4), NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned short>(module.Offset(0xB947C8), PlayerResource_BoolStats_Size);
// DT_PlayerResource::m_killStats SendProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C547C + 2, CPlayerResource_OriginalSize + PlayerResource_KillStats_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C54E2 + 2, CPlayerResource_OriginalSize + PlayerResource_KillStats_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C54FE + 4, PlayerResource_KillStats_Length);
ChangeOffset<unsigned int>(module.Offset(0x5C547C + 2), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C54E2 + 2), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C54FE + 4), PlayerResource_KillStats_Length);
// DT_PlayerResource::m_killStats DataMap
ChangeOffset<unsigned int>((char*)baseAddress + 0xB94808, CPlayerResource_OriginalSize + PlayerResource_KillStats_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB9480C, PlayerResource_KillStats_Length);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB94830, PlayerResource_KillStats_Size);
ChangeOffset<unsigned int>(module.Offset(0xB94808), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start);
ChangeOffset<unsigned short>(module.Offset(0xB9480C), PlayerResource_KillStats_Length);
ChangeOffset<unsigned short>(module.Offset(0xB94830), PlayerResource_KillStats_Size);
// DT_PlayerResource::m_scoreStats SendProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C5528 + 2, CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C5576 + 2, CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C5584 + 4, PlayerResource_ScoreStats_Length);
ChangeOffset<unsigned int>(module.Offset(0x5C5528 + 2), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C5576 + 2), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C5584 + 4), PlayerResource_ScoreStats_Length);
// DT_PlayerResource::m_scoreStats DataMap
ChangeOffset<unsigned int>((char*)baseAddress + 0xB94870, CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB94874, PlayerResource_ScoreStats_Length);
ChangeOffset<unsigned short>((char*)baseAddress + 0xB94898, PlayerResource_ScoreStats_Size);
ChangeOffset<unsigned int>(module.Offset(0xB94870), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
ChangeOffset<unsigned short>(module.Offset(0xB94874), PlayerResource_ScoreStats_Length);
ChangeOffset<unsigned short>(module.Offset(0xB94898), PlayerResource_ScoreStats_Size);
// CPlayerResource::UpdatePlayerData - m_bConnected
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C66EE + 4, CPlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C672E + 4, CPlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C66EE + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C672E + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start);
// CPlayerResource::UpdatePlayerData - m_iPing
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C6394 + 4, CPlayerResource_OriginalSize + PlayerResource_Ping_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C63DB + 4, CPlayerResource_OriginalSize + PlayerResource_Ping_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C6394 + 4), CPlayerResource_OriginalSize + PlayerResource_Ping_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C63DB + 4), CPlayerResource_OriginalSize + PlayerResource_Ping_Start);
// CPlayerResource::UpdatePlayerData - m_iTeam
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C63FD + 4, CPlayerResource_OriginalSize + PlayerResource_Team_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C6442 + 4, CPlayerResource_OriginalSize + PlayerResource_Team_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C63FD + 4), CPlayerResource_OriginalSize + PlayerResource_Team_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C6442 + 4), CPlayerResource_OriginalSize + PlayerResource_Team_Start);
// CPlayerResource::UpdatePlayerData - m_iPRHealth
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C645B + 4, CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C64A0 + 4, CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C645B + 4), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C64A0 + 4), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
// CPlayerResource::UpdatePlayerData - m_bConnected
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C64AA + 4, CPlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C64F0 + 4, CPlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C64AA + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C64F0 + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start);
// CPlayerResource::UpdatePlayerData - m_bAlive
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C650A + 4, CPlayerResource_OriginalSize + PlayerResource_Alive_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C654F + 4, CPlayerResource_OriginalSize + PlayerResource_Alive_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C650A + 4), CPlayerResource_OriginalSize + PlayerResource_Alive_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C654F + 4), CPlayerResource_OriginalSize + PlayerResource_Alive_Start);
// CPlayerResource::UpdatePlayerData - m_boolStats
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C6557 + 4, CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C65A5 + 4, CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C6557 + 4), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C65A5 + 4), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
// CPlayerResource::UpdatePlayerData - m_scoreStats
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C65C2 + 3, CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C65E3 + 4, CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C65C2 + 3), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C65E3 + 4), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
// CPlayerResource::UpdatePlayerData - m_killStats
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C6654 + 3, CPlayerResource_OriginalSize + PlayerResource_KillStats_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x5C665B + 3, CPlayerResource_OriginalSize + PlayerResource_KillStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C6654 + 3), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x5C665B + 3), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start);
// GameLoop::RunUserCmds - rebuild
HookEnabler hook;
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x483D10, &RunUserCmds_Hook, reinterpret_cast<LPVOID*>(&RunUserCmds_Original));
*(DWORD*)((char*)baseAddress + 0x14E7390) = 0;
auto DT_PlayerResource_Construct = (__int64(__fastcall*)())((char*)baseAddress + 0x5C4FE0);
*module.Offset(0x14E7390).As<DWORD*>() = 0;
auto DT_PlayerResource_Construct = module.Offset(0x5C4FE0).As<__int64(__fastcall*)()>();
DT_PlayerResource_Construct();
constexpr int CTeam_OriginalSize = 3336;
@ -471,191 +442,188 @@ void InitialiseMaxPlayersOverride_Server(HMODULE baseAddress)
constexpr int CTeam_ModifiedSize = CTeam_OriginalSize + CTeam_AddedSize;
// CTeam class allocation function - allocate a bigger amount to fit all new team player data
ChangeOffset<unsigned int>((char*)baseAddress + 0x23924A + 1, CTeam_ModifiedSize);
ChangeOffset<unsigned int>(module.Offset(0x23924A + 1), CTeam_ModifiedSize);
// CTeam::CTeam - increase memset length to clean newly allocated data
ChangeOffset<unsigned int>((char*)baseAddress + 0x2395AE + 2, 256 + CTeam_AddedSize);
ChangeOffset<unsigned int>(module.Offset(0x2395AE + 2), 256 + CTeam_AddedSize);
// hook required to change the size of DT_Team::"player_array"
HookEnabler hook2;
ENABLER_CREATEHOOK(hook2, (char*)baseAddress + 0x12B130, &SendPropArray2_Hook, reinterpret_cast<LPVOID*>(&SendPropArray2_Original));
hook2.~HookEnabler(); // force hook before calling construct function
*(DWORD*)((char*)baseAddress + 0xC945A0) = 0;
auto DT_Team_Construct = (__int64(__fastcall*)())((char*)baseAddress + 0x238F50);
*module.Offset(0xC945A0).As<DWORD*>() = 0;
auto DT_Team_Construct = module.Offset(0x238F50).As<__int64(__fastcall*)()>();
DT_Team_Construct();
}
typedef __int64 (*RecvPropArray2_Type)(__int64 recvProp, int elements, int flags, const char* name, __int64 proxyFn);
RecvPropArray2_Type RecvPropArray2_Original;
__int64 __fastcall RecvPropArray2_Hook(__int64 recvProp, int elements, int flags, const char* name, __int64 proxyFn)
// clang-format off
AUTOHOOK(RecvPropArray2, client.dll + 0x1CEDA0,
__int64, __fastcall, (__int64 recvProp, int elements, int flags, const char* name, __int64 proxyFn))
// clang-format on
{
// Change the amount of elements to account for a bigger player amount
if (!strcmp(name, "\"player_array\""))
elements = NEW_MAX_PLAYERS;
return RecvPropArray2_Original(recvProp, elements, flags, name, proxyFn);
return RecvPropArray2(recvProp, elements, flags, name, proxyFn);
}
void InitialiseMaxPlayersOverride_Client(HMODULE baseAddress)
ON_DLL_LOAD("client.dll", MaxPlayersOverride_Client, (CModule module))
{
if (!MaxPlayersIncreaseEnabled())
return;
AUTOHOOK_DISPATCH_MODULE(client.dll)
constexpr int C_PlayerResource_OriginalSize = 5768;
constexpr int C_PlayerResource_AddedSize = PlayerResource_TotalSize;
constexpr int C_PlayerResource_ModifiedSize = C_PlayerResource_OriginalSize + C_PlayerResource_AddedSize;
// C_PlayerResource class allocation function - allocate a bigger amount to fit all new max player data
ChangeOffset<unsigned int>((char*)baseAddress + 0x164C41 + 1, C_PlayerResource_ModifiedSize);
ChangeOffset<unsigned int>(module.Offset(0x164C41 + 1), C_PlayerResource_ModifiedSize);
// C_PlayerResource::C_PlayerResource - change loop end value
ChangeOffset<unsigned char>((char*)baseAddress + 0x1640C4 + 2, NEW_MAX_PLAYERS - 32);
ChangeOffset<unsigned char>(module.Offset(0x1640C4 + 2), NEW_MAX_PLAYERS - 32);
// C_PlayerResource::C_PlayerResource - change m_szName address
ChangeOffset<unsigned int>(
(char*)baseAddress + 0x1640D0 + 3, C_PlayerResource_OriginalSize + PlayerResource_Name_Start); // appended to the end of the class
module.Offset(0x1640D0 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); // appended to the end of the class
// C_PlayerResource::C_PlayerResource - change m_szName address
ChangeOffset<unsigned int>(
(char*)baseAddress + 0x1640D0 + 3, C_PlayerResource_OriginalSize + PlayerResource_Name_Start); // appended to the end of the class
module.Offset(0x1640D0 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); // appended to the end of the class
// C_PlayerResource::C_PlayerResource - increase memset length to clean newly allocated data
ChangeOffset<unsigned int>((char*)baseAddress + 0x1640D0 + 3, 2244 + C_PlayerResource_AddedSize);
ChangeOffset<unsigned int>(module.Offset(0x1640D0 + 3), 2244 + C_PlayerResource_AddedSize);
// C_PlayerResource::UpdatePlayerName - change m_szName address
ChangeOffset<unsigned int>((char*)baseAddress + 0x16431F + 3, C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
ChangeOffset<unsigned int>(module.Offset(0x16431F + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
// C_PlayerResource::GetPlayerName - change m_szName address 1
ChangeOffset<unsigned int>((char*)baseAddress + 0x1645B1 + 3, C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
ChangeOffset<unsigned int>(module.Offset(0x1645B1 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
// C_PlayerResource::GetPlayerName - change m_szName address 2
ChangeOffset<unsigned int>((char*)baseAddress + 0x1645C0 + 3, C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
ChangeOffset<unsigned int>(module.Offset(0x1645C0 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
// C_PlayerResource::GetPlayerName - change m_szName address 3
ChangeOffset<unsigned int>((char*)baseAddress + 0x1645DD + 3, C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
ChangeOffset<unsigned int>(module.Offset(0x1645DD + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
// C_PlayerResource::GetPlayerName internal func - change m_szName address 1
ChangeOffset<unsigned int>((char*)baseAddress + 0x164B71 + 4, C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
ChangeOffset<unsigned int>(module.Offset(0x164B71 + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
// C_PlayerResource::GetPlayerName internal func - change m_szName address 2
ChangeOffset<unsigned int>((char*)baseAddress + 0x164B9B + 4, C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
ChangeOffset<unsigned int>(module.Offset(0x164B9B + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
// C_PlayerResource::GetPlayerName2 (?) - change m_szName address 1
ChangeOffset<unsigned int>((char*)baseAddress + 0x164641 + 3, C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
ChangeOffset<unsigned int>(module.Offset(0x164641 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
// C_PlayerResource::GetPlayerName2 (?) - change m_szName address 2
ChangeOffset<unsigned int>((char*)baseAddress + 0x164650 + 3, C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
ChangeOffset<unsigned int>(module.Offset(0x164650 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
// C_PlayerResource::GetPlayerName2 (?) - change m_szName address 3
ChangeOffset<unsigned int>((char*)baseAddress + 0x16466D + 3, C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
ChangeOffset<unsigned int>(module.Offset(0x16466D + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
// C_PlayerResource::GetPlayerName internal func - change m_szName2 (?) address 1
ChangeOffset<unsigned int>((char*)baseAddress + 0x164BA3 + 4, C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
ChangeOffset<unsigned int>(module.Offset(0x164BA3 + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
// C_PlayerResource::GetPlayerName internal func - change m_szName2 (?) address 2
ChangeOffset<unsigned int>((char*)baseAddress + 0x164BCE + 4, C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
ChangeOffset<unsigned int>(module.Offset(0x164BCE + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
// C_PlayerResource::GetPlayerName internal func - change m_szName2 (?) address 3
ChangeOffset<unsigned int>((char*)baseAddress + 0x164BE7 + 4, C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
ChangeOffset<unsigned int>(module.Offset(0x164BE7 + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
// C_PlayerResource::m_szName
ChangeOffset<unsigned int>((char*)baseAddress + 0xc350f8, C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xc350f8 + 4, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0xc350f8), C_PlayerResource_OriginalSize + PlayerResource_Name_Start);
ChangeOffset<unsigned short>(module.Offset(0xc350f8 + 4), NEW_MAX_PLAYERS + 1);
// DT_PlayerResource size
ChangeOffset<unsigned int>((char*)baseAddress + 0x163415 + 6, C_PlayerResource_ModifiedSize);
ChangeOffset<unsigned int>(module.Offset(0x163415 + 6), C_PlayerResource_ModifiedSize);
// DT_PlayerResource::m_iPing RecvProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x163492 + 2, C_PlayerResource_OriginalSize + PlayerResource_Ping_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x1634D6 + 2, C_PlayerResource_OriginalSize + PlayerResource_Ping_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x163515 + 5, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0x163492 + 2), C_PlayerResource_OriginalSize + PlayerResource_Ping_Start);
ChangeOffset<unsigned int>(module.Offset(0x1634D6 + 2), C_PlayerResource_OriginalSize + PlayerResource_Ping_Start);
ChangeOffset<unsigned int>(module.Offset(0x163515 + 5), NEW_MAX_PLAYERS + 1);
// C_PlayerResource::m_iPing
ChangeOffset<unsigned int>((char*)baseAddress + 0xc35170, C_PlayerResource_OriginalSize + PlayerResource_Ping_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xc35170 + 4, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0xc35170), C_PlayerResource_OriginalSize + PlayerResource_Ping_Start);
ChangeOffset<unsigned short>(module.Offset(0xc35170 + 4), NEW_MAX_PLAYERS + 1);
// DT_PlayerResource::m_iTeam RecvProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x163549 + 2, C_PlayerResource_OriginalSize + PlayerResource_Team_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x1635C8 + 2, C_PlayerResource_OriginalSize + PlayerResource_Team_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x1635AD + 5, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0x163549 + 2), C_PlayerResource_OriginalSize + PlayerResource_Team_Start);
ChangeOffset<unsigned int>(module.Offset(0x1635C8 + 2), C_PlayerResource_OriginalSize + PlayerResource_Team_Start);
ChangeOffset<unsigned int>(module.Offset(0x1635AD + 5), NEW_MAX_PLAYERS + 1);
// C_PlayerResource::m_iTeam
ChangeOffset<unsigned int>((char*)baseAddress + 0xc351e8, C_PlayerResource_OriginalSize + PlayerResource_Team_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xc351e8 + 4, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0xc351e8), C_PlayerResource_OriginalSize + PlayerResource_Team_Start);
ChangeOffset<unsigned short>(module.Offset(0xc351e8 + 4), NEW_MAX_PLAYERS + 1);
// DT_PlayerResource::m_iPRHealth RecvProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x1635F9 + 2, C_PlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x163625 + 2, C_PlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x163675 + 5, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0x1635F9 + 2), C_PlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
ChangeOffset<unsigned int>(module.Offset(0x163625 + 2), C_PlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
ChangeOffset<unsigned int>(module.Offset(0x163675 + 5), NEW_MAX_PLAYERS + 1);
// C_PlayerResource::m_iPRHealth
ChangeOffset<unsigned int>((char*)baseAddress + 0xc35260, C_PlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xc35260 + 4, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0xc35260), C_PlayerResource_OriginalSize + PlayerResource_PRHealth_Start);
ChangeOffset<unsigned short>(module.Offset(0xc35260 + 4), NEW_MAX_PLAYERS + 1);
// DT_PlayerResource::m_bConnected RecvProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x1636A9 + 2, C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x1636D5 + 2, C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x163725 + 5, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0x1636A9 + 2), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>(module.Offset(0x1636D5 + 2), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>(module.Offset(0x163725 + 5), NEW_MAX_PLAYERS + 1);
// C_PlayerResource::m_bConnected
ChangeOffset<unsigned int>((char*)baseAddress + 0xc352d8, C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xc352d8 + 4, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0xc352d8), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned short>(module.Offset(0xc352d8 + 4), NEW_MAX_PLAYERS + 1);
// DT_PlayerResource::m_bAlive RecvProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x163759 + 2, C_PlayerResource_OriginalSize + PlayerResource_Alive_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x163785 + 2, C_PlayerResource_OriginalSize + PlayerResource_Alive_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x1637D5 + 5, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0x163759 + 2), C_PlayerResource_OriginalSize + PlayerResource_Alive_Start);
ChangeOffset<unsigned int>(module.Offset(0x163785 + 2), C_PlayerResource_OriginalSize + PlayerResource_Alive_Start);
ChangeOffset<unsigned int>(module.Offset(0x1637D5 + 5), NEW_MAX_PLAYERS + 1);
// C_PlayerResource::m_bAlive
ChangeOffset<unsigned int>((char*)baseAddress + 0xc35350, C_PlayerResource_OriginalSize + PlayerResource_Alive_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xc35350 + 4, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0xc35350), C_PlayerResource_OriginalSize + PlayerResource_Alive_Start);
ChangeOffset<unsigned short>(module.Offset(0xc35350 + 4), NEW_MAX_PLAYERS + 1);
// DT_PlayerResource::m_boolStats RecvProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x163809 + 2, C_PlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x163835 + 2, C_PlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x163885 + 5, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0x163809 + 2), C_PlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x163835 + 2), C_PlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x163885 + 5), NEW_MAX_PLAYERS + 1);
// C_PlayerResource::m_boolStats
ChangeOffset<unsigned int>((char*)baseAddress + 0xc353c8, C_PlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xc353c8 + 4, NEW_MAX_PLAYERS + 1);
ChangeOffset<unsigned int>(module.Offset(0xc353c8), C_PlayerResource_OriginalSize + PlayerResource_BoolStats_Start);
ChangeOffset<unsigned short>(module.Offset(0xc353c8 + 4), NEW_MAX_PLAYERS + 1);
// DT_PlayerResource::m_killStats RecvProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x1638B3 + 2, C_PlayerResource_OriginalSize + PlayerResource_KillStats_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x1638E5 + 2, C_PlayerResource_OriginalSize + PlayerResource_KillStats_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x163935 + 5, PlayerResource_KillStats_Length);
ChangeOffset<unsigned int>(module.Offset(0x1638B3 + 2), C_PlayerResource_OriginalSize + PlayerResource_KillStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x1638E5 + 2), C_PlayerResource_OriginalSize + PlayerResource_KillStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x163935 + 5), PlayerResource_KillStats_Length);
// C_PlayerResource::m_killStats
ChangeOffset<unsigned int>((char*)baseAddress + 0xc35440, C_PlayerResource_OriginalSize + PlayerResource_KillStats_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xc35440 + 4, PlayerResource_KillStats_Length);
ChangeOffset<unsigned int>(module.Offset(0xc35440), C_PlayerResource_OriginalSize + PlayerResource_KillStats_Start);
ChangeOffset<unsigned short>(module.Offset(0xc35440 + 4), PlayerResource_KillStats_Length);
// DT_PlayerResource::m_scoreStats RecvProp
ChangeOffset<unsigned int>((char*)baseAddress + 0x163969 + 2, C_PlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x163995 + 2, C_PlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
ChangeOffset<unsigned int>((char*)baseAddress + 0x1639E5 + 5, PlayerResource_ScoreStats_Length);
ChangeOffset<unsigned int>(module.Offset(0x163969 + 2), C_PlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x163995 + 2), C_PlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
ChangeOffset<unsigned int>(module.Offset(0x1639E5 + 5), PlayerResource_ScoreStats_Length);
// C_PlayerResource::m_scoreStats
ChangeOffset<unsigned int>((char*)baseAddress + 0xc354b8, C_PlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
ChangeOffset<unsigned short>((char*)baseAddress + 0xc354b8 + 4, PlayerResource_ScoreStats_Length);
ChangeOffset<unsigned int>(module.Offset(0xc354b8), C_PlayerResource_OriginalSize + PlayerResource_ScoreStats_Start);
ChangeOffset<unsigned short>(module.Offset(0xc354b8 + 4), PlayerResource_ScoreStats_Length);
// C_PlayerResource::GetPlayerName - change m_bConnected address
ChangeOffset<unsigned int>((char*)baseAddress + 0x164599 + 3, C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>(module.Offset(0x164599 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
// C_PlayerResource::GetPlayerName2 (?) - change m_bConnected address
ChangeOffset<unsigned int>((char*)baseAddress + 0x164629 + 3, C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>(module.Offset(0x164629 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
// C_PlayerResource::GetPlayerName internal func - change m_bConnected address
ChangeOffset<unsigned int>((char*)baseAddress + 0x164B13 + 3, C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>(module.Offset(0x164B13 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
// Some other get name func (that seems to be unused) - change m_bConnected address
ChangeOffset<unsigned int>((char*)baseAddress + 0x164860 + 3, C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>(module.Offset(0x164860 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
// Some other get name func 2 (that seems to be unused too) - change m_bConnected address
ChangeOffset<unsigned int>((char*)baseAddress + 0x164834 + 3, C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
ChangeOffset<unsigned int>(module.Offset(0x164834 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start);
*(DWORD*)((char*)baseAddress + 0xC35068) = 0;
auto DT_PlayerResource_Construct = (__int64(__fastcall*)())((char*)baseAddress + 0x163400);
*module.Offset(0xC35068).As<DWORD*>() = 0;
auto DT_PlayerResource_Construct = module.Offset(0x163400).As<__int64(__fastcall*)()>();
DT_PlayerResource_Construct();
constexpr int C_Team_OriginalSize = 3200;
@ -663,20 +631,15 @@ void InitialiseMaxPlayersOverride_Client(HMODULE baseAddress)
constexpr int C_Team_ModifiedSize = C_Team_OriginalSize + C_Team_AddedSize;
// C_Team class allocation function - allocate a bigger amount to fit all new team player data
ChangeOffset<unsigned int>((char*)baseAddress + 0x182321 + 1, C_Team_ModifiedSize);
ChangeOffset<unsigned int>(module.Offset(0x182321 + 1), C_Team_ModifiedSize);
// C_Team::C_Team - increase memset length to clean newly allocated data
ChangeOffset<unsigned int>((char*)baseAddress + 0x1804A2 + 2, 256 + C_Team_AddedSize);
ChangeOffset<unsigned int>(module.Offset(0x1804A2 + 2), 256 + C_Team_AddedSize);
// DT_Team size
ChangeOffset<unsigned int>((char*)baseAddress + 0xC3AA0C, C_Team_ModifiedSize);
ChangeOffset<unsigned int>(module.Offset(0xC3AA0C), C_Team_ModifiedSize);
// hook required to change the size of DT_Team::"player_array"
HookEnabler hook;
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x1CEDA0, &RecvPropArray2_Hook, reinterpret_cast<LPVOID*>(&RecvPropArray2_Original));
hook.~HookEnabler(); // force hook before calling construct function
*(DWORD*)((char*)baseAddress + 0xC3AFF8) = 0;
auto DT_Team_Construct = (__int64(__fastcall*)())((char*)baseAddress + 0x17F950);
*module.Offset(0xC3AFF8).As<DWORD*>() = 0;
auto DT_Team_Construct = module.Offset(0x17F950).As<__int64(__fastcall*)()>();
DT_Team_Construct();
}

View File

@ -1,4 +1,7 @@
#pragma once
void InitialiseMaxPlayersOverride_Engine(HMODULE baseAddress);
void InitialiseMaxPlayersOverride_Server(HMODULE baseAddress);
void InitialiseMaxPlayersOverride_Client(HMODULE baseAddress);
// should we use R2 for this? not sure
namespace R2 // use R2 namespace for game funcs
{
int GetMaxPlayers();
} // namespace R2

View File

@ -1,6 +1,8 @@
#include "pch.h"
#include "memalloc.h"
#include "gameutils.h"
#include "tier0.h"
using namespace Tier0;
// TODO: rename to malloc and free after removing statically compiled .libs
@ -8,9 +10,8 @@ extern "C" void* _malloc_base(size_t n)
{
// allocate into static buffer if g_pMemAllocSingleton isn't initialised
if (!g_pMemAllocSingleton)
{
InitialiseTier0GameUtilFunctions(GetModuleHandleA("tier0.dll"));
}
TryCreateGlobalMemAlloc();
return g_pMemAllocSingleton->m_vtable->Alloc(g_pMemAllocSingleton, n);
}
@ -22,19 +23,16 @@ extern "C" void* _malloc_base(size_t n)
extern "C" void _free_base(void* p)
{
if (!g_pMemAllocSingleton)
{
spdlog::warn("Trying to free something before g_pMemAllocSingleton was ready, this should never happen");
InitialiseTier0GameUtilFunctions(GetModuleHandleA("tier0.dll"));
}
TryCreateGlobalMemAlloc();
g_pMemAllocSingleton->m_vtable->Free(g_pMemAllocSingleton, p);
}
extern "C" void* _realloc_base(void* oldPtr, size_t size)
{
if (!g_pMemAllocSingleton)
{
InitialiseTier0GameUtilFunctions(GetModuleHandleA("tier0.dll"));
}
TryCreateGlobalMemAlloc();
return g_pMemAllocSingleton->m_vtable->Realloc(g_pMemAllocSingleton, oldPtr, size);
}

348
NorthstarDLL/memory.cpp Normal file
View File

@ -0,0 +1,348 @@
#include "pch.h"
#include "memory.h"
MemoryAddress::MemoryAddress() : m_nAddress(0) {}
MemoryAddress::MemoryAddress(const uintptr_t nAddress) : m_nAddress(nAddress) {}
MemoryAddress::MemoryAddress(const void* pAddress) : m_nAddress(reinterpret_cast<uintptr_t>(pAddress)) {}
// operators
MemoryAddress::operator uintptr_t() const
{
return m_nAddress;
}
MemoryAddress::operator void*() const
{
return reinterpret_cast<void*>(m_nAddress);
}
MemoryAddress::operator bool() const
{
return m_nAddress != 0;
}
bool MemoryAddress::operator==(const MemoryAddress& other) const
{
return m_nAddress == other.m_nAddress;
}
bool MemoryAddress::operator!=(const MemoryAddress& other) const
{
return m_nAddress != other.m_nAddress;
}
bool MemoryAddress::operator==(const uintptr_t& addr) const
{
return m_nAddress == addr;
}
bool MemoryAddress::operator!=(const uintptr_t& addr) const
{
return m_nAddress != addr;
}
MemoryAddress MemoryAddress::operator+(const MemoryAddress& other) const
{
return Offset(other.m_nAddress);
}
MemoryAddress MemoryAddress::operator-(const MemoryAddress& other) const
{
return MemoryAddress(m_nAddress - other.m_nAddress);
}
MemoryAddress MemoryAddress::operator+(const uintptr_t& addr) const
{
return Offset(addr);
}
MemoryAddress MemoryAddress::operator-(const uintptr_t& addr) const
{
return MemoryAddress(m_nAddress - addr);
}
MemoryAddress MemoryAddress::operator*() const
{
return Deref();
}
// traversal
MemoryAddress MemoryAddress::Offset(const uintptr_t nOffset) const
{
return MemoryAddress(m_nAddress + nOffset);
}
MemoryAddress MemoryAddress::Deref(const int nNumDerefs) const
{
uintptr_t ret = m_nAddress;
for (int i = 0; i < nNumDerefs; i++)
ret = *reinterpret_cast<uintptr_t*>(ret);
return MemoryAddress(ret);
}
// patching
void MemoryAddress::Patch(const uint8_t* pBytes, const size_t nSize)
{
if (nSize)
WriteProcessMemory(GetCurrentProcess(), reinterpret_cast<LPVOID>(m_nAddress), pBytes, nSize, NULL);
}
void MemoryAddress::Patch(const std::initializer_list<uint8_t> bytes)
{
uint8_t* pBytes = new uint8_t[bytes.size()];
int i = 0;
for (const uint8_t& byte : bytes)
pBytes[i++] = byte;
Patch(pBytes, bytes.size());
delete[] pBytes;
}
inline std::vector<uint8_t> HexBytesToString(const char* pHexString)
{
std::vector<uint8_t> ret;
int size = strlen(pHexString);
for (int i = 0; i < size; i++)
{
// If this is a space character, ignore it
if (isspace(pHexString[i]))
continue;
if (i < size - 1)
{
BYTE result = 0;
for (int j = 0; j < 2; j++)
{
int val = 0;
char c = *(pHexString + i + j);
if (c >= 'a')
{
val = c - 'a' + 0xA;
}
else if (c >= 'A')
{
val = c - 'A' + 0xA;
}
else if (isdigit(c))
{
val = c - '0';
}
else
{
assert(false, "Failed to parse invalid hex string.");
val = -1;
}
result += (j == 0) ? val * 16 : val;
}
ret.push_back(result);
}
i++;
}
return ret;
}
void MemoryAddress::Patch(const char* pBytes)
{
std::vector<uint8_t> vBytes = HexBytesToString(pBytes);
Patch(vBytes.data(), vBytes.size());
}
void MemoryAddress::NOP(const size_t nSize)
{
uint8_t* pBytes = new uint8_t[nSize];
memset(pBytes, 0x90, nSize);
Patch(pBytes, nSize);
delete[] pBytes;
}
bool MemoryAddress::IsMemoryReadable(const size_t nSize)
{
static SYSTEM_INFO sysInfo;
if (!sysInfo.dwPageSize)
GetSystemInfo(&sysInfo);
MEMORY_BASIC_INFORMATION memInfo;
if (!VirtualQuery(reinterpret_cast<LPCVOID>(m_nAddress), &memInfo, sizeof(memInfo)))
return false;
return memInfo.RegionSize >= nSize && memInfo.State & MEM_COMMIT && !(memInfo.Protect & PAGE_NOACCESS);
}
CModule::CModule(const HMODULE pModule)
{
MODULEINFO mInfo {0};
if (pModule && pModule != INVALID_HANDLE_VALUE)
GetModuleInformation(GetCurrentProcess(), pModule, &mInfo, sizeof(MODULEINFO));
m_nModuleSize = static_cast<size_t>(mInfo.SizeOfImage);
m_pModuleBase = reinterpret_cast<uintptr_t>(mInfo.lpBaseOfDll);
m_nAddress = m_pModuleBase;
if (!m_nModuleSize || !m_pModuleBase)
return;
m_pDOSHeader = reinterpret_cast<IMAGE_DOS_HEADER*>(m_pModuleBase);
m_pNTHeaders = reinterpret_cast<IMAGE_NT_HEADERS64*>(m_pModuleBase + m_pDOSHeader->e_lfanew);
const IMAGE_SECTION_HEADER* hSection = IMAGE_FIRST_SECTION(m_pNTHeaders); // Get first image section.
for (WORD i = 0; i < m_pNTHeaders->FileHeader.NumberOfSections; i++) // Loop through the sections.
{
const IMAGE_SECTION_HEADER& hCurrentSection = hSection[i]; // Get current section.
ModuleSections_t moduleSection = ModuleSections_t(
std::string(reinterpret_cast<const char*>(hCurrentSection.Name)),
static_cast<uintptr_t>(m_pModuleBase + hCurrentSection.VirtualAddress),
hCurrentSection.SizeOfRawData);
if (!strcmp((const char*)hCurrentSection.Name, ".text"))
m_ExecutableCode = moduleSection;
else if (!strcmp((const char*)hCurrentSection.Name, ".pdata"))
m_ExceptionTable = moduleSection;
else if (!strcmp((const char*)hCurrentSection.Name, ".data"))
m_RunTimeData = moduleSection;
else if (!strcmp((const char*)hCurrentSection.Name, ".rdata"))
m_ReadOnlyData = moduleSection;
m_vModuleSections.push_back(moduleSection); // Push back a struct with the section data.
}
}
CModule::CModule(const char* pModuleName) : CModule(GetModuleHandleA(pModuleName)) {}
MemoryAddress CModule::GetExport(const char* pExportName)
{
return MemoryAddress(reinterpret_cast<uintptr_t>(GetProcAddress(reinterpret_cast<HMODULE>(m_nAddress), pExportName)));
}
MemoryAddress CModule::FindPattern(const uint8_t* pPattern, const char* pMask)
{
if (!m_ExecutableCode.IsSectionValid())
return MemoryAddress();
uint64_t nBase = static_cast<uint64_t>(m_ExecutableCode.m_pSectionBase);
uint64_t nSize = static_cast<uint64_t>(m_ExecutableCode.m_nSectionSize);
const uint8_t* pData = reinterpret_cast<uint8_t*>(nBase);
const uint8_t* pEnd = pData + static_cast<uint32_t>(nSize) - strlen(pMask);
int nMasks[64]; // 64*16 = enough masks for 1024 bytes.
int iNumMasks = static_cast<int>(ceil(static_cast<float>(strlen(pMask)) / 16.f));
memset(nMasks, '\0', iNumMasks * sizeof(int));
for (intptr_t i = 0; i < iNumMasks; ++i)
{
for (intptr_t j = strnlen(pMask + i * 16, 16) - 1; j >= 0; --j)
{
if (pMask[i * 16 + j] == 'x')
{
_bittestandset(reinterpret_cast<LONG*>(&nMasks[i]), j);
}
}
}
__m128i xmm1 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(pPattern));
__m128i xmm2, xmm3, msks;
for (; pData != pEnd; _mm_prefetch(reinterpret_cast<const char*>(++pData + 64), _MM_HINT_NTA))
{
if (pPattern[0] == pData[0])
{
xmm2 = _mm_loadu_si128(reinterpret_cast<const __m128i*>(pData));
msks = _mm_cmpeq_epi8(xmm1, xmm2);
if ((_mm_movemask_epi8(msks) & nMasks[0]) == nMasks[0])
{
for (uintptr_t i = 1; i < static_cast<uintptr_t>(iNumMasks); ++i)
{
xmm2 = _mm_loadu_si128(reinterpret_cast<const __m128i*>((pData + i * 16)));
xmm3 = _mm_loadu_si128(reinterpret_cast<const __m128i*>((pPattern + i * 16)));
msks = _mm_cmpeq_epi8(xmm2, xmm3);
if ((_mm_movemask_epi8(msks) & nMasks[i]) == nMasks[i])
{
if ((i + 1) == iNumMasks)
{
return MemoryAddress(const_cast<uint8_t*>(pData));
}
}
else
goto CONTINUE;
}
return MemoryAddress((&*(const_cast<uint8_t*>(pData))));
}
}
CONTINUE:;
}
return MemoryAddress();
}
inline std::pair<std::vector<uint8_t>, std::string> MaskedBytesFromPattern(const char* pPatternString)
{
std::vector<uint8_t> vRet;
std::string sMask;
int size = strlen(pPatternString);
for (int i = 0; i < size; i++)
{
// If this is a space character, ignore it
if (isspace(pPatternString[i]))
continue;
if (pPatternString[i] == '?')
{
// Add a wildcard
vRet.push_back(0);
sMask.append("?");
}
else if (i < size - 1)
{
BYTE result = 0;
for (int j = 0; j < 2; j++)
{
int val = 0;
char c = *(pPatternString + i + j);
if (c >= 'a')
{
val = c - 'a' + 0xA;
}
else if (c >= 'A')
{
val = c - 'A' + 0xA;
}
else if (isdigit(c))
{
val = c - '0';
}
else
{
assert(false, "Failed to parse invalid pattern string.");
val = -1;
}
result += (j == 0) ? val * 16 : val;
}
vRet.push_back(result);
sMask.append("x");
}
i++;
}
return std::make_pair(vRet, sMask);
}
MemoryAddress CModule::FindPattern(const char* pPattern)
{
const auto pattern = MaskedBytesFromPattern(pPattern);
return FindPattern(pattern.first.data(), pattern.second.c_str());
}

90
NorthstarDLL/memory.h Normal file
View File

@ -0,0 +1,90 @@
#pragma once
class MemoryAddress
{
public:
uintptr_t m_nAddress;
public:
MemoryAddress();
MemoryAddress(const uintptr_t nAddress);
MemoryAddress(const void* pAddress);
// operators
operator uintptr_t() const;
operator void*() const;
operator bool() const;
bool operator==(const MemoryAddress& other) const;
bool operator!=(const MemoryAddress& other) const;
bool operator==(const uintptr_t& addr) const;
bool operator!=(const uintptr_t& addr) const;
MemoryAddress operator+(const MemoryAddress& other) const;
MemoryAddress operator-(const MemoryAddress& other) const;
MemoryAddress operator+(const uintptr_t& other) const;
MemoryAddress operator-(const uintptr_t& other) const;
MemoryAddress operator*() const;
template <typename T> T As()
{
return reinterpret_cast<T>(m_nAddress);
}
// traversal
MemoryAddress Offset(const uintptr_t nOffset) const;
MemoryAddress Deref(const int nNumDerefs = 1) const;
// patching
void Patch(const uint8_t* pBytes, const size_t nSize);
void Patch(const std::initializer_list<uint8_t> bytes);
void Patch(const char* pBytes);
void NOP(const size_t nSize);
bool IsMemoryReadable(const size_t nSize);
};
// based on https://github.com/Mauler125/r5sdk/blob/master/r5dev/public/include/module.h
class CModule : public MemoryAddress
{
public:
struct ModuleSections_t
{
ModuleSections_t(void) = default;
ModuleSections_t(const std::string& svSectionName, uintptr_t pSectionBase, size_t nSectionSize)
: m_svSectionName(svSectionName), m_pSectionBase(pSectionBase), m_nSectionSize(nSectionSize)
{
}
bool IsSectionValid(void) const
{
return m_nSectionSize != 0;
}
std::string m_svSectionName; // Name of section.
uintptr_t m_pSectionBase {}; // Start address of section.
size_t m_nSectionSize {}; // Size of section.
};
ModuleSections_t m_ExecutableCode;
ModuleSections_t m_ExceptionTable;
ModuleSections_t m_RunTimeData;
ModuleSections_t m_ReadOnlyData;
private:
std::string m_svModuleName;
uintptr_t m_pModuleBase {};
DWORD m_nModuleSize {};
IMAGE_NT_HEADERS64* m_pNTHeaders = nullptr;
IMAGE_DOS_HEADER* m_pDOSHeader = nullptr;
std::vector<ModuleSections_t> m_vModuleSections;
public:
CModule() = delete; // no default, we need a module name
CModule(const HMODULE pModule);
CModule(const char* pModuleName);
MemoryAddress GetExport(const char* pExportName);
MemoryAddress FindPattern(const uint8_t* pPattern, const char* pMask);
MemoryAddress FindPattern(const char* pPattern);
};

View File

@ -1,49 +0,0 @@
#include "pch.h"
#include "miscclientfixes.h"
#include "hookutils.h"
#include "dedicated.h"
typedef void* (*CrashingWeaponActivityFuncType)(void* a1);
CrashingWeaponActivityFuncType CrashingWeaponActivityFunc0;
CrashingWeaponActivityFuncType CrashingWeaponActivityFunc1;
void* CrashingWeaponActivityFunc0Hook(void* a1)
{
// this return is safe, other functions that use this value seemingly dont care about it being null
if (!a1)
return 0;
return CrashingWeaponActivityFunc0(a1);
}
void* CrashingWeaponActivityFunc1Hook(void* a1)
{
// this return is safe, other functions that use this value seemingly dont care about it being null
if (!a1)
return 0;
return CrashingWeaponActivityFunc1(a1);
}
void InitialiseMiscClientFixes(HMODULE baseAddress)
{
if (IsDedicatedServer())
return;
HookEnabler hook;
// these functions will occasionally pass a null pointer on respawn, unsure what causes this but seems easiest just to return null if
// null, which seems to work fine fucking sucks this has to be fixed like this but unsure what exactly causes this serverside, breaks
// vanilla compatibility to a degree tho will say i have about 0 clue what exactly these functions do, testing this it doesn't even seem
// like they do much of anything i can see tbh
ENABLER_CREATEHOOK(
hook, (char*)baseAddress + 0x5A92D0, &CrashingWeaponActivityFunc0Hook, reinterpret_cast<LPVOID*>(&CrashingWeaponActivityFunc0));
ENABLER_CREATEHOOK(
hook, (char*)baseAddress + 0x5A9310, &CrashingWeaponActivityFunc1Hook, reinterpret_cast<LPVOID*>(&CrashingWeaponActivityFunc1));
// experimental: allow cl_extrapolate to be enabled without cheats
{
void* ptr = (char*)baseAddress + 0x275F9D9;
*((char*)ptr) = (char)0;
}
}

View File

@ -1,2 +0,0 @@
#pragma once
void InitialiseMiscClientFixes(HMODULE baseAddress);

View File

@ -1,60 +1,162 @@
#include "pch.h"
#include "misccommands.h"
#include "concommand.h"
#include "gameutils.h"
#include "playlist.h"
#include "r2engine.h"
#include "r2client.h"
#include "tier0.h"
#include "hoststate.h"
#include "masterserver.h"
#include "modmanager.h"
#include "serverauthentication.h"
#include "squirrel.h"
void ConCommand_force_newgame(const CCommand& arg)
{
if (arg.ArgC() < 2)
return;
R2::g_pHostState->m_iNextState = R2::HostState_t::HS_NEW_GAME;
strncpy(R2::g_pHostState->m_levelName, arg.Arg(1), sizeof(R2::g_pHostState->m_levelName));
}
void ConCommand_ns_start_reauth_and_leave_to_lobby(const CCommand& arg)
{
// hack for special case where we're on a local server, so we erase our own newly created auth data on disconnect
g_pMasterServerManager->m_bNewgameAfterSelfAuth = true;
g_pMasterServerManager->AuthenticateWithOwnServer(R2::g_pLocalPlayerUserID, g_pMasterServerManager->m_sOwnClientAuthToken);
}
void ConCommand_ns_end_reauth_and_leave_to_lobby(const CCommand& arg)
{
if (g_pServerAuthentication->m_RemoteAuthenticationData.size())
R2::g_pCVar->FindVar("serverfilter")->SetValue(g_pServerAuthentication->m_RemoteAuthenticationData.begin()->first.c_str());
// weird way of checking, but check if client script vm is initialised, mainly just to allow players to cancel this
if (g_pSquirrel<ScriptContext::CLIENT>->m_pSQVM)
{
g_pServerAuthentication->m_bNeedLocalAuthForNewgame = true;
// this won't set playlist correctly on remote clients, don't think they can set playlist until they've left which sorta
// fucks things should maybe set this in HostState_NewGame?
R2::SetCurrentPlaylist("tdm");
strcpy(R2::g_pHostState->m_levelName, "mp_lobby");
R2::g_pHostState->m_iNextState = R2::HostState_t::HS_NEW_GAME;
}
}
void AddMiscConCommands()
{
MAKE_CONCMD(
RegisterConCommand(
"force_newgame",
ConCommand_force_newgame,
"forces a map load through directly setting g_pHostState->m_iNextState to HS_NEW_GAME",
FCVAR_NONE,
[](const CCommand& arg)
{
if (arg.ArgC() < 2)
return;
FCVAR_NONE);
g_pHostState->m_iNextState = HS_NEW_GAME;
strncpy(g_pHostState->m_levelName, arg.Arg(1), sizeof(g_pHostState->m_levelName));
});
MAKE_CONCMD(
RegisterConCommand(
"ns_start_reauth_and_leave_to_lobby",
ConCommand_ns_start_reauth_and_leave_to_lobby,
"called by the server, used to reauth and return the player to lobby when leaving a game",
FCVAR_SERVER_CAN_EXECUTE,
[](const CCommand& arg)
{
// hack for special case where we're on a local server, so we erase our own newly created auth data on disconnect
g_MasterServerManager->m_bNewgameAfterSelfAuth = true;
g_MasterServerManager->AuthenticateWithOwnServer(g_LocalPlayerUserID, g_MasterServerManager->m_sOwnClientAuthToken);
});
FCVAR_SERVER_CAN_EXECUTE);
// this is a concommand because we make a deferred call to it from another thread
MAKE_CONCMD(
"ns_end_reauth_and_leave_to_lobby",
"",
FCVAR_NONE,
[](const CCommand& arg)
{
Cbuf_AddText(
Cbuf_GetCurrentPlayer(),
fmt::format("serverfilter {}", g_ServerAuthenticationManager->m_authData.begin()->first).c_str(),
cmd_source_t::kCommandSrcCode);
Cbuf_Execute();
// weird way of checking, but check if client script vm is initialised, mainly just to allow players to cancel this
if (g_ClientSquirrelManager->sqvm)
{
g_ServerAuthenticationManager->m_bNeedLocalAuthForNewgame = true;
// this won't set playlist correctly on remote clients, don't think they can set playlist until they've left which sorta
// fucks things should maybe set this in HostState_NewGame?
SetCurrentPlaylist("tdm");
strcpy(g_pHostState->m_levelName, "mp_lobby");
g_pHostState->m_iNextState = HS_NEW_GAME;
}
});
RegisterConCommand("ns_end_reauth_and_leave_to_lobby", ConCommand_ns_end_reauth_and_leave_to_lobby, "", FCVAR_NONE);
}
// fixes up various cvar flags to have more sane values
void FixupCvarFlags()
{
if (Tier0::CommandLine()->CheckParm("-allowdevcvars"))
{
// strip hidden and devonly cvar flags
int iNumCvarsAltered = 0;
for (auto& pair : R2::g_pCVar->DumpToMap())
{
// strip flags
int flags = pair.second->GetFlags();
if (flags & FCVAR_DEVELOPMENTONLY)
{
flags &= ~FCVAR_DEVELOPMENTONLY;
iNumCvarsAltered++;
}
if (flags & FCVAR_HIDDEN)
{
flags &= ~FCVAR_HIDDEN;
iNumCvarsAltered++;
}
pair.second->m_nFlags = flags;
}
spdlog::info("Removed {} hidden/devonly cvar flags", iNumCvarsAltered);
}
// make all engine client commands FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS
// these are usually checked through CGameClient::IsEngineClientCommand, but we get more control over this if we just do it through
// cvar flags
const char** ppEngineClientCommands = CModule("engine.dll").Offset(0x7C5EF0).As<const char**>();
int i = 0;
do
{
ConCommandBase* pCommand = R2::g_pCVar->FindCommandBase(ppEngineClientCommands[i]);
if (pCommand) // not all the commands in this array actually exist in respawn source
pCommand->m_nFlags |= FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS;
} while (ppEngineClientCommands[++i]);
// array of cvars and the flags we want to add to them
const std::vector<std::tuple<const char*, uint32_t>> CVAR_FIXUP_ADD_FLAGS = {
// system commands (i.e. necessary for proper functionality)
// servers need to be able to disconnect
{"disconnect", FCVAR_SERVER_CAN_EXECUTE},
// cheat commands
{"give", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"give_server", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"givecurrentammo", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"takecurrentammo", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"switchclass", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"set", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"_setClassVarServer", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"ent_create", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"ent_throw", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"ent_setname", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"ent_teleport", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"ent_remove", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"ent_remove_all", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"ent_fire", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"particle_create", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"particle_recreate", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"particle_kill", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"test_setteam", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"melee_lunge_ent", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}};
// array of cvars and the flags we want to remove from them
const std::vector<std::tuple<const char*, uint32_t>> CVAR_FIXUP_REMOVE_FLAGS = {
// unsure how this command works, not even sure it's used on retail servers, deffo shouldn't be used on northstar
{"migrateme", FCVAR_SERVER_CAN_EXECUTE | FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"recheck", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}, // we don't need this on northstar servers, it's for communities
// unsure how these work exactly (rpt system likely somewhat stripped?), removing anyway since they won't be used
{"rpt_client_enable", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS},
{"rpt_password", FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS}};
for (auto& fixup : CVAR_FIXUP_ADD_FLAGS)
{
ConCommandBase* command = R2::g_pCVar->FindCommandBase(std::get<0>(fixup));
if (command)
command->m_nFlags |= std::get<1>(fixup);
}
for (auto& fixup : CVAR_FIXUP_REMOVE_FLAGS)
{
ConCommandBase* command = R2::g_pCVar->FindCommandBase(std::get<0>(fixup));
if (command)
command->m_nFlags &= ~std::get<1>(fixup);
}
}

View File

@ -1,2 +1,3 @@
#pragma once
void AddMiscConCommands();
void FixupCvarFlags();

View File

@ -1,26 +1,7 @@
#include "pch.h"
#include "miscserverfixes.h"
#include "hookutils.h"
#include "nsmem.h"
void InitialiseMiscServerFixes(HMODULE baseAddress)
ON_DLL_LOAD("server.dll", MiscServerFixes, (CModule module))
{
uintptr_t ba = (uintptr_t)baseAddress;
// ret at the start of the concommand GenerateObjFile as it can crash servers
{
NSMem::BytePatch(ba + 0x38D920, "C3");
}
// nop out call to VGUI shutdown since it crashes the game when quitting from the console
{
NSMem::NOP(ba + 0x154A96, 5);
}
// ret at the start of CServerGameClients::ClientCommandKeyValues as it has no benefit and is forwarded to client (i.e. security issue)
// this prevents the attack vector of client=>server=>client, however server=>client also has clientside patches
{
NSMem::BytePatch(ba + 0x153920, "C3");
}
module.Offset(0x154A96).NOP(5);
}

View File

@ -1 +0,0 @@
void InitialiseMiscServerFixes(HMODULE baseAddress);

View File

@ -1,76 +1,73 @@
#include "pch.h"
#include "miscserverscript.h"
#include "squirrel.h"
#include "masterserver.h"
#include "serverauthentication.h"
#include "gameutils.h"
#include "dedicated.h"
#include "r2client.h"
#include "r2server.h"
// annoying helper function because i can't figure out getting players or entities from sqvm rn
// wish i didn't have to do it like this, but here we are
void* GetPlayerByIndex(int playerIndex)
#include <filesystem>
// void function NSEarlyWritePlayerPersistenceForLeave( entity player )
SQRESULT SQ_EarlyWritePlayerPersistenceForLeave(HSquirrelVM* sqvm)
{
const int PLAYER_ARRAY_OFFSET = 0x12A53F90;
const int PLAYER_SIZE = 0x2D728;
void* playerArrayBase = (char*)GetModuleHandleA("engine.dll") + PLAYER_ARRAY_OFFSET;
void* player = (char*)playerArrayBase + (playerIndex * PLAYER_SIZE);
return player;
}
// void function NSEarlyWritePlayerIndexPersistenceForLeave( int playerIndex )
SQRESULT SQ_EarlyWritePlayerIndexPersistenceForLeave(void* sqvm)
{
int playerIndex = ServerSq_getinteger(sqvm, 1);
void* player = GetPlayerByIndex(playerIndex);
if (!g_ServerAuthenticationManager->m_additionalPlayerData.count(player))
const R2::CBasePlayer* pPlayer = g_pSquirrel<ScriptContext::SERVER>->getentity<R2::CBasePlayer>(sqvm, 1);
if (!pPlayer)
{
ServerSq_pusherror(sqvm, fmt::format("Invalid playerindex {}", playerIndex).c_str());
return SQRESULT_ERROR;
spdlog::warn("NSEarlyWritePlayerPersistenceForLeave got null player");
g_pSquirrel<ScriptContext::SERVER>->pushbool(sqvm, false);
return SQRESULT_NOTNULL;
}
g_ServerAuthenticationManager->m_additionalPlayerData[player].needPersistenceWriteOnLeave = false;
g_ServerAuthenticationManager->WritePersistentData(player);
R2::CBaseClient* pClient = &R2::g_pClientArray[pPlayer->m_nPlayerIndex];
if (g_pServerAuthentication->m_PlayerAuthenticationData.find(pClient) == g_pServerAuthentication->m_PlayerAuthenticationData.end())
{
g_pSquirrel<ScriptContext::SERVER>->pushbool(sqvm, false);
return SQRESULT_NOTNULL;
}
g_pServerAuthentication->m_PlayerAuthenticationData[pClient].needPersistenceWriteOnLeave = false;
g_pServerAuthentication->WritePersistentData(pClient);
return SQRESULT_NULL;
}
// bool function NSIsWritingPlayerPersistence()
SQRESULT SQ_IsWritingPlayerPersistence(void* sqvm)
SQRESULT SQ_IsWritingPlayerPersistence(HSquirrelVM* sqvm)
{
ServerSq_pushbool(sqvm, g_MasterServerManager->m_bSavingPersistentData);
g_pSquirrel<ScriptContext::SERVER>->pushbool(sqvm, g_pMasterServerManager->m_bSavingPersistentData);
return SQRESULT_NOTNULL;
}
// bool function NSIsPlayerIndexLocalPlayer( int playerIndex )
SQRESULT SQ_IsPlayerIndexLocalPlayer(void* sqvm)
// bool function NSIsPlayerLocalPlayer( entity player )
SQRESULT SQ_IsPlayerLocalPlayer(HSquirrelVM* sqvm)
{
int playerIndex = ServerSq_getinteger(sqvm, 1);
void* player = GetPlayerByIndex(playerIndex);
if (!g_ServerAuthenticationManager->m_additionalPlayerData.count(player))
const R2::CBasePlayer* pPlayer = g_pSquirrel<ScriptContext::SERVER>->getentity<R2::CBasePlayer>(sqvm, 1);
if (!pPlayer)
{
ServerSq_pusherror(sqvm, fmt::format("Invalid playerindex {}", playerIndex).c_str());
return SQRESULT_ERROR;
spdlog::warn("NSIsPlayerLocalPlayer got null player");
g_pSquirrel<ScriptContext::SERVER>->pushbool(sqvm, false);
return SQRESULT_NOTNULL;
}
ServerSq_pushbool(sqvm, !strcmp(g_LocalPlayerUserID, (char*)player + 0xF500));
R2::CBaseClient* pClient = &R2::g_pClientArray[pPlayer->m_nPlayerIndex];
g_pSquirrel<ScriptContext::SERVER>->pushbool(sqvm, !strcmp(R2::g_pLocalPlayerUserID, pClient->m_UID));
return SQRESULT_NOTNULL;
}
// bool function NSIsDedicated()
SQRESULT SQ_IsDedicated(void* sqvm)
SQRESULT SQ_IsDedicated(HSquirrelVM* sqvm)
{
ServerSq_pushbool(sqvm, IsDedicatedServer());
g_pSquirrel<ScriptContext::SERVER>->pushbool(sqvm, IsDedicatedServer());
return SQRESULT_NOTNULL;
}
void InitialiseMiscServerScriptCommand(HMODULE baseAddress)
ON_DLL_LOAD_RELIESON("server.dll", MiscServerScriptCommands, ServerSquirrel, (CModule module))
{
g_ServerSquirrelManager->AddFuncRegistration(
"void", "NSEarlyWritePlayerIndexPersistenceForLeave", "int playerIndex", "", SQ_EarlyWritePlayerIndexPersistenceForLeave);
g_ServerSquirrelManager->AddFuncRegistration("bool", "NSIsWritingPlayerPersistence", "", "", SQ_IsWritingPlayerPersistence);
g_ServerSquirrelManager->AddFuncRegistration("bool", "NSIsPlayerIndexLocalPlayer", "int playerIndex", "", SQ_IsPlayerIndexLocalPlayer);
g_ServerSquirrelManager->AddFuncRegistration("bool", "NSIsDedicated", "", "", SQ_IsDedicated);
g_pSquirrel<ScriptContext::SERVER>->AddFuncRegistration(
"void", "NSEarlyWritePlayerPersistenceForLeave", "entity player", "", SQ_EarlyWritePlayerPersistenceForLeave);
g_pSquirrel<ScriptContext::SERVER>->AddFuncRegistration("bool", "NSIsWritingPlayerPersistence", "", "", SQ_IsWritingPlayerPersistence);
g_pSquirrel<ScriptContext::SERVER>->AddFuncRegistration("bool", "NSIsPlayerLocalPlayer", "entity player", "", SQ_IsPlayerLocalPlayer);
g_pSquirrel<ScriptContext::SERVER>->AddFuncRegistration("bool", "NSIsDedicated", "", "", SQ_IsDedicated);
}

View File

@ -1,2 +0,0 @@
void InitialiseMiscServerScriptCommand(HMODULE baseAddress);
void* GetPlayerByIndex(int playerIndex);

View File

@ -1,37 +1,35 @@
#include "pch.h"
#include "modlocalisation.h"
#include "hookutils.h"
#include "modmanager.h"
typedef bool (*AddLocalisationFileType)(void* g_pVguiLocalize, const char* path, const char* pathId, char unknown);
AddLocalisationFileType AddLocalisationFile;
AUTOHOOK_INIT()
bool loadModLocalisationFiles = true;
bool AddLocalisationFileHook(void* g_pVguiLocalize, const char* path, const char* pathId, char unknown)
// clang-format off
AUTOHOOK(AddLocalisationFile, localize.dll + 0x6D80,
bool, __fastcall, (void* pVguiLocalize, const char* path, const char* pathId, char unknown))
// clang-format on
{
bool ret = AddLocalisationFile(g_pVguiLocalize, path, pathId, unknown);
static bool bLoadModLocalisationFiles = true;
bool ret = AddLocalisationFile(pVguiLocalize, path, pathId, unknown);
if (ret)
spdlog::info("Loaded localisation file {} successfully", path);
if (!loadModLocalisationFiles)
if (!bLoadModLocalisationFiles)
return ret;
loadModLocalisationFiles = false;
bLoadModLocalisationFiles = false;
for (Mod mod : g_ModManager->m_loadedMods)
if (mod.Enabled)
for (Mod mod : g_pModManager->m_LoadedMods)
if (mod.m_bEnabled)
for (std::string& localisationFile : mod.LocalisationFiles)
AddLocalisationFile(g_pVguiLocalize, localisationFile.c_str(), pathId, unknown);
AddLocalisationFile(pVguiLocalize, localisationFile.c_str(), pathId, unknown);
loadModLocalisationFiles = true;
bLoadModLocalisationFiles = true;
return ret;
}
void InitialiseModLocalisation(HMODULE baseAddress)
ON_DLL_LOAD_CLIENT("localize.dll", Localize, (CModule module))
{
HookEnabler hook;
ENABLER_CREATEHOOK(hook, (char*)baseAddress + 0x6D80, AddLocalisationFileHook, reinterpret_cast<LPVOID*>(&AddLocalisationFile));
AUTOHOOK_DISPATCH()
}

View File

@ -1,3 +0,0 @@
#pragma once
void InitialiseModLocalisation(HMODULE baseAddress);

View File

@ -4,6 +4,10 @@
#include "concommand.h"
#include "audio.h"
#include "masterserver.h"
#include "filesystem.h"
#include "rpakfilesystem.h"
#include "nsprefix.h"
#include "rapidjson/error/en.h"
#include "rapidjson/document.h"
#include "rapidjson/ostreamwrapper.h"
@ -13,17 +17,14 @@
#include <string>
#include <sstream>
#include <vector>
#include "filesystem.h"
#include "rpakfilesystem.h"
#include "nsprefix.h"
ModManager* g_ModManager;
ModManager* g_pModManager;
Mod::Mod(fs::path modDir, char* jsonBuf)
{
wasReadSuccessfully = false;
m_bWasReadSuccessfully = false;
ModDirectory = modDir;
m_ModDirectory = modDir;
rapidjson_document modJson;
modJson.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>(jsonBuf);
@ -106,11 +107,55 @@ Mod::Mod(fs::path modDir, char* jsonBuf)
else
convar->HelpString = "";
// todo: could possibly parse FCVAR names here instead, would be easier
convar->Flags = FCVAR_NONE;
if (convarObj.HasMember("Flags"))
convar->Flags = convarObj["Flags"].GetInt();
else
convar->Flags = FCVAR_NONE;
{
// read raw integer flags
if (convarObj["Flags"].IsInt())
convar->Flags = convarObj["Flags"].GetInt();
else if (convarObj["Flags"].IsString())
{
// parse cvar flags from string
// example string: ARCHIVE_PLAYERPROFILE | GAMEDLL
std::string sFlags = convarObj["Flags"].GetString();
sFlags += '|'; // add additional | so we register the last flag
std::string sCurrentFlag;
for (int i = 0; i < sFlags.length(); i++)
{
if (isspace(sFlags[i]))
continue;
// if we encounter a |, add current string as a flag
if (sFlags[i] == '|')
{
bool bHasFlags = false;
int iCurrentFlags;
for (auto& flagPair : g_PrintCommandFlags)
{
if (!sCurrentFlag.compare(flagPair.second))
{
iCurrentFlags = flagPair.first;
bHasFlags = true;
break;
}
}
if (bHasFlags)
convar->Flags |= iCurrentFlags;
else
spdlog::warn("Mod ConVar {} has unknown flag {}", convar->Name, sCurrentFlag);
sCurrentFlag = "";
}
else
sCurrentFlag += sFlags[i];
}
}
}
ConVars.push_back(convar);
}
@ -127,7 +172,7 @@ Mod::Mod(fs::path modDir, char* jsonBuf)
ModScript script;
script.Path = scriptObj["Path"].GetString();
script.RsonRunOn = scriptObj["RunOn"].GetString();
script.RunOn = scriptObj["RunOn"].GetString();
if (scriptObj.HasMember("ServerCallback") && scriptObj["ServerCallback"].IsObject())
{
@ -206,7 +251,7 @@ Mod::Mod(fs::path modDir, char* jsonBuf)
}
}
wasReadSuccessfully = true;
m_bWasReadSuccessfully = true;
}
ModManager::ModManager()
@ -223,7 +268,7 @@ ModManager::ModManager()
void ModManager::LoadMods()
{
if (m_hasLoadedMods)
if (m_bHasLoadedMods)
UnloadMods();
std::vector<fs::path> modDirs;
@ -232,7 +277,7 @@ void ModManager::LoadMods()
fs::remove_all(GetCompiledAssetsPath());
fs::create_directories(GetModFolderPath());
DependencyConstants.clear();
m_DependencyConstants.clear();
// read enabled mods cfg
std::ifstream enabledModsStream(GetNorthstarPrefix() + "/enabledmods.json");
@ -244,10 +289,10 @@ void ModManager::LoadMods()
enabledModsStringStream << (char)enabledModsStream.get();
enabledModsStream.close();
m_enabledModsCfg.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>(
m_EnabledModsCfg.Parse<rapidjson::ParseFlag::kParseCommentsFlag | rapidjson::ParseFlag::kParseTrailingCommasFlag>(
enabledModsStringStream.str().c_str());
m_hasEnabledModsCfg = m_enabledModsCfg.IsObject();
m_bHasEnabledModsCfg = m_EnabledModsCfg.IsObject();
}
// get mod directories
@ -277,41 +322,41 @@ void ModManager::LoadMods()
for (auto& pair : mod.DependencyConstants)
{
if (DependencyConstants.find(pair.first) != DependencyConstants.end() && DependencyConstants[pair.first] != pair.second)
if (m_DependencyConstants.find(pair.first) != m_DependencyConstants.end() && m_DependencyConstants[pair.first] != pair.second)
{
spdlog::error("Constant {} in mod {} already exists in another mod.", pair.first, mod.Name);
mod.wasReadSuccessfully = false;
mod.m_bWasReadSuccessfully = false;
break;
}
if (DependencyConstants.find(pair.first) == DependencyConstants.end())
DependencyConstants.emplace(pair);
if (m_DependencyConstants.find(pair.first) == m_DependencyConstants.end())
m_DependencyConstants.emplace(pair);
}
if (m_hasEnabledModsCfg && m_enabledModsCfg.HasMember(mod.Name.c_str()))
mod.Enabled = m_enabledModsCfg[mod.Name.c_str()].IsTrue();
if (m_bHasEnabledModsCfg && m_EnabledModsCfg.HasMember(mod.Name.c_str()))
mod.m_bEnabled = m_EnabledModsCfg[mod.Name.c_str()].IsTrue();
else
mod.Enabled = true;
mod.m_bEnabled = true;
if (mod.wasReadSuccessfully)
if (mod.m_bWasReadSuccessfully)
{
spdlog::info("Loaded mod {} successfully", mod.Name);
if (mod.Enabled)
if (mod.m_bEnabled)
spdlog::info("Mod {} is enabled", mod.Name);
else
spdlog::info("Mod {} is disabled", mod.Name);
m_loadedMods.push_back(mod);
m_LoadedMods.push_back(mod);
}
else
spdlog::warn("Skipping loading mod file {}", (modDir / "mod.json").string());
}
// sort by load prio, lowest-highest
std::sort(m_loadedMods.begin(), m_loadedMods.end(), [](Mod& a, Mod& b) { return a.LoadPriority < b.LoadPriority; });
std::sort(m_LoadedMods.begin(), m_LoadedMods.end(), [](Mod& a, Mod& b) { return a.LoadPriority < b.LoadPriority; });
for (Mod& mod : m_loadedMods)
for (Mod& mod : m_LoadedMods)
{
if (!mod.Enabled)
if (!mod.m_bEnabled)
continue;
// register convars
@ -319,15 +364,15 @@ void ModManager::LoadMods()
// preexisting convars note: we don't delete convars if they already exist because they're used for script stuff, unfortunately this
// causes us to leak memory on reload, but not much, potentially find a way to not do this at some point
for (ModConVar* convar : mod.ConVars)
if (!g_pCVar->FindVar(convar->Name.c_str())) // make sure convar isn't registered yet, unsure if necessary but idk what
// behaviour is for defining same convar multiple times
if (!R2::g_pCVar->FindVar(convar->Name.c_str())) // make sure convar isn't registered yet, unsure if necessary but idk what
// behaviour is for defining same convar multiple times
new ConVar(convar->Name.c_str(), convar->DefaultValue.c_str(), convar->Flags, convar->HelpString.c_str());
// read vpk paths
if (fs::exists(mod.ModDirectory / "vpk"))
if (fs::exists(mod.m_ModDirectory / "vpk"))
{
// read vpk cfg
std::ifstream vpkJsonStream(mod.ModDirectory / "vpk/vpk.json");
std::ifstream vpkJsonStream(mod.m_ModDirectory / "vpk/vpk.json");
std::stringstream vpkJsonStringStream;
bool bUseVPKJson = false;
@ -345,7 +390,7 @@ void ModManager::LoadMods()
bUseVPKJson = !dVpkJson.HasParseError() && dVpkJson.IsObject();
}
for (fs::directory_entry file : fs::directory_iterator(mod.ModDirectory / "vpk"))
for (fs::directory_entry file : fs::directory_iterator(mod.m_ModDirectory / "vpk"))
{
// a bunch of checks to make sure we're only adding dir vpks and their paths are good
// note: the game will literally only load vpks with the english prefix
@ -356,25 +401,24 @@ void ModManager::LoadMods()
std::string formattedPath = file.path().filename().string();
// this really fucking sucks but it'll work
std::string vpkName =
(file.path().parent_path() / formattedPath.substr(strlen("english"), formattedPath.find(".bsp") - 3)).string();
std::string vpkName = formattedPath.substr(strlen("english"), formattedPath.find(".bsp") - 3);
ModVPKEntry& modVpk = mod.Vpks.emplace_back();
modVpk.m_bAutoLoad = !bUseVPKJson || (dVpkJson.HasMember("Preload") && dVpkJson["Preload"].IsObject() &&
dVpkJson["Preload"].HasMember(vpkName) && dVpkJson["Preload"][vpkName].IsTrue());
modVpk.m_sVpkPath = vpkName;
modVpk.m_sVpkPath = (file.path().parent_path() / vpkName).string();
if (m_hasLoadedMods && modVpk.m_bAutoLoad)
(*g_Filesystem)->m_vtable->MountVPK(*g_Filesystem, vpkName.c_str());
if (m_bHasLoadedMods && modVpk.m_bAutoLoad)
(*R2::g_pFilesystem)->m_vtable->MountVPK(*R2::g_pFilesystem, vpkName.c_str());
}
}
}
// read rpak paths
if (fs::exists(mod.ModDirectory / "paks"))
if (fs::exists(mod.m_ModDirectory / "paks"))
{
// read rpak cfg
std::ifstream rpakJsonStream(mod.ModDirectory / "paks/rpak.json");
std::ifstream rpakJsonStream(mod.m_ModDirectory / "paks/rpak.json");
std::stringstream rpakJsonStringStream;
bool bUseRpakJson = false;
@ -406,7 +450,7 @@ void ModManager::LoadMods()
}
}
for (fs::directory_entry file : fs::directory_iterator(mod.ModDirectory / "paks"))
for (fs::directory_entry file : fs::directory_iterator(mod.m_ModDirectory / "paks"))
{
// ensure we're only loading rpaks
if (fs::is_regular_file(file) && file.path().extension() == ".rpak")
@ -417,39 +461,39 @@ void ModManager::LoadMods()
modPak.m_bAutoLoad =
!bUseRpakJson || (dRpakJson.HasMember("Preload") && dRpakJson["Preload"].IsObject() &&
dRpakJson["Preload"].HasMember(pakName) && dRpakJson["Preload"][pakName].IsTrue());
// postload things
if (!bUseRpakJson ||
(dRpakJson.HasMember("Postload") && dRpakJson["Postload"].IsObject() && dRpakJson["Postload"].HasMember(pakName)))
{
modPak.m_sLoadAfterPak = dRpakJson["Postload"][pakName].GetString();
}
modPak.m_sPakName = pakName;
// not using atm because we need to resolve path to rpak
// if (m_hasLoadedMods && modPak.m_bAutoLoad)
// g_PakLoadManager->LoadPakAsync(pakName.c_str());
// g_pPakLoadManager->LoadPakAsync(pakName.c_str());
}
}
}
// read keyvalues paths
if (fs::exists(mod.ModDirectory / "keyvalues"))
if (fs::exists(mod.m_ModDirectory / "keyvalues"))
{
for (fs::directory_entry file : fs::recursive_directory_iterator(mod.ModDirectory / "keyvalues"))
for (fs::directory_entry file : fs::recursive_directory_iterator(mod.m_ModDirectory / "keyvalues"))
{
if (fs::is_regular_file(file))
{
std::string kvStr = file.path().lexically_relative(mod.ModDirectory / "keyvalues").lexically_normal().string();
std::string kvStr =
g_pModManager->NormaliseModFilePath(file.path().lexically_relative(mod.m_ModDirectory / "keyvalues"));
mod.KeyValues.emplace(STR_HASH(kvStr), kvStr);
}
}
}
// read pdiff
if (fs::exists(mod.ModDirectory / "mod.pdiff"))
if (fs::exists(mod.m_ModDirectory / "mod.pdiff"))
{
std::ifstream pdiffStream(mod.ModDirectory / "mod.pdiff");
std::ifstream pdiffStream(mod.m_ModDirectory / "mod.pdiff");
if (!pdiffStream.fail())
{
@ -464,17 +508,17 @@ void ModManager::LoadMods()
}
// read bink video paths
if (fs::exists(mod.ModDirectory / "media"))
if (fs::exists(mod.m_ModDirectory / "media"))
{
for (fs::directory_entry file : fs::recursive_directory_iterator(mod.ModDirectory / "media"))
for (fs::directory_entry file : fs::recursive_directory_iterator(mod.m_ModDirectory / "media"))
if (fs::is_regular_file(file) && file.path().extension() == ".bik")
mod.BinkVideos.push_back(file.path().filename().string());
}
// try to load audio
if (fs::exists(mod.ModDirectory / "audio"))
if (fs::exists(mod.m_ModDirectory / "audio"))
{
for (fs::directory_entry file : fs::directory_iterator(mod.ModDirectory / "audio"))
for (fs::directory_entry file : fs::directory_iterator(mod.m_ModDirectory / "audio"))
{
if (fs::is_regular_file(file) && file.path().extension().string() == ".json")
{
@ -489,23 +533,23 @@ void ModManager::LoadMods()
}
// in a seperate loop because we register mod files in reverse order, since mods loaded later should have their files prioritised
for (int64_t i = m_loadedMods.size() - 1; i > -1; i--)
for (int64_t i = m_LoadedMods.size() - 1; i > -1; i--)
{
if (!m_loadedMods[i].Enabled)
if (!m_LoadedMods[i].m_bEnabled)
continue;
if (fs::exists(m_loadedMods[i].ModDirectory / MOD_OVERRIDE_DIR))
if (fs::exists(m_LoadedMods[i].m_ModDirectory / MOD_OVERRIDE_DIR))
{
for (fs::directory_entry file : fs::recursive_directory_iterator(m_loadedMods[i].ModDirectory / MOD_OVERRIDE_DIR))
for (fs::directory_entry file : fs::recursive_directory_iterator(m_LoadedMods[i].m_ModDirectory / MOD_OVERRIDE_DIR))
{
fs::path path = file.path().lexically_relative(m_loadedMods[i].ModDirectory / MOD_OVERRIDE_DIR).lexically_normal();
if (file.is_regular_file() && m_modFiles.find(path.string()) == m_modFiles.end())
std::string path =
g_pModManager->NormaliseModFilePath(file.path().lexically_relative(m_LoadedMods[i].m_ModDirectory / MOD_OVERRIDE_DIR));
if (file.is_regular_file() && m_ModFiles.find(path) == m_ModFiles.end())
{
ModOverrideFile modFile;
modFile.owningMod = &m_loadedMods[i];
modFile.path = path;
m_modFiles.insert(std::make_pair(path.string(), modFile));
modFile.m_pOwningMod = &m_LoadedMods[i];
modFile.m_Path = path;
m_ModFiles.insert(std::make_pair(path, modFile));
}
}
}
@ -517,9 +561,9 @@ void ModManager::LoadMods()
modinfoDoc.AddMember("Mods", rapidjson_document::GenericValue(rapidjson::kArrayType), modinfoDoc.GetAllocator());
int currentModIndex = 0;
for (Mod& mod : m_loadedMods)
for (Mod& mod : m_LoadedMods)
{
if (!mod.Enabled || (!mod.RequiredOnClient && !mod.Pdiff.size()))
if (!mod.m_bEnabled || (!mod.RequiredOnClient && !mod.Pdiff.size()))
continue;
modinfoDoc["Mods"].PushBack(rapidjson_document::GenericValue(rapidjson::kObjectType), modinfoDoc.GetAllocator());
@ -535,27 +579,27 @@ void ModManager::LoadMods()
buffer.Clear();
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
modinfoDoc.Accept(writer);
g_MasterServerManager->m_sOwnModInfoJson = std::string(buffer.GetString());
g_pMasterServerManager->m_sOwnModInfoJson = std::string(buffer.GetString());
m_hasLoadedMods = true;
m_bHasLoadedMods = true;
}
void ModManager::UnloadMods()
{
// clean up stuff from mods before we unload
m_modFiles.clear();
m_ModFiles.clear();
fs::remove_all(GetCompiledAssetsPath());
g_CustomAudioManager.ClearAudioOverrides();
if (!m_hasEnabledModsCfg)
m_enabledModsCfg.SetObject();
if (!m_bHasEnabledModsCfg)
m_EnabledModsCfg.SetObject();
for (Mod& mod : m_loadedMods)
for (Mod& mod : m_LoadedMods)
{
// remove all built kvs
for (std::pair<size_t, std::string> kvPaths : mod.KeyValues)
fs::remove(GetCompiledAssetsPath() / fs::path(kvPaths.second).lexically_relative(mod.ModDirectory));
fs::remove(GetCompiledAssetsPath() / fs::path(kvPaths.second).lexically_relative(mod.m_ModDirectory));
mod.KeyValues.clear();
@ -563,27 +607,39 @@ void ModManager::UnloadMods()
// should we be doing this here or should scripts be doing this manually?
// main issue with doing this here is when we reload mods for connecting to a server, we write enabled mods, which isn't necessarily
// what we wanna do
if (!m_enabledModsCfg.HasMember(mod.Name.c_str()))
m_enabledModsCfg.AddMember(
if (!m_EnabledModsCfg.HasMember(mod.Name.c_str()))
m_EnabledModsCfg.AddMember(
rapidjson_document::StringRefType(mod.Name.c_str()),
rapidjson_document::GenericValue(false),
m_enabledModsCfg.GetAllocator());
m_EnabledModsCfg.GetAllocator());
m_enabledModsCfg[mod.Name.c_str()].SetBool(mod.Enabled);
m_EnabledModsCfg[mod.Name.c_str()].SetBool(mod.m_bEnabled);
}
std::ofstream writeStream(GetNorthstarPrefix() + "/enabledmods.json");
rapidjson::OStreamWrapper writeStreamWrapper(writeStream);
rapidjson::Writer<rapidjson::OStreamWrapper> writer(writeStreamWrapper);
m_enabledModsCfg.Accept(writer);
m_EnabledModsCfg.Accept(writer);
// do we need to dealloc individual entries in m_loadedMods? idk, rework
m_loadedMods.clear();
m_LoadedMods.clear();
}
std::string ModManager::NormaliseModFilePath(const fs::path path)
{
std::string str = path.lexically_normal().string();
// force to lowercase
for (char& c : str)
if (c <= 'Z' && c >= 'A')
c = c - ('Z' - 'z');
return str;
}
void ModManager::CompileAssetsForFile(const char* filename)
{
size_t fileHash = STR_HASH(fs::path(filename).lexically_normal().string());
size_t fileHash = STR_HASH(NormaliseModFilePath(fs::path(filename)));
if (fileHash == m_hScriptsRsonHash)
BuildScriptsRson();
@ -592,9 +648,9 @@ void ModManager::CompileAssetsForFile(const char* filename)
else
{
// check if we should build keyvalues, depending on whether any of our mods have patch kvs for this file
for (Mod& mod : m_loadedMods)
for (Mod& mod : m_LoadedMods)
{
if (!mod.Enabled)
if (!mod.m_bEnabled)
continue;
if (mod.KeyValues.find(fileHash) != mod.KeyValues.end())
@ -608,14 +664,7 @@ void ModManager::CompileAssetsForFile(const char* filename)
void ConCommand_reload_mods(const CCommand& args)
{
g_ModManager->LoadMods();
}
void InitialiseModManager(HMODULE baseAddress)
{
g_ModManager = new ModManager;
RegisterConCommand("reload_mods", ConCommand_reload_mods, "reloads mods", FCVAR_NONE);
g_pModManager->LoadMods();
}
fs::path GetModFolderPath()
@ -626,3 +675,10 @@ fs::path GetCompiledAssetsPath()
{
return fs::path(GetNorthstarPrefix() + COMPILED_ASSETS_SUFFIX);
}
ON_DLL_LOAD_RELIESON("engine.dll", ModManager, (ConCommand, MasterServer), (CModule module))
{
g_pModManager = new ModManager;
RegisterConCommand("reload_mods", ConCommand_reload_mods, "reloads mods", FCVAR_NONE);
}

View File

@ -1,14 +1,15 @@
#pragma once
#include "convar.h"
#include "memalloc.h"
#include "squirrel.h"
#include "rapidjson/document.h"
#include <string>
#include <vector>
#include <filesystem>
#include "rapidjson/document.h"
#include "memalloc.h"
namespace fs = std::filesystem;
const std::string MOD_FOLDER_SUFFIX = "/mods";
const std::string REMOTE_MOD_FOLDER_SUFFIX = "/runtime/remote/mods";
const fs::path MOD_OVERRIDE_DIR = "mod";
const std::string COMPILED_ASSETS_SUFFIX = "/runtime/compiled";
@ -24,9 +25,6 @@ struct ModConVar
struct ModScriptCallback
{
public:
// would've liked to make it possible to hook arbitrary codecallbacks, but couldn't find a function that calls some ui ones
// std::string HookedCodeCallback;
ScriptContext Context;
// called before the codecallback is executed
@ -39,7 +37,7 @@ struct ModScript
{
public:
std::string Path;
std::string RsonRunOn;
std::string RunOn;
std::vector<ModScriptCallback> Callbacks;
};
@ -64,8 +62,10 @@ class Mod
{
public:
// runtime stuff
fs::path ModDirectory;
bool Enabled = true;
bool m_bEnabled = true;
bool m_bWasReadSuccessfully = false;
fs::path m_ModDirectory;
// bool m_bIsRemote;
// mod.json stuff:
@ -100,14 +100,9 @@ class Mod
std::vector<ModRpakEntry> Rpaks;
std::unordered_map<std::string, std::string>
RpakAliases; // paks we alias to other rpaks, e.g. to load sp_crashsite paks on the map mp_crashsite
// iterated over to create squirrel VM constants depending if a mod exists or not.
// this only exists because we cannot access g_ModManager whilst mods are being loaded for the first time for some reason.
std::unordered_map<std::string, std::string> DependencyConstants;
// other stuff
bool wasReadSuccessfully = false;
public:
Mod(fs::path modPath, char* jsonBuf);
};
@ -115,42 +110,40 @@ class Mod
struct ModOverrideFile
{
public:
Mod* owningMod;
fs::path path;
Mod* m_pOwningMod;
fs::path m_Path;
};
class ModManager
{
private:
bool m_hasLoadedMods = false;
bool m_hasEnabledModsCfg;
rapidjson_document m_enabledModsCfg;
bool m_bHasLoadedMods = false;
bool m_bHasEnabledModsCfg;
rapidjson_document m_EnabledModsCfg;
// precalculated hashes
size_t m_hScriptsRsonHash;
size_t m_hPdefHash;
public:
std::vector<Mod> m_loadedMods;
std::unordered_map<std::string, ModOverrideFile> m_modFiles;
// iterated over to create squirrel VM constants depending if a mod exists or not.
// here because constants are global anyways.
std::unordered_map<std::string, std::string> DependencyConstants;
std::vector<Mod> m_LoadedMods;
std::unordered_map<std::string, ModOverrideFile> m_ModFiles;
std::unordered_map<std::string, std::string> m_DependencyConstants;
public:
ModManager();
void LoadMods();
void UnloadMods();
std::string NormaliseModFilePath(const fs::path path);
void CompileAssetsForFile(const char* filename);
// compile asset type stuff, these are done in files under Mods/Compiled/
// compile asset type stuff, these are done in files under runtime/compiled/
void BuildScriptsRson();
void TryBuildKeyValues(const char* filename);
void BuildPdef();
};
void InitialiseModManager(HMODULE baseAddress);
fs::path GetModFolderPath();
fs::path GetCompiledAssetsPath();
extern ModManager* g_ModManager;
extern ModManager* g_pModManager;

View File

@ -1,193 +0,0 @@
#pragma once
#include "pch.h"
// KittenPopo's memory stuff, made for northstar (because I really can't handle working with northstar's original memory stuff tbh)
#pragma region Pattern Scanning
namespace NSMem
{
inline std::vector<int> HexBytesToString(const char* str)
{
std::vector<int> patternNums;
int size = strlen(str);
for (int i = 0; i < size; i++)
{
char c = str[i];
// If this is a space character, ignore it
if (c == ' ' || c == '\t')
continue;
if (c == '?')
{
// Add a wildcard (-1)
patternNums.push_back(-1);
}
else if (i < size - 1)
{
BYTE result = 0;
for (int j = 0; j < 2; j++)
{
int val = 0;
char c = *(str + i + j);
if (c >= 'a')
{
val = c - 'a' + 0xA;
}
else if (c >= 'A')
{
val = c - 'A' + 0xA;
}
else if (isdigit(c))
{
val = c - '0';
}
else
{
assert(false);
val = -1;
}
result += (j == 0) ? val * 16 : val;
}
patternNums.push_back(result);
}
i++;
}
return patternNums;
}
inline void* PatternScan(void* module, const int* pattern, int patternSize, int offset)
{
if (!module)
return NULL;
auto dosHeader = (PIMAGE_DOS_HEADER)module;
auto ntHeaders = (PIMAGE_NT_HEADERS)((BYTE*)module + dosHeader->e_lfanew);
auto sizeOfImage = ntHeaders->OptionalHeader.SizeOfImage;
auto scanBytes = (BYTE*)module;
for (auto i = 0; i < sizeOfImage - patternSize; ++i)
{
bool found = true;
for (auto j = 0; j < patternSize; ++j)
{
if (scanBytes[i + j] != pattern[j] && pattern[j] != -1)
{
found = false;
break;
}
}
if (found)
{
uintptr_t addressInt = (uintptr_t)(&scanBytes[i]) + offset;
return (uint8_t*)addressInt;
}
}
return nullptr;
}
inline void* PatternScan(const char* moduleName, const char* pattern, int offset = 0)
{
std::vector<int> patternNums = HexBytesToString(pattern);
return PatternScan(GetModuleHandleA(moduleName), &patternNums[0], patternNums.size(), offset);
}
inline void BytePatch(uintptr_t address, const BYTE* vals, int size)
{
WriteProcessMemory(GetCurrentProcess(), (LPVOID)address, vals, size, NULL);
}
inline void BytePatch(uintptr_t address, std::initializer_list<BYTE> vals)
{
std::vector<BYTE> bytes = vals;
if (!bytes.empty())
BytePatch(address, &bytes[0], bytes.size());
}
inline void BytePatch(uintptr_t address, const char* bytesStr)
{
std::vector<int> byteInts = HexBytesToString(bytesStr);
std::vector<BYTE> bytes;
for (int v : byteInts)
bytes.push_back(v);
if (!bytes.empty())
BytePatch(address, &bytes[0], bytes.size());
}
inline void NOP(uintptr_t address, int size)
{
BYTE* buf = (BYTE*)malloc(size);
memset(buf, 0x90, size);
BytePatch(address, buf, size);
free(buf);
}
inline bool IsMemoryReadable(void* ptr, size_t size)
{
static SYSTEM_INFO sysInfo;
if (!sysInfo.dwPageSize)
GetSystemInfo(&sysInfo); // This should always be 4096 unless ur playing on NES or some shit but whatever
MEMORY_BASIC_INFORMATION memInfo;
if (!VirtualQuery(ptr, &memInfo, sizeof(memInfo)))
return false;
if (memInfo.RegionSize < size)
return false;
return (memInfo.State & MEM_COMMIT) && !(memInfo.Protect & PAGE_NOACCESS);
}
} // namespace NSMem
#pragma region KHOOK
struct KHookPatternInfo
{
const char *moduleName, *pattern;
int offset = 0;
KHookPatternInfo(const char* moduleName, const char* pattern, int offset = 0) : moduleName(moduleName), pattern(pattern), offset(offset)
{
}
};
struct KHook
{
KHookPatternInfo targetFunc;
void* targetFuncAddr;
void* hookFunc;
void** original;
static inline std::vector<KHook*> _allHooks;
KHook(KHookPatternInfo targetFunc, void* hookFunc, void** original) : targetFunc(targetFunc)
{
this->hookFunc = hookFunc;
this->original = original;
_allHooks.push_back(this);
}
bool Setup()
{
targetFuncAddr = NSMem::PatternScan(targetFunc.moduleName, targetFunc.pattern, targetFunc.offset);
if (!targetFuncAddr)
return false;
return (MH_CreateHook(targetFuncAddr, hookFunc, original) == MH_OK) && (MH_EnableHook(targetFuncAddr) == MH_OK);
}
};
#define KHOOK(name, funcPatternInfo, returnType, convention, args) \
returnType convention hk##name args; \
auto o##name = (returnType(convention*) args)0; \
KHook k##name = KHook(KHookPatternInfo funcPatternInfo, reinterpret_cast<void*>(&hk##name), (void**)&o##name); \
returnType convention hk##name args
#pragma endregion

View File

@ -1,13 +1,13 @@
#include <string>
#include "pch.h"
#include "nsprefix.h"
#include <string>
std::string GetNorthstarPrefix()
{
return NORTHSTAR_FOLDER_PREFIX;
}
void parseConfigurables()
void InitialiseNorthstarPrefix()
{
char* clachar = strstr(GetCommandLineA(), "-profile=");
if (clachar)

View File

@ -3,5 +3,5 @@
static std::string NORTHSTAR_FOLDER_PREFIX;
void InitialiseNorthstarPrefix();
std::string GetNorthstarPrefix();
void parseConfigurables();

View File

@ -8,8 +8,6 @@
#define _WINSOCK_DEPRECATED_NO_WARNINGS // temp because i'm very lazy and want to use inet_addr, remove later
#define RAPIDJSON_HAS_STDSTRING 1
// httplib ssl
// add headers that you want to pre-compile here
#include "memalloc.h"
@ -20,11 +18,14 @@
#include <filesystem>
#include <sstream>
namespace fs = std::filesystem;
#include "logging.h"
#include "include/MinHook.h"
#include "MinHook.h"
#include "spdlog/spdlog.h"
#include "libcurl/include/curl/curl.h"
#include "hookutils.h"
#include "hooks.h"
#include "memory.h"
template <typename ReturnType, typename... Args> ReturnType CallVFunc(int index, void* thisPtr, Args... args)
{

View File

@ -1,8 +1,8 @@
#include "pch.h"
#include "modmanager.h"
#include "filesystem.h"
#include "hookutils.h"
#include "pdef.h"
#include <map>
#include <sstream>
#include <fstream>
@ -14,11 +14,11 @@ void ModManager::BuildPdef()
fs::path MOD_PDEF_PATH = fs::path(GetCompiledAssetsPath() / MOD_PDEF_SUFFIX);
fs::remove(MOD_PDEF_PATH);
std::string pdef = ReadVPKOriginalFile(VPK_PDEF_PATH);
std::string pdef = R2::ReadVPKOriginalFile(VPK_PDEF_PATH);
for (Mod& mod : m_loadedMods)
for (Mod& mod : m_LoadedMods)
{
if (!mod.Enabled || !mod.Pdiff.size())
if (!mod.m_bEnabled || !mod.Pdiff.size())
continue;
// this code probably isn't going to be pretty lol
@ -107,11 +107,11 @@ void ModManager::BuildPdef()
writeStream.close();
ModOverrideFile overrideFile;
overrideFile.owningMod = nullptr;
overrideFile.path = VPK_PDEF_PATH;
overrideFile.m_pOwningMod = nullptr;
overrideFile.m_Path = VPK_PDEF_PATH;
if (m_modFiles.find(VPK_PDEF_PATH) == m_modFiles.end())
m_modFiles.insert(std::make_pair(VPK_PDEF_PATH, overrideFile));
if (m_ModFiles.find(VPK_PDEF_PATH) == m_ModFiles.end())
m_ModFiles.insert(std::make_pair(VPK_PDEF_PATH, overrideFile));
else
m_modFiles[VPK_PDEF_PATH] = overrideFile;
m_ModFiles[VPK_PDEF_PATH] = overrideFile;
}

View File

@ -1,30 +1,94 @@
#include "pch.h"
#include "playlist.h"
#include "nsmem.h"
#include "concommand.h"
#include "convar.h"
#include "gameutils.h"
#include "hookutils.h"
#include "squirrel.h"
#include "hoststate.h"
#include "serverpresence.h"
typedef char (*Onclc_SetPlaylistVarOverrideType)(void* a1, void* a2);
Onclc_SetPlaylistVarOverrideType Onclc_SetPlaylistVarOverride;
AUTOHOOK_INIT()
typedef int (*GetCurrentGamemodeMaxPlayersType)();
GetCurrentGamemodeMaxPlayersType GetCurrentGamemodeMaxPlayers;
// function type defined in gameutils.h
SetPlaylistVarOverrideType SetPlaylistVarOverrideOriginal;
GetCurrentPlaylistVarType GetCurrentPlaylistVarOriginal;
// use the R2 namespace for game funcs
namespace R2
{
const char* (*GetCurrentPlaylistName)();
void (*SetCurrentPlaylist)(const char* pPlaylistName);
void (*SetPlaylistVarOverride)(const char* pVarName, const char* pValue);
const char* (*GetCurrentPlaylistVar)(const char* pVarName, bool bUseOverrides);
} // namespace R2
ConVar* Cvar_ns_use_clc_SetPlaylistVarOverride;
// clang-format off
AUTOHOOK(clc_SetPlaylistVarOverride__Process, engine.dll + 0x222180,
char, __fastcall, (void* a1, void* a2))
// clang-format on
{
// the private_match playlist on mp_lobby is the only situation where there should be any legitimate sending of this netmessage
if (!Cvar_ns_use_clc_SetPlaylistVarOverride->GetBool() || strcmp(R2::GetCurrentPlaylistName(), "private_match") ||
strcmp(R2::g_pHostState->m_levelName, "mp_lobby"))
return 1;
return clc_SetPlaylistVarOverride__Process(a1, a2);
}
// clang-format off
AUTOHOOK(SetCurrentPlaylist, engine.dll + 0x18EB20,
bool, __fastcall, (const char* pPlaylistName))
// clang-format on
{
bool bSuccess = SetCurrentPlaylist(pPlaylistName);
if (bSuccess)
{
spdlog::info("Set playlist to {}", R2::GetCurrentPlaylistName());
g_pServerPresence->SetPlaylist(R2::GetCurrentPlaylistName());
}
return bSuccess;
}
// clang-format off
AUTOHOOK(SetPlaylistVarOverride, engine.dll + 0x18ED00,
void, __fastcall, (const char* pVarName, const char* pValue))
// clang-format on
{
if (strlen(pValue) >= 64)
return;
SetPlaylistVarOverride(pVarName, pValue);
}
// clang-format off
AUTOHOOK(GetCurrentPlaylistVar, engine.dll + 0x18C680,
const char*, __fastcall, (const char* pVarName, bool bUseOverrides))
// clang-format on
{
if (!bUseOverrides && !strcmp(pVarName, "max_players"))
bUseOverrides = true;
return GetCurrentPlaylistVar(pVarName, bUseOverrides);
}
// clang-format off
AUTOHOOK(GetCurrentGamemodeMaxPlayers, engine.dll + 0x18C430,
int, __fastcall, ())
// clang-format on
{
const char* pMaxPlayers = R2::GetCurrentPlaylistVar("max_players", 0);
if (!pMaxPlayers)
return GetCurrentGamemodeMaxPlayers();
int iMaxPlayers = atoi(pMaxPlayers);
return iMaxPlayers;
}
void ConCommand_playlist(const CCommand& args)
{
if (args.ArgC() < 2)
return;
SetCurrentPlaylist(args.Arg(1));
R2::SetCurrentPlaylist(args.Arg(1));
}
void ConCommand_setplaylistvaroverride(const CCommand& args)
@ -33,74 +97,34 @@ void ConCommand_setplaylistvaroverride(const CCommand& args)
return;
for (int i = 1; i < args.ArgC(); i += 2)
SetPlaylistVarOverride(args.Arg(i), args.Arg(i + 1));
R2::SetPlaylistVarOverride(args.Arg(i), args.Arg(i + 1));
}
char Onclc_SetPlaylistVarOverrideHook(void* a1, void* a2)
ON_DLL_LOAD_RELIESON("engine.dll", PlaylistHooks, (ConCommand, ConVar), (CModule module))
{
// the private_match playlist is the only situation where there should be any legitimate sending of this netmessage
// todo: check mp_lobby here too
if (!Cvar_ns_use_clc_SetPlaylistVarOverride->GetBool() || strcmp(GetCurrentPlaylistName(), "private_match"))
return 1;
AUTOHOOK_DISPATCH()
return Onclc_SetPlaylistVarOverride(a1, a2);
}
R2::GetCurrentPlaylistName = module.Offset(0x18C640).As<const char* (*)()>();
R2::SetCurrentPlaylist = module.Offset(0x18EB20).As<void (*)(const char*)>();
R2::SetPlaylistVarOverride = module.Offset(0x18ED00).As<void (*)(const char*, const char*)>();
R2::GetCurrentPlaylistVar = module.Offset(0x18C680).As<const char* (*)(const char*, bool)>();
void SetPlaylistVarOverrideHook(const char* varName, const char* value)
{
if (strlen(value) >= 64)
return;
SetPlaylistVarOverrideOriginal(varName, value);
}
char* GetCurrentPlaylistVarHook(const char* varName, bool useOverrides)
{
if (!useOverrides && !strcmp(varName, "max_players"))
useOverrides = true;
return GetCurrentPlaylistVarOriginal(varName, useOverrides);
}
int GetCurrentGamemodeMaxPlayersHook()
{
char* maxPlayersStr = GetCurrentPlaylistVar("max_players", 0);
if (!maxPlayersStr)
return GetCurrentGamemodeMaxPlayers();
int maxPlayers = atoi(maxPlayersStr);
return maxPlayers;
}
void InitialisePlaylistHooks(HMODULE baseAddress)
{
// playlist is the name of the command on respawn servers, but we already use setplaylist so can't get rid of it
RegisterConCommand("playlist", ConCommand_playlist, "Sets the current playlist", FCVAR_NONE);
RegisterConCommand("setplaylist", ConCommand_playlist, "Sets the current playlist", FCVAR_NONE);
RegisterConCommand("setplaylistvaroverrides", ConCommand_setplaylistvaroverride, "sets a playlist var override", FCVAR_NONE);
// note: clc_SetPlaylistVarOverride is pretty insecure, since it allows for entirely arbitrary playlist var overrides to be sent to the
// server this is somewhat restricted on custom servers to prevent it being done outside of private matches, but ideally it should be
// server, this is somewhat restricted on custom servers to prevent it being done outside of private matches, but ideally it should be
// disabled altogether, since the custom menus won't use it anyway this should only really be accepted if you want vanilla client
// compatibility
Cvar_ns_use_clc_SetPlaylistVarOverride = new ConVar(
"ns_use_clc_SetPlaylistVarOverride", "0", FCVAR_GAMEDLL, "Whether the server should accept clc_SetPlaylistVarOverride messages");
HookEnabler hook;
ENABLER_CREATEHOOK(
hook, (char*)baseAddress + 0x222180, &Onclc_SetPlaylistVarOverrideHook, reinterpret_cast<LPVOID*>(&Onclc_SetPlaylistVarOverride));
ENABLER_CREATEHOOK(
hook, (char*)baseAddress + 0x18ED00, &SetPlaylistVarOverrideHook, reinterpret_cast<LPVOID*>(&SetPlaylistVarOverrideOriginal));
ENABLER_CREATEHOOK(
hook, (char*)baseAddress + 0x18C680, &GetCurrentPlaylistVarHook, reinterpret_cast<LPVOID*>(&GetCurrentPlaylistVarOriginal));
ENABLER_CREATEHOOK(
hook, (char*)baseAddress + 0x18C430, &GetCurrentGamemodeMaxPlayersHook, reinterpret_cast<LPVOID*>(&GetCurrentGamemodeMaxPlayers));
uintptr_t ba = (uintptr_t)baseAddress;
// patch to prevent clc_SetPlaylistVarOverride from being able to crash servers if we reach max overrides due to a call to Error (why is
// this possible respawn, wtf) todo: add a warning for this
{
NSMem::BytePatch(ba + 0x18ED8D, "C3");
}
module.Offset(0x18ED8D).Patch("C3");
// patch to allow setplaylistvaroverride to be called before map init on dedicated and private match launched through the game
NSMem::NOP(ba + 0x18ED17, 6);
module.Offset(0x18ED17).NOP(6);
}

View File

@ -1,2 +1,10 @@
#pragma once
void InitialisePlaylistHooks(HMODULE baseAddress);
// use the R2 namespace for game funcs
namespace R2
{
extern const char* (*GetCurrentPlaylistName)();
extern void (*SetCurrentPlaylist)(const char* pPlaylistName);
extern void (*SetPlaylistVarOverride)(const char* pVarName, const char* pValue);
extern const char* (*GetCurrentPlaylistVar)(const char* pVarName, bool bUseOverrides);
} // namespace R2

View File

@ -1,9 +1,11 @@
#include "pch.h"
#include "squirrel.h"
#include "plugins.h"
#include <chrono>
#include "masterserver.h"
#include "convar.h"
#include "serverpresence.h"
#include <chrono>
#include <windows.h>
/// <summary>
@ -109,31 +111,31 @@ void initGameState()
}
// string gamemode, string gamemodeName, string map, string mapName, bool connected, bool loading
SQRESULT SQ_UpdateGameStateUI(void* sqvm)
SQRESULT SQ_UpdateGameStateUI(HSquirrelVM* sqvm)
{
AcquireSRWLockExclusive(&gameStateLock);
gameState.map = ClientSq_getstring(sqvm, 1);
gameState.mapDisplayName = ClientSq_getstring(sqvm, 2);
gameState.playlist = ClientSq_getstring(sqvm, 3);
gameState.playlistDisplayName = ClientSq_getstring(sqvm, 4);
gameState.connected = ClientSq_getbool(sqvm, 5);
gameState.loading = ClientSq_getbool(sqvm, 6);
gameState.map = g_pSquirrel<ScriptContext::UI>->getstring(sqvm, 1);
gameState.mapDisplayName = g_pSquirrel<ScriptContext::UI>->getstring(sqvm, 2);
gameState.playlist = g_pSquirrel<ScriptContext::UI>->getstring(sqvm, 3);
gameState.playlistDisplayName = g_pSquirrel<ScriptContext::UI>->getstring(sqvm, 4);
gameState.connected = g_pSquirrel<ScriptContext::UI>->getbool(sqvm, 5);
gameState.loading = g_pSquirrel<ScriptContext::UI>->getbool(sqvm, 6);
ReleaseSRWLockExclusive(&gameStateLock);
return SQRESULT_NOTNULL;
}
// int playerCount, int outScore, int secondHighestScore, int highestScore, bool roundBased, int scoreLimit
SQRESULT SQ_UpdateGameStateClient(void* sqvm)
SQRESULT SQ_UpdateGameStateClient(HSquirrelVM* sqvm)
{
AcquireSRWLockExclusive(&gameStateLock);
AcquireSRWLockExclusive(&serverInfoLock);
gameState.players = ClientSq_getinteger(sqvm, 1);
serverInfo.maxPlayers = ClientSq_getinteger(sqvm, 2);
gameState.ourScore = ClientSq_getinteger(sqvm, 3);
gameState.secondHighestScore = ClientSq_getinteger(sqvm, 4);
gameState.highestScore = ClientSq_getinteger(sqvm, 5);
serverInfo.roundBased = ClientSq_getbool(sqvm, 6);
serverInfo.scoreLimit = ClientSq_getbool(sqvm, 7);
gameState.players = g_pSquirrel<ScriptContext::CLIENT>->getinteger(sqvm, 1);
serverInfo.maxPlayers = g_pSquirrel<ScriptContext::CLIENT>->getinteger(sqvm, 2);
gameState.ourScore = g_pSquirrel<ScriptContext::CLIENT>->getinteger(sqvm, 3);
gameState.secondHighestScore = g_pSquirrel<ScriptContext::CLIENT>->getinteger(sqvm, 4);
gameState.highestScore = g_pSquirrel<ScriptContext::CLIENT>->getinteger(sqvm, 5);
serverInfo.roundBased = g_pSquirrel<ScriptContext::CLIENT>->getbool(sqvm, 6);
serverInfo.scoreLimit = g_pSquirrel<ScriptContext::CLIENT>->getbool(sqvm, 7);
ReleaseSRWLockExclusive(&gameStateLock);
ReleaseSRWLockExclusive(&serverInfoLock);
return SQRESULT_NOTNULL;
@ -141,59 +143,59 @@ SQRESULT SQ_UpdateGameStateClient(void* sqvm)
// string id, string name, string password, int players, int maxPlayers, string map, string mapDisplayName, string playlist, string
// playlistDisplayName
SQRESULT SQ_UpdateServerInfo(void* sqvm)
SQRESULT SQ_UpdateServerInfo(HSquirrelVM* sqvm)
{
AcquireSRWLockExclusive(&gameStateLock);
AcquireSRWLockExclusive(&serverInfoLock);
serverInfo.id = ClientSq_getstring(sqvm, 1);
serverInfo.name = ClientSq_getstring(sqvm, 2);
serverInfo.password = ClientSq_getstring(sqvm, 3);
gameState.players = ClientSq_getinteger(sqvm, 4);
serverInfo.maxPlayers = ClientSq_getinteger(sqvm, 5);
gameState.map = ClientSq_getstring(sqvm, 6);
gameState.mapDisplayName = ClientSq_getstring(sqvm, 7);
gameState.playlist = ClientSq_getstring(sqvm, 8);
gameState.playlistDisplayName = ClientSq_getstring(sqvm, 9);
serverInfo.id = g_pSquirrel<ScriptContext::UI>->getstring(sqvm, 1);
serverInfo.name = g_pSquirrel<ScriptContext::UI>->getstring(sqvm, 2);
serverInfo.password = g_pSquirrel<ScriptContext::UI>->getstring(sqvm, 3);
gameState.players = g_pSquirrel<ScriptContext::UI>->getinteger(sqvm, 4);
serverInfo.maxPlayers = g_pSquirrel<ScriptContext::UI>->getinteger(sqvm, 5);
gameState.map = g_pSquirrel<ScriptContext::UI>->getstring(sqvm, 6);
gameState.mapDisplayName = g_pSquirrel<ScriptContext::UI>->getstring(sqvm, 7);
gameState.playlist = g_pSquirrel<ScriptContext::UI>->getstring(sqvm, 8);
gameState.playlistDisplayName = g_pSquirrel<ScriptContext::UI>->getstring(sqvm, 9);
ReleaseSRWLockExclusive(&gameStateLock);
ReleaseSRWLockExclusive(&serverInfoLock);
return SQRESULT_NOTNULL;
}
// int maxPlayers
SQRESULT SQ_UpdateServerInfoBetweenRounds(void* sqvm)
SQRESULT SQ_UpdateServerInfoBetweenRounds(HSquirrelVM* sqvm)
{
AcquireSRWLockExclusive(&serverInfoLock);
serverInfo.id = ClientSq_getstring(sqvm, 1);
serverInfo.name = ClientSq_getstring(sqvm, 2);
serverInfo.password = ClientSq_getstring(sqvm, 3);
serverInfo.maxPlayers = ClientSq_getinteger(sqvm, 4);
serverInfo.id = g_pSquirrel<ScriptContext::CLIENT>->getstring(sqvm, 1);
serverInfo.name = g_pSquirrel<ScriptContext::CLIENT>->getstring(sqvm, 2);
serverInfo.password = g_pSquirrel<ScriptContext::CLIENT>->getstring(sqvm, 3);
serverInfo.maxPlayers = g_pSquirrel<ScriptContext::CLIENT>->getinteger(sqvm, 4);
ReleaseSRWLockExclusive(&serverInfoLock);
return SQRESULT_NOTNULL;
}
// float timeInFuture
SQRESULT SQ_UpdateTimeInfo(void* sqvm)
SQRESULT SQ_UpdateTimeInfo(HSquirrelVM* sqvm)
{
AcquireSRWLockExclusive(&serverInfoLock);
serverInfo.endTime = ceil(ClientSq_getfloat(sqvm, 1));
serverInfo.endTime = ceil(g_pSquirrel<ScriptContext::CLIENT>->getfloat(sqvm, 1));
ReleaseSRWLockExclusive(&serverInfoLock);
return SQRESULT_NOTNULL;
}
// bool loading
SQRESULT SQ_SetConnected(void* sqvm)
SQRESULT SQ_SetConnected(HSquirrelVM* sqvm)
{
AcquireSRWLockExclusive(&gameStateLock);
gameState.loading = ClientSq_getbool(sqvm, 1);
gameState.loading = g_pSquirrel<ScriptContext::UI>->getbool(sqvm, 1);
ReleaseSRWLockExclusive(&gameStateLock);
return SQRESULT_NOTNULL;
}
SQRESULT SQ_UpdateListenServer(void* sqvm)
SQRESULT SQ_UpdateListenServer(HSquirrelVM* sqvm)
{
AcquireSRWLockExclusive(&serverInfoLock);
serverInfo.id = g_MasterServerManager->m_sOwnServerId;
serverInfo.password = Cvar_ns_server_password->GetString();
serverInfo.id = g_pMasterServerManager->m_sOwnServerId;
serverInfo.password = ""; // g_pServerPresence->Cvar_ns_server_password->GetString(); todo this fr
ReleaseSRWLockExclusive(&serverInfoLock);
return SQRESULT_NOTNULL;
}
@ -384,26 +386,26 @@ int getPlayerInfoBool(bool* out_ptr, PlayerInfoType var)
return n;
}
void InitialisePluginCommands(HMODULE baseAddress)
ON_DLL_LOAD_CLIENT_RELIESON("client.dll", PluginCommands, ClientSquirrel, (CModule module))
{
// i swear there's a way to make this not have be run in 2 contexts but i can't figure it out
// some funcs i need are just not available in UI or CLIENT
if (g_UISquirrelManager && g_ClientSquirrelManager)
if (g_pSquirrel<ScriptContext::UI> && g_pSquirrel<ScriptContext::CLIENT>)
{
g_UISquirrelManager->AddFuncRegistration(
g_pSquirrel<ScriptContext::UI>->AddFuncRegistration(
"void",
"NSUpdateGameStateUI",
"string gamemode, string gamemodeName, string map, string mapName, bool connected, bool loading",
"",
SQ_UpdateGameStateUI);
g_ClientSquirrelManager->AddFuncRegistration(
g_pSquirrel<ScriptContext::CLIENT>->AddFuncRegistration(
"void",
"NSUpdateGameStateClient",
"int playerCount, int maxPlayers, int outScore, int secondHighestScore, int highestScore, bool roundBased, int scoreLimit",
"",
SQ_UpdateGameStateClient);
g_UISquirrelManager->AddFuncRegistration(
g_pSquirrel<ScriptContext::UI>->AddFuncRegistration(
"void",
"NSUpdateServerInfo",
"string id, string name, string password, int players, int maxPlayers, string map, string mapDisplayName, string playlist, "
@ -411,10 +413,10 @@ void InitialisePluginCommands(HMODULE baseAddress)
"playlistDisplayName",
"",
SQ_UpdateServerInfo);
g_ClientSquirrelManager->AddFuncRegistration(
g_pSquirrel<ScriptContext::CLIENT>->AddFuncRegistration(
"void", "NSUpdateServerInfoReload", "int maxPlayers", "", SQ_UpdateServerInfoBetweenRounds);
g_ClientSquirrelManager->AddFuncRegistration("void", "NSUpdateTimeInfo", "float timeInFuture", "", SQ_UpdateTimeInfo);
g_UISquirrelManager->AddFuncRegistration("void", "NSSetLoading", "bool loading", "", SQ_SetConnected);
g_UISquirrelManager->AddFuncRegistration("void", "NSUpdateListenServer", "", "", SQ_UpdateListenServer);
g_pSquirrel<ScriptContext::CLIENT>->AddFuncRegistration("void", "NSUpdateTimeInfo", "float timeInFuture", "", SQ_UpdateTimeInfo);
g_pSquirrel<ScriptContext::UI>->AddFuncRegistration("void", "NSSetLoading", "bool loading", "", SQ_SetConnected);
g_pSquirrel<ScriptContext::UI>->AddFuncRegistration("void", "NSUpdateListenServer", "", "", SQ_UpdateListenServer);
}
}

View File

@ -15,5 +15,3 @@ int getPlayerInfoBool(bool* out_ptr, PlayerInfoType var);
void initGameState();
void* getPluginObject(PluginObject var);
void InitialisePluginCommands(HMODULE baseAddress);

View File

@ -0,0 +1,6 @@
#pragma once
#include "concommand.h"
void PrintCommandHelpDialogue(const ConCommandBase* command, const char* name);
void TryPrintCvarHelpForCommand(const char* pCommand);
void InitialiseCommandPrint();

View File

@ -0,0 +1,174 @@
#include "pch.h"
#include "printcommand.h"
#include "convar.h"
#include "concommand.h"
void PrintCommandHelpDialogue(const ConCommandBase* command, const char* name)
{
if (!command)
{
spdlog::info("unknown command {}", name);
return;
}
// temp because command->IsCommand does not currently work
ConVar* cvar = R2::g_pCVar->FindVar(command->m_pszName);
// build string for flags if not FCVAR_NONE
std::string flagString;
if (command->GetFlags() != FCVAR_NONE)
{
flagString = "( ";
for (auto& flagPair : g_PrintCommandFlags)
{
if (command->GetFlags() & flagPair.first)
{
// special case, slightly hacky: PRINTABLEONLY is for commands, GAMEDLL_FOR_REMOTE_CLIENTS is for concommands, both have the
// same value
if (flagPair.first == FCVAR_PRINTABLEONLY)
{
if (cvar && !strcmp(flagPair.second, "GAMEDLL_FOR_REMOTE_CLIENTS"))
continue;
if (!cvar && !strcmp(flagPair.second, "PRINTABLEONLY"))
continue;
}
flagString += flagPair.second;
flagString += " ";
}
}
flagString += ") ";
}
if (cvar)
spdlog::info("\"{}\" = \"{}\" {}- {}", cvar->GetBaseName(), cvar->GetString(), flagString, cvar->GetHelpText());
else
spdlog::info("\"{}\" {} - {}", command->m_pszName, flagString, command->GetHelpText());
}
void TryPrintCvarHelpForCommand(const char* pCommand)
{
// try to display help text for an inputted command string from the console
int pCommandLen = strlen(pCommand);
char* pCvarStr = new char[pCommandLen];
strcpy(pCvarStr, pCommand);
// trim whitespace from right
for (int i = pCommandLen - 1; i; i--)
{
if (isspace(pCvarStr[i]))
pCvarStr[i] = '\0';
else
break;
}
// check if we're inputting a cvar, but not setting it at all
ConVar* cvar = R2::g_pCVar->FindVar(pCvarStr);
if (cvar)
PrintCommandHelpDialogue(&cvar->m_ConCommandBase, pCvarStr);
delete[] pCvarStr;
}
void ConCommand_help(const CCommand& arg)
{
if (arg.ArgC() < 2)
{
spdlog::info("Usage: help <cvarname>");
return;
}
PrintCommandHelpDialogue(R2::g_pCVar->FindCommandBase(arg.Arg(1)), arg.Arg(1));
}
void ConCommand_find(const CCommand& arg)
{
if (arg.ArgC() < 2)
{
spdlog::info("Usage: find <string> [<string>...]");
return;
}
char pTempName[256];
char pTempSearchTerm[256];
for (auto& map : R2::g_pCVar->DumpToMap())
{
bool bPrintCommand = true;
for (int i = 0; i < arg.ArgC() - 1; i++)
{
// make lowercase to avoid case sensitivity
strncpy_s(pTempName, sizeof(pTempName), map.second->m_pszName, sizeof(pTempName) - 1);
strncpy_s(pTempSearchTerm, sizeof(pTempSearchTerm), arg.Arg(i + 1), sizeof(pTempSearchTerm) - 1);
for (int i = 0; pTempName[i]; i++)
pTempName[i] = tolower(pTempName[i]);
for (int i = 0; pTempSearchTerm[i]; i++)
pTempSearchTerm[i] = tolower(pTempSearchTerm[i]);
if (!strstr(pTempName, pTempSearchTerm))
{
bPrintCommand = false;
break;
}
}
if (bPrintCommand)
PrintCommandHelpDialogue(map.second, map.second->m_pszName);
}
}
void ConCommand_findflags(const CCommand& arg)
{
if (arg.ArgC() < 2)
{
spdlog::info("Usage: findflags <string>");
for (auto& flagPair : g_PrintCommandFlags)
spdlog::info(" - {}", flagPair.second);
return;
}
// convert input flag to uppercase
char* upperFlag = new char[strlen(arg.Arg(1))];
strcpy(upperFlag, arg.Arg(1));
for (int i = 0; upperFlag[i]; i++)
upperFlag[i] = toupper(upperFlag[i]);
// resolve flag name => int flags
int resolvedFlag = FCVAR_NONE;
for (auto& flagPair : g_PrintCommandFlags)
{
if (!strcmp(flagPair.second, upperFlag))
{
resolvedFlag |= flagPair.first;
break;
}
}
// print cvars
for (auto& map : R2::g_pCVar->DumpToMap())
{
if (map.second->m_nFlags & resolvedFlag)
PrintCommandHelpDialogue(map.second, map.second->m_pszName);
}
delete[] upperFlag;
}
void InitialiseCommandPrint()
{
RegisterConCommand("find", ConCommand_find, "Find concommands with the specified string in their name/help text.", FCVAR_NONE);
RegisterConCommand("findflags", ConCommand_findflags, "Find concommands by flags.", FCVAR_NONE);
// help is already a command, so we need to modify the preexisting command to use our func instead
// and clear the flags also
ConCommand* helpCommand = R2::g_pCVar->FindCommand("help");
helpCommand->m_nFlags = FCVAR_NONE;
helpCommand->m_pCommandCallback = ConCommand_help;
}

168
NorthstarDLL/printmaps.cpp Normal file
View File

@ -0,0 +1,168 @@
#include "pch.h"
#include "printmaps.h"
#include "convar.h"
#include "concommand.h"
#include "modmanager.h"
#include "tier0.h"
#include "r2engine.h"
#include <filesystem>
#include <regex>
AUTOHOOK_INIT()
enum class MapSource_t
{
VPK,
GAMEDIR,
MOD
};
const std::unordered_map<MapSource_t, const char*> PrintMapSource = {
{MapSource_t::VPK, "VPK"}, {MapSource_t::MOD, "MOD"}, {MapSource_t::GAMEDIR, "R2"}};
struct MapVPKInfo
{
std::string name;
std::string parent;
MapSource_t source;
};
// our current list of maps in the game
std::vector<MapVPKInfo> vMapList;
void RefreshMapList()
{
vMapList.clear();
// get modded maps
// TODO: could probably check mod vpks to get mapnames from there too?
for (auto& modFilePair : g_pModManager->m_ModFiles)
{
ModOverrideFile file = modFilePair.second;
if (file.m_Path.extension() == ".bsp" && file.m_Path.parent_path().string() == "maps") // only allow mod maps actually in /maps atm
{
MapVPKInfo& map = vMapList.emplace_back();
map.name = file.m_Path.stem().string();
map.parent = file.m_pOwningMod->Name;
map.source = MapSource_t::MOD;
}
}
// get maps in vpk
{
const int iNumRetailNonMapVpks = 1;
static const char* const ppRetailNonMapVpks[] = {
"englishclient_frontend.bsp.pak000_dir.vpk"}; // don't include mp_common here as it contains mp_lobby
// matches directory vpks, and captures their map name in the first group
static const std::regex rVpkMapRegex("englishclient_([a-zA-Z_]+)\\.bsp\\.pak000_dir\\.vpk", std::regex::icase);
for (fs::directory_entry file : fs::directory_iterator("./vpk"))
{
std::string pathString = file.path().filename().string();
bool bIsValidMapVpk = true;
for (int i = 0; i < iNumRetailNonMapVpks; i++)
{
if (!pathString.compare(ppRetailNonMapVpks[i]))
{
bIsValidMapVpk = false;
break;
}
}
if (!bIsValidMapVpk)
continue;
// run our map vpk regex on the filename
std::smatch match;
std::regex_match(pathString, match, rVpkMapRegex);
if (match.length() < 2)
continue;
std::string mapName = match[1].str();
// special case: englishclient_mp_common contains mp_lobby, so hardcode the name here
if (mapName == "mp_common")
mapName = "mp_lobby";
MapVPKInfo& map = vMapList.emplace_back();
map.name = mapName;
map.parent = pathString;
map.source = MapSource_t::VPK;
}
}
// get maps in game dir
for (fs::directory_entry file : fs::directory_iterator(fmt::format("{}/maps", R2::g_pModName)))
{
if (file.path().extension() == ".bsp")
{
MapVPKInfo& map = vMapList.emplace_back();
map.name = file.path().stem().string();
map.parent = "R2";
map.source = MapSource_t::GAMEDIR;
}
}
}
// clang-format off
AUTOHOOK(_Host_Map_f_CompletionFunc, engine.dll + 0x161AE0,
int, __fastcall, (const char const* cmdname, const char const* partial, char commands[COMMAND_COMPLETION_MAXITEMS][COMMAND_COMPLETION_ITEM_LENGTH]))
// clang-format on
{
// don't update our map list often from this func, only refresh every 10 seconds so we avoid constantly reading fs
static double flLastAutocompleteRefresh = -999;
if (flLastAutocompleteRefresh + 10.0 < Tier0::Plat_FloatTime())
{
RefreshMapList();
flLastAutocompleteRefresh = Tier0::Plat_FloatTime();
}
// use a custom autocomplete func for all map loading commands
const int cmdLength = strlen(cmdname);
const char* query = partial + cmdLength;
const int queryLength = strlen(query);
int numMaps = 0;
for (int i = 0; i < vMapList.size() && numMaps < COMMAND_COMPLETION_MAXITEMS; i++)
{
if (!strncmp(query, vMapList[i].name.c_str(), queryLength))
{
strcpy(commands[numMaps], cmdname);
strncpy_s(
commands[numMaps++] + cmdLength,
COMMAND_COMPLETION_ITEM_LENGTH,
&vMapList[i].name[0],
COMMAND_COMPLETION_ITEM_LENGTH - cmdLength);
}
}
return numMaps;
}
void ConCommand_maps(const CCommand& args)
{
if (args.ArgC() < 2)
{
spdlog::info("Usage: maps <substring>");
spdlog::info("maps * for full listing");
return;
}
RefreshMapList();
for (MapVPKInfo& map : vMapList) // need to figure out a nice way to include parent path without making the formatting awful
if ((*args.Arg(1) == '*' && !args.Arg(1)[1]) || strstr(map.name.c_str(), args.Arg(1)))
spdlog::info("({}) {}", PrintMapSource.at(map.source), map.name);
}
void InitialiseMapsPrint()
{
AUTOHOOK_DISPATCH()
ConCommand* mapsCommand = R2::g_pCVar->FindCommand("maps");
mapsCommand->m_pCommandCallback = ConCommand_maps;
}

2
NorthstarDLL/printmaps.h Normal file
View File

@ -0,0 +1,2 @@
#pragma once
void InitialiseMapsPrint();

Some files were not shown because too many files have changed in this diff Show More