From e04f3b36accccb590a2d51b4829256b9964ac3fd Mon Sep 17 00:00:00 2001 From: Emma Miler Date: Mon, 19 Dec 2022 19:32:16 +0100 Subject: Restructuring (#365) * Remove launcher proxy * Restructuring * More restructuring * Fix include dirs * Fix merge * Remove clang thing * Filters * Oops --- NorthstarDLL/NorthstarDLL.vcxproj | 262 ++-- NorthstarDLL/NorthstarDLL.vcxproj.filters | 814 ++++++------ NorthstarDLL/audio.cpp | 507 -------- NorthstarDLL/audio.h | 46 - NorthstarDLL/bansystem.cpp | 222 ---- NorthstarDLL/bansystem.h | 19 - NorthstarDLL/bitbuf.h | 1148 ----------------- NorthstarDLL/bits.cpp | 46 - NorthstarDLL/bits.h | 10 - NorthstarDLL/buildainfile.cpp | 400 ------ NorthstarDLL/chatcommand.cpp | 36 - NorthstarDLL/client/audio.cpp | 507 ++++++++ NorthstarDLL/client/audio.h | 46 + NorthstarDLL/client/chatcommand.cpp | 36 + NorthstarDLL/client/clientauthhooks.cpp | 64 + NorthstarDLL/client/clientruihooks.cpp | 24 + NorthstarDLL/client/clientvideooverrides.cpp | 42 + NorthstarDLL/client/debugoverlay.cpp | 141 +++ NorthstarDLL/client/demofixes.cpp | 26 + NorthstarDLL/client/diskvmtfixes.cpp | 16 + NorthstarDLL/client/languagehooks.cpp | 116 ++ NorthstarDLL/client/latencyflex.cpp | 44 + NorthstarDLL/client/localchatwriter.cpp | 450 +++++++ NorthstarDLL/client/localchatwriter.h | 65 + NorthstarDLL/client/modlocalisation.cpp | 35 + NorthstarDLL/client/r2client.cpp | 20 + NorthstarDLL/client/r2client.h | 11 + NorthstarDLL/clientauthhooks.cpp | 64 - NorthstarDLL/clientchathooks.cpp | 70 -- NorthstarDLL/clientruihooks.cpp | 24 - NorthstarDLL/clientvideooverrides.cpp | 42 - NorthstarDLL/color.cpp | 26 - NorthstarDLL/color.h | 197 --- NorthstarDLL/concommand.cpp | 151 --- NorthstarDLL/concommand.h | 140 --- NorthstarDLL/config/profile.cpp | 44 + NorthstarDLL/config/profile.h | 7 + NorthstarDLL/convar.cpp | 490 -------- NorthstarDLL/convar.h | 192 --- NorthstarDLL/core/convar/concommand.cpp | 151 +++ NorthstarDLL/core/convar/concommand.h | 140 +++ NorthstarDLL/core/convar/convar.cpp | 490 ++++++++ NorthstarDLL/core/convar/convar.h | 192 +++ NorthstarDLL/core/convar/cvar.cpp | 31 + NorthstarDLL/core/convar/cvar.h | 43 + NorthstarDLL/core/filesystem/filesystem.cpp | 186 +++ NorthstarDLL/core/filesystem/filesystem.h | 85 ++ NorthstarDLL/core/filesystem/rpakfilesystem.cpp | 342 +++++ NorthstarDLL/core/filesystem/rpakfilesystem.h | 39 + NorthstarDLL/core/hooks.cpp | 426 +++++++ NorthstarDLL/core/hooks.h | 311 +++++ NorthstarDLL/core/math/bitbuf.h | 1148 +++++++++++++++++ NorthstarDLL/core/math/bits.cpp | 46 + NorthstarDLL/core/math/bits.h | 10 + NorthstarDLL/core/math/color.cpp | 26 + NorthstarDLL/core/math/color.h | 197 +++ NorthstarDLL/core/math/vector.h | 52 + NorthstarDLL/core/memalloc.cpp | 74 ++ NorthstarDLL/core/memalloc.h | 49 + NorthstarDLL/core/memory.cpp | 348 ++++++ NorthstarDLL/core/memory.h | 90 ++ NorthstarDLL/core/structs.h | 62 + NorthstarDLL/core/tier0.cpp | 37 + NorthstarDLL/core/tier0.h | 68 + NorthstarDLL/crashhandler.cpp | 376 ------ NorthstarDLL/crashhandler.h | 26 - NorthstarDLL/cvar.cpp | 31 - NorthstarDLL/cvar.h | 43 - NorthstarDLL/debugoverlay.cpp | 141 --- NorthstarDLL/dedicated.cpp | 295 ----- NorthstarDLL/dedicated.h | 3 - NorthstarDLL/dedicated/dedicated.cpp | 295 +++++ NorthstarDLL/dedicated/dedicated.h | 3 + NorthstarDLL/dedicated/dedicatedmaterialsystem.cpp | 41 + NorthstarDLL/dedicatedmaterialsystem.cpp | 41 - NorthstarDLL/demofixes.cpp | 26 - NorthstarDLL/diskvmtfixes.cpp | 16 - NorthstarDLL/dllmain.cpp | 19 +- NorthstarDLL/dllmain.h | 4 + NorthstarDLL/engine/host.cpp | 31 + NorthstarDLL/engine/hoststate.cpp | 124 ++ NorthstarDLL/engine/hoststate.h | 45 + NorthstarDLL/engine/r2engine.cpp | 36 + NorthstarDLL/engine/r2engine.h | 185 +++ NorthstarDLL/engine/runframe.cpp | 18 + NorthstarDLL/exploitfixes.cpp | 458 ------- NorthstarDLL/exploitfixes_lzss.cpp | 79 -- NorthstarDLL/exploitfixes_utf8parser.cpp | 200 --- NorthstarDLL/filesystem.cpp | 186 --- NorthstarDLL/filesystem.h | 85 -- NorthstarDLL/hooks.cpp | 426 ------- NorthstarDLL/hooks.h | 311 ----- NorthstarDLL/host.cpp | 37 - NorthstarDLL/hoststate.cpp | 124 -- NorthstarDLL/hoststate.h | 45 - NorthstarDLL/httprequesthandler.cpp | 586 --------- NorthstarDLL/httprequesthandler.h | 132 -- NorthstarDLL/kb_act.cpp | 45 - NorthstarDLL/keyvalues.cpp | 1316 -------------------- NorthstarDLL/keyvalues.h | 134 -- NorthstarDLL/languagehooks.cpp | 116 -- NorthstarDLL/latencyflex.cpp | 44 - NorthstarDLL/limits.cpp | 298 ----- NorthstarDLL/limits.h | 51 - NorthstarDLL/localchatwriter.cpp | 450 ------- NorthstarDLL/localchatwriter.h | 65 - NorthstarDLL/logging.cpp | 205 --- NorthstarDLL/logging.h | 130 -- NorthstarDLL/logging/crashhandler.cpp | 376 ++++++ NorthstarDLL/logging/crashhandler.h | 26 + NorthstarDLL/logging/logging.cpp | 205 +++ NorthstarDLL/logging/logging.h | 130 ++ NorthstarDLL/logging/loghooks.cpp | 262 ++++ NorthstarDLL/logging/loghooks.h | 1 + NorthstarDLL/logging/sourceconsole.cpp | 92 ++ NorthstarDLL/logging/sourceconsole.h | 86 ++ NorthstarDLL/loghooks.cpp | 263 ---- NorthstarDLL/loghooks.h | 1 - NorthstarDLL/main.h | 4 - NorthstarDLL/masterserver.cpp | 1224 ------------------ NorthstarDLL/masterserver.h | 188 --- NorthstarDLL/masterserver/masterserver.cpp | 1224 ++++++++++++++++++ NorthstarDLL/masterserver/masterserver.h | 188 +++ NorthstarDLL/maxplayers.cpp | 645 ---------- NorthstarDLL/maxplayers.h | 7 - NorthstarDLL/memalloc.cpp | 74 -- NorthstarDLL/memalloc.h | 49 - NorthstarDLL/memory.cpp | 348 ------ NorthstarDLL/memory.h | 90 -- NorthstarDLL/misccommands.cpp | 315 ----- NorthstarDLL/misccommands.h | 3 - NorthstarDLL/miscserverfixes.cpp | 7 - NorthstarDLL/miscserverscript.cpp | 60 - NorthstarDLL/modkeyvalues.cpp | 107 -- NorthstarDLL/modlocalisation.cpp | 35 - NorthstarDLL/modmanager.cpp | 727 ----------- NorthstarDLL/modmanager.h | 154 --- NorthstarDLL/modpdef.cpp | 119 -- NorthstarDLL/mods/compiled/kb_act.cpp | 45 + NorthstarDLL/mods/compiled/modkeyvalues.cpp | 107 ++ NorthstarDLL/mods/compiled/modpdef.cpp | 119 ++ NorthstarDLL/mods/compiled/modscriptsrson.cpp | 66 + NorthstarDLL/mods/modmanager.cpp | 725 +++++++++++ NorthstarDLL/mods/modmanager.h | 154 +++ NorthstarDLL/modscriptsrson.cpp | 66 - NorthstarDLL/nsprefix.cpp | 44 - NorthstarDLL/nsprefix.h | 7 - NorthstarDLL/pch.h | 14 +- NorthstarDLL/playlist.cpp | 130 -- NorthstarDLL/playlist.h | 10 - NorthstarDLL/plugin_abi.h | 68 - NorthstarDLL/plugins.cpp | 422 ------- NorthstarDLL/plugins.h | 17 - NorthstarDLL/plugins/plugin_abi.h | 68 + NorthstarDLL/plugins/plugins.cpp | 422 +++++++ NorthstarDLL/plugins/plugins.h | 17 + NorthstarDLL/printcommand.h | 6 - NorthstarDLL/printcommands.cpp | 174 --- NorthstarDLL/printmaps.cpp | 168 --- NorthstarDLL/printmaps.h | 2 - NorthstarDLL/r2client.cpp | 20 - NorthstarDLL/r2client.h | 11 - NorthstarDLL/r2engine.cpp | 36 - NorthstarDLL/r2engine.h | 185 --- NorthstarDLL/r2server.cpp | 17 - NorthstarDLL/r2server.h | 28 - NorthstarDLL/rpakfilesystem.cpp | 342 ----- NorthstarDLL/rpakfilesystem.h | 39 - NorthstarDLL/runframe.cpp | 20 - NorthstarDLL/scriptbrowserhooks.cpp | 25 - NorthstarDLL/scriptdatatables.cpp | 910 -------------- NorthstarDLL/scriptjson.cpp | 266 ---- NorthstarDLL/scriptmainmenupromos.cpp | 124 -- NorthstarDLL/scriptmodmenu.cpp | 166 --- NorthstarDLL/scripts/client/clientchathooks.cpp | 70 ++ NorthstarDLL/scripts/client/scriptbrowserhooks.cpp | 25 + .../scripts/client/scriptmainmenupromos.cpp | 124 ++ NorthstarDLL/scripts/client/scriptmodmenu.cpp | 166 +++ .../scripts/client/scriptserverbrowser.cpp | 410 ++++++ .../client/scriptservertoclientstringcommand.cpp | 19 + NorthstarDLL/scripts/scriptdatatables.cpp | 910 ++++++++++++++ NorthstarDLL/scripts/scripthttprequesthandler.cpp | 586 +++++++++ NorthstarDLL/scripts/scripthttprequesthandler.h | 132 ++ NorthstarDLL/scripts/scriptjson.cpp | 249 ++++ NorthstarDLL/scripts/scriptutility.cpp | 14 + NorthstarDLL/scripts/server/miscserverfixes.cpp | 7 + NorthstarDLL/scripts/server/miscserverscript.cpp | 60 + NorthstarDLL/scripts/server/scriptuserinfo.cpp | 105 ++ NorthstarDLL/scriptserverbrowser.cpp | 410 ------ NorthstarDLL/scriptservertoclientstringcommand.cpp | 19 - NorthstarDLL/scriptuserinfo.cpp | 105 -- NorthstarDLL/scriptutility.cpp | 14 - NorthstarDLL/server/auth/bansystem.cpp | 222 ++++ NorthstarDLL/server/auth/bansystem.h | 19 + NorthstarDLL/server/auth/serverauthentication.cpp | 462 +++++++ NorthstarDLL/server/auth/serverauthentication.h | 59 + NorthstarDLL/server/buildainfile.cpp | 400 ++++++ NorthstarDLL/server/r2server.cpp | 17 + NorthstarDLL/server/r2server.h | 28 + NorthstarDLL/server/serverchathooks.cpp | 172 +++ NorthstarDLL/server/serverchathooks.h | 25 + NorthstarDLL/server/serverpresence.cpp | 237 ++++ NorthstarDLL/server/serverpresence.h | 92 ++ NorthstarDLL/serverauthentication.cpp | 462 ------- NorthstarDLL/serverauthentication.h | 59 - NorthstarDLL/serverchathooks.cpp | 172 --- NorthstarDLL/serverchathooks.h | 25 - NorthstarDLL/serverpresence.cpp | 237 ---- NorthstarDLL/serverpresence.h | 92 -- NorthstarDLL/shared/exploit_fixes/exploitfixes.cpp | 458 +++++++ .../shared/exploit_fixes/exploitfixes_lzss.cpp | 79 ++ .../exploit_fixes/exploitfixes_utf8parser.cpp | 200 +++ NorthstarDLL/shared/exploit_fixes/ns_limits.cpp | 298 +++++ NorthstarDLL/shared/exploit_fixes/ns_limits.h | 51 + NorthstarDLL/shared/keyvalues.cpp | 1316 ++++++++++++++++++++ NorthstarDLL/shared/keyvalues.h | 134 ++ NorthstarDLL/shared/maxplayers.cpp | 645 ++++++++++ NorthstarDLL/shared/maxplayers.h | 7 + NorthstarDLL/shared/misccommands.cpp | 314 +++++ NorthstarDLL/shared/misccommands.h | 3 + NorthstarDLL/shared/playlist.cpp | 130 ++ NorthstarDLL/shared/playlist.h | 10 + NorthstarDLL/shared/sourceinterface.cpp | 49 + NorthstarDLL/shared/sourceinterface.h | 31 + NorthstarDLL/sourceconsole.cpp | 92 -- NorthstarDLL/sourceconsole.h | 86 -- NorthstarDLL/sourceinterface.cpp | 49 - NorthstarDLL/sourceinterface.h | 31 - NorthstarDLL/squirrel.cpp | 739 ----------- NorthstarDLL/squirrel.h | 469 ------- NorthstarDLL/squirrel/squirrel.cpp | 739 +++++++++++ NorthstarDLL/squirrel/squirrel.h | 469 +++++++ NorthstarDLL/squirrel/squirrelautobind.cpp | 21 + NorthstarDLL/squirrel/squirrelautobind.h | 76 ++ NorthstarDLL/squirrel/squirrelclasstypes.h | 239 ++++ NorthstarDLL/squirrel/squirreldatatypes.h | 495 ++++++++ NorthstarDLL/squirrelautobind.cpp | 21 - NorthstarDLL/squirrelautobind.h | 76 -- NorthstarDLL/squirrelclasstypes.h | 239 ---- NorthstarDLL/squirreldatatypes.h | 495 -------- NorthstarDLL/structs.h | 63 - NorthstarDLL/tier0.cpp | 37 - NorthstarDLL/tier0.h | 68 - NorthstarDLL/util/printcommands.cpp | 174 +++ NorthstarDLL/util/printcommands.h | 6 + NorthstarDLL/util/printmaps.cpp | 168 +++ NorthstarDLL/util/printmaps.h | 2 + NorthstarDLL/util/version.cpp | 96 ++ NorthstarDLL/util/version.h | 6 + NorthstarDLL/vector.h | 52 - NorthstarDLL/version.cpp | 96 -- NorthstarDLL/version.h | 6 - 252 files changed, 22823 insertions(+), 22832 deletions(-) delete mode 100644 NorthstarDLL/audio.cpp delete mode 100644 NorthstarDLL/audio.h delete mode 100644 NorthstarDLL/bansystem.cpp delete mode 100644 NorthstarDLL/bansystem.h delete mode 100644 NorthstarDLL/bitbuf.h delete mode 100644 NorthstarDLL/bits.cpp delete mode 100644 NorthstarDLL/bits.h delete mode 100644 NorthstarDLL/buildainfile.cpp delete mode 100644 NorthstarDLL/chatcommand.cpp create mode 100644 NorthstarDLL/client/audio.cpp create mode 100644 NorthstarDLL/client/audio.h create mode 100644 NorthstarDLL/client/chatcommand.cpp create mode 100644 NorthstarDLL/client/clientauthhooks.cpp create mode 100644 NorthstarDLL/client/clientruihooks.cpp create mode 100644 NorthstarDLL/client/clientvideooverrides.cpp create mode 100644 NorthstarDLL/client/debugoverlay.cpp create mode 100644 NorthstarDLL/client/demofixes.cpp create mode 100644 NorthstarDLL/client/diskvmtfixes.cpp create mode 100644 NorthstarDLL/client/languagehooks.cpp create mode 100644 NorthstarDLL/client/latencyflex.cpp create mode 100644 NorthstarDLL/client/localchatwriter.cpp create mode 100644 NorthstarDLL/client/localchatwriter.h create mode 100644 NorthstarDLL/client/modlocalisation.cpp create mode 100644 NorthstarDLL/client/r2client.cpp create mode 100644 NorthstarDLL/client/r2client.h delete mode 100644 NorthstarDLL/clientauthhooks.cpp delete mode 100644 NorthstarDLL/clientchathooks.cpp delete mode 100644 NorthstarDLL/clientruihooks.cpp delete mode 100644 NorthstarDLL/clientvideooverrides.cpp delete mode 100644 NorthstarDLL/color.cpp delete mode 100644 NorthstarDLL/color.h delete mode 100644 NorthstarDLL/concommand.cpp delete mode 100644 NorthstarDLL/concommand.h create mode 100644 NorthstarDLL/config/profile.cpp create mode 100644 NorthstarDLL/config/profile.h delete mode 100644 NorthstarDLL/convar.cpp delete mode 100644 NorthstarDLL/convar.h create mode 100644 NorthstarDLL/core/convar/concommand.cpp create mode 100644 NorthstarDLL/core/convar/concommand.h create mode 100644 NorthstarDLL/core/convar/convar.cpp create mode 100644 NorthstarDLL/core/convar/convar.h create mode 100644 NorthstarDLL/core/convar/cvar.cpp create mode 100644 NorthstarDLL/core/convar/cvar.h create mode 100644 NorthstarDLL/core/filesystem/filesystem.cpp create mode 100644 NorthstarDLL/core/filesystem/filesystem.h create mode 100644 NorthstarDLL/core/filesystem/rpakfilesystem.cpp create mode 100644 NorthstarDLL/core/filesystem/rpakfilesystem.h create mode 100644 NorthstarDLL/core/hooks.cpp create mode 100644 NorthstarDLL/core/hooks.h create mode 100644 NorthstarDLL/core/math/bitbuf.h create mode 100644 NorthstarDLL/core/math/bits.cpp create mode 100644 NorthstarDLL/core/math/bits.h create mode 100644 NorthstarDLL/core/math/color.cpp create mode 100644 NorthstarDLL/core/math/color.h create mode 100644 NorthstarDLL/core/math/vector.h create mode 100644 NorthstarDLL/core/memalloc.cpp create mode 100644 NorthstarDLL/core/memalloc.h create mode 100644 NorthstarDLL/core/memory.cpp create mode 100644 NorthstarDLL/core/memory.h create mode 100644 NorthstarDLL/core/structs.h create mode 100644 NorthstarDLL/core/tier0.cpp create mode 100644 NorthstarDLL/core/tier0.h delete mode 100644 NorthstarDLL/crashhandler.cpp delete mode 100644 NorthstarDLL/crashhandler.h delete mode 100644 NorthstarDLL/cvar.cpp delete mode 100644 NorthstarDLL/cvar.h delete mode 100644 NorthstarDLL/debugoverlay.cpp delete mode 100644 NorthstarDLL/dedicated.cpp delete mode 100644 NorthstarDLL/dedicated.h create mode 100644 NorthstarDLL/dedicated/dedicated.cpp create mode 100644 NorthstarDLL/dedicated/dedicated.h create mode 100644 NorthstarDLL/dedicated/dedicatedmaterialsystem.cpp delete mode 100644 NorthstarDLL/dedicatedmaterialsystem.cpp delete mode 100644 NorthstarDLL/demofixes.cpp delete mode 100644 NorthstarDLL/diskvmtfixes.cpp create mode 100644 NorthstarDLL/dllmain.h create mode 100644 NorthstarDLL/engine/host.cpp create mode 100644 NorthstarDLL/engine/hoststate.cpp create mode 100644 NorthstarDLL/engine/hoststate.h create mode 100644 NorthstarDLL/engine/r2engine.cpp create mode 100644 NorthstarDLL/engine/r2engine.h create mode 100644 NorthstarDLL/engine/runframe.cpp delete mode 100644 NorthstarDLL/exploitfixes.cpp delete mode 100644 NorthstarDLL/exploitfixes_lzss.cpp delete mode 100644 NorthstarDLL/exploitfixes_utf8parser.cpp delete mode 100644 NorthstarDLL/filesystem.cpp delete mode 100644 NorthstarDLL/filesystem.h delete mode 100644 NorthstarDLL/hooks.cpp delete mode 100644 NorthstarDLL/hooks.h delete mode 100644 NorthstarDLL/host.cpp delete mode 100644 NorthstarDLL/hoststate.cpp delete mode 100644 NorthstarDLL/hoststate.h delete mode 100644 NorthstarDLL/httprequesthandler.cpp delete mode 100644 NorthstarDLL/httprequesthandler.h delete mode 100644 NorthstarDLL/kb_act.cpp delete mode 100644 NorthstarDLL/keyvalues.cpp delete mode 100644 NorthstarDLL/keyvalues.h delete mode 100644 NorthstarDLL/languagehooks.cpp delete mode 100644 NorthstarDLL/latencyflex.cpp delete mode 100644 NorthstarDLL/limits.cpp delete mode 100644 NorthstarDLL/limits.h delete mode 100644 NorthstarDLL/localchatwriter.cpp delete mode 100644 NorthstarDLL/localchatwriter.h delete mode 100644 NorthstarDLL/logging.cpp delete mode 100644 NorthstarDLL/logging.h create mode 100644 NorthstarDLL/logging/crashhandler.cpp create mode 100644 NorthstarDLL/logging/crashhandler.h create mode 100644 NorthstarDLL/logging/logging.cpp create mode 100644 NorthstarDLL/logging/logging.h create mode 100644 NorthstarDLL/logging/loghooks.cpp create mode 100644 NorthstarDLL/logging/loghooks.h create mode 100644 NorthstarDLL/logging/sourceconsole.cpp create mode 100644 NorthstarDLL/logging/sourceconsole.h delete mode 100644 NorthstarDLL/loghooks.cpp delete mode 100644 NorthstarDLL/loghooks.h delete mode 100644 NorthstarDLL/main.h delete mode 100644 NorthstarDLL/masterserver.cpp delete mode 100644 NorthstarDLL/masterserver.h create mode 100644 NorthstarDLL/masterserver/masterserver.cpp create mode 100644 NorthstarDLL/masterserver/masterserver.h delete mode 100644 NorthstarDLL/maxplayers.cpp delete mode 100644 NorthstarDLL/maxplayers.h delete mode 100644 NorthstarDLL/memalloc.cpp delete mode 100644 NorthstarDLL/memalloc.h delete mode 100644 NorthstarDLL/memory.cpp delete mode 100644 NorthstarDLL/memory.h delete mode 100644 NorthstarDLL/misccommands.cpp delete mode 100644 NorthstarDLL/misccommands.h delete mode 100644 NorthstarDLL/miscserverfixes.cpp delete mode 100644 NorthstarDLL/miscserverscript.cpp delete mode 100644 NorthstarDLL/modkeyvalues.cpp delete mode 100644 NorthstarDLL/modlocalisation.cpp delete mode 100644 NorthstarDLL/modmanager.cpp delete mode 100644 NorthstarDLL/modmanager.h delete mode 100644 NorthstarDLL/modpdef.cpp create mode 100644 NorthstarDLL/mods/compiled/kb_act.cpp create mode 100644 NorthstarDLL/mods/compiled/modkeyvalues.cpp create mode 100644 NorthstarDLL/mods/compiled/modpdef.cpp create mode 100644 NorthstarDLL/mods/compiled/modscriptsrson.cpp create mode 100644 NorthstarDLL/mods/modmanager.cpp create mode 100644 NorthstarDLL/mods/modmanager.h delete mode 100644 NorthstarDLL/modscriptsrson.cpp delete mode 100644 NorthstarDLL/nsprefix.cpp delete mode 100644 NorthstarDLL/nsprefix.h delete mode 100644 NorthstarDLL/playlist.cpp delete mode 100644 NorthstarDLL/playlist.h delete mode 100644 NorthstarDLL/plugin_abi.h delete mode 100644 NorthstarDLL/plugins.cpp delete mode 100644 NorthstarDLL/plugins.h create mode 100644 NorthstarDLL/plugins/plugin_abi.h create mode 100644 NorthstarDLL/plugins/plugins.cpp create mode 100644 NorthstarDLL/plugins/plugins.h delete mode 100644 NorthstarDLL/printcommand.h delete mode 100644 NorthstarDLL/printcommands.cpp delete mode 100644 NorthstarDLL/printmaps.cpp delete mode 100644 NorthstarDLL/printmaps.h delete mode 100644 NorthstarDLL/r2client.cpp delete mode 100644 NorthstarDLL/r2client.h delete mode 100644 NorthstarDLL/r2engine.cpp delete mode 100644 NorthstarDLL/r2engine.h delete mode 100644 NorthstarDLL/r2server.cpp delete mode 100644 NorthstarDLL/r2server.h delete mode 100644 NorthstarDLL/rpakfilesystem.cpp delete mode 100644 NorthstarDLL/rpakfilesystem.h delete mode 100644 NorthstarDLL/runframe.cpp delete mode 100644 NorthstarDLL/scriptbrowserhooks.cpp delete mode 100644 NorthstarDLL/scriptdatatables.cpp delete mode 100644 NorthstarDLL/scriptjson.cpp delete mode 100644 NorthstarDLL/scriptmainmenupromos.cpp delete mode 100644 NorthstarDLL/scriptmodmenu.cpp create mode 100644 NorthstarDLL/scripts/client/clientchathooks.cpp create mode 100644 NorthstarDLL/scripts/client/scriptbrowserhooks.cpp create mode 100644 NorthstarDLL/scripts/client/scriptmainmenupromos.cpp create mode 100644 NorthstarDLL/scripts/client/scriptmodmenu.cpp create mode 100644 NorthstarDLL/scripts/client/scriptserverbrowser.cpp create mode 100644 NorthstarDLL/scripts/client/scriptservertoclientstringcommand.cpp create mode 100644 NorthstarDLL/scripts/scriptdatatables.cpp create mode 100644 NorthstarDLL/scripts/scripthttprequesthandler.cpp create mode 100644 NorthstarDLL/scripts/scripthttprequesthandler.h create mode 100644 NorthstarDLL/scripts/scriptjson.cpp create mode 100644 NorthstarDLL/scripts/scriptutility.cpp create mode 100644 NorthstarDLL/scripts/server/miscserverfixes.cpp create mode 100644 NorthstarDLL/scripts/server/miscserverscript.cpp create mode 100644 NorthstarDLL/scripts/server/scriptuserinfo.cpp delete mode 100644 NorthstarDLL/scriptserverbrowser.cpp delete mode 100644 NorthstarDLL/scriptservertoclientstringcommand.cpp delete mode 100644 NorthstarDLL/scriptuserinfo.cpp delete mode 100644 NorthstarDLL/scriptutility.cpp create mode 100644 NorthstarDLL/server/auth/bansystem.cpp create mode 100644 NorthstarDLL/server/auth/bansystem.h create mode 100644 NorthstarDLL/server/auth/serverauthentication.cpp create mode 100644 NorthstarDLL/server/auth/serverauthentication.h create mode 100644 NorthstarDLL/server/buildainfile.cpp create mode 100644 NorthstarDLL/server/r2server.cpp create mode 100644 NorthstarDLL/server/r2server.h create mode 100644 NorthstarDLL/server/serverchathooks.cpp create mode 100644 NorthstarDLL/server/serverchathooks.h create mode 100644 NorthstarDLL/server/serverpresence.cpp create mode 100644 NorthstarDLL/server/serverpresence.h delete mode 100644 NorthstarDLL/serverauthentication.cpp delete mode 100644 NorthstarDLL/serverauthentication.h delete mode 100644 NorthstarDLL/serverchathooks.cpp delete mode 100644 NorthstarDLL/serverchathooks.h delete mode 100644 NorthstarDLL/serverpresence.cpp delete mode 100644 NorthstarDLL/serverpresence.h create mode 100644 NorthstarDLL/shared/exploit_fixes/exploitfixes.cpp create mode 100644 NorthstarDLL/shared/exploit_fixes/exploitfixes_lzss.cpp create mode 100644 NorthstarDLL/shared/exploit_fixes/exploitfixes_utf8parser.cpp create mode 100644 NorthstarDLL/shared/exploit_fixes/ns_limits.cpp create mode 100644 NorthstarDLL/shared/exploit_fixes/ns_limits.h create mode 100644 NorthstarDLL/shared/keyvalues.cpp create mode 100644 NorthstarDLL/shared/keyvalues.h create mode 100644 NorthstarDLL/shared/maxplayers.cpp create mode 100644 NorthstarDLL/shared/maxplayers.h create mode 100644 NorthstarDLL/shared/misccommands.cpp create mode 100644 NorthstarDLL/shared/misccommands.h create mode 100644 NorthstarDLL/shared/playlist.cpp create mode 100644 NorthstarDLL/shared/playlist.h create mode 100644 NorthstarDLL/shared/sourceinterface.cpp create mode 100644 NorthstarDLL/shared/sourceinterface.h delete mode 100644 NorthstarDLL/sourceconsole.cpp delete mode 100644 NorthstarDLL/sourceconsole.h delete mode 100644 NorthstarDLL/sourceinterface.cpp delete mode 100644 NorthstarDLL/sourceinterface.h delete mode 100644 NorthstarDLL/squirrel.cpp delete mode 100644 NorthstarDLL/squirrel.h create mode 100644 NorthstarDLL/squirrel/squirrel.cpp create mode 100644 NorthstarDLL/squirrel/squirrel.h create mode 100644 NorthstarDLL/squirrel/squirrelautobind.cpp create mode 100644 NorthstarDLL/squirrel/squirrelautobind.h create mode 100644 NorthstarDLL/squirrel/squirrelclasstypes.h create mode 100644 NorthstarDLL/squirrel/squirreldatatypes.h delete mode 100644 NorthstarDLL/squirrelautobind.cpp delete mode 100644 NorthstarDLL/squirrelautobind.h delete mode 100644 NorthstarDLL/squirrelclasstypes.h delete mode 100644 NorthstarDLL/squirreldatatypes.h delete mode 100644 NorthstarDLL/structs.h delete mode 100644 NorthstarDLL/tier0.cpp delete mode 100644 NorthstarDLL/tier0.h create mode 100644 NorthstarDLL/util/printcommands.cpp create mode 100644 NorthstarDLL/util/printcommands.h create mode 100644 NorthstarDLL/util/printmaps.cpp create mode 100644 NorthstarDLL/util/printmaps.h create mode 100644 NorthstarDLL/util/version.cpp create mode 100644 NorthstarDLL/util/version.h delete mode 100644 NorthstarDLL/vector.h delete mode 100644 NorthstarDLL/version.cpp delete mode 100644 NorthstarDLL/version.h (limited to 'NorthstarDLL') diff --git a/NorthstarDLL/NorthstarDLL.vcxproj b/NorthstarDLL/NorthstarDLL.vcxproj index e0dae8a2..7a57fce9 100644 --- a/NorthstarDLL/NorthstarDLL.vcxproj +++ b/NorthstarDLL/NorthstarDLL.vcxproj @@ -62,7 +62,7 @@ Use pch.h stdcpp20 - $(SolutionDir)include;%(AdditionalIncludeDirectories) + $(ProjectDir);$(SolutionDir)include;%(AdditionalIncludeDirectories) Windows @@ -92,7 +92,7 @@ Use pch.h stdcpp20 - $(SolutionDir)include;%(AdditionalIncludeDirectories) + $(ProjectDir);$(SolutionDir)include;%(AdditionalIncludeDirectories) MultiThreadedDLL Disabled @@ -390,146 +390,148 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Use + + + + + + + + + + + + + + + Create Create - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - diff --git a/NorthstarDLL/NorthstarDLL.vcxproj.filters b/NorthstarDLL/NorthstarDLL.vcxproj.filters index a100b2b0..5eb2d429 100644 --- a/NorthstarDLL/NorthstarDLL.vcxproj.filters +++ b/NorthstarDLL/NorthstarDLL.vcxproj.filters @@ -16,12 +16,6 @@ {d4199e4b-10d2-43ce-af9c-e1fa79e1e64e} - - {b6f79919-9735-476d-8798-067a75cbeca0} - - - {ca657be5-c2d8-4322-a689-1154aaafe57b} - {8596cc1c-0492-4467-91e3-1f03b7e19f77} @@ -52,257 +46,143 @@ {85aacdee-0f92-4ec4-b20c-0739c1175055} - - {3d41d3fc-8a3b-4358-b3e8-4f06dc96abfe} - - - {d69760a9-d5ec-4f3e-8f43-f74041654d44} - - - {365e5c1f-4b2f-4d8b-a1d8-cdef401ca689} - - - {24fd0855-9288-4129-93ba-c6cafdc98d1b} - {4cb0dd89-5f16-4549-a864-34ca3075352a} {ea1e17a6-40b7-4e1b-8edb-e9ae704ce604} - - {51910ba0-2ff8-461d-9f67-8d7907b57d22} + + {757f33f0-f17b-4ecd-8b95-2157b2d2f03c} + + + {7f9657c5-d4c7-464f-b2f6-d9afbbe0bb42} + + + {e45d18f7-8992-47ac-837c-4cb99f5a0487} + + + {62167757-13c1-4f57-a8f5-15a3c800997b} + + + {25e33568-4125-406d-8661-01ae2abf61a5} + + + {008e67ce-bf6a-4792-9ded-69a434edea00} + + + {dbf03bda-dd0a-44ce-a6b1-361f7f21a10e} + + + {d83e1c5c-c9c7-47a2-b406-9b5a93e3cc29} + + + {57fb0fa6-7665-4d37-bb2b-6f5570023d83} + + + {08436037-e86f-4410-8db6-37c8b10ca5a1} + + + {e8fb33f4-47b5-4fd9-b6c0-2ca1bd8cac81} + + + {d60e54d0-b4e4-4210-af6b-59bdd84bed0f} + + + {06c7928e-bf66-446e-9cde-c8135c79d14b} + + + {0b8b4a41-6131-4388-85d2-f5754a304610} + + + {9ccd8616-d961-4c37-96e0-3219bd293afb} + + + {a9390f6a-5031-4c65-a636-82cf90899435} + + + {acf049ba-358a-4520-8813-2f802ce7167e} + + + {0075910c-80c9-490f-a0d4-f9e410e4a918} - - {325e0d7d-6832-496d-8d8e-968fdfa5dd40} + + {31143dec-abe6-477a-9cae-045c5b57380b} - - {802d0771-62f1-4733-89f9-57a4d8864b8d} + + {c6c4746d-d6e1-4dba-a3fc-e645b28ac83f} - - {04fd662a-6e70-494c-b720-c694a5cc2fb1} + + {3fba68ef-d715-4e18-b4a2-05d3fee00e2d} - - {a18afb37-5fdd-4340-a6b4-a6541593e398} + + {ae9c5353-a3ab-44a9-a328-91df6aa8c80e} - - {4a8a695a-a103-4b1f-b314-0ec19a253119} + + {87869634-fa0f-48f8-a306-94d5a8157e1e} - - {d8a83b5e-9a23-4124-824f-eab37880cb08} + + {0ad64562-eb98-4015-a4fc-6fdc63ac9322} - - {2cbddb28-0b17-4881-847d-8773da52b268} + + {13d0affc-1b4d-4090-8ad6-e9ae8fc56599} - - {4db0d1e9-9035-457f-87f1-5dc3f13b6b9e} + + {b264a27d-619b-49b3-acbb-4e138ba29daf} - - {59b0f68f-daa7-4641-b6fa-8464b56da2bb} + + {35658430-465d-433c-aa2d-b8266edb205a} - - {3e892d07-2239-44da-9cf3-c288a34cf9a2} + + {0517ea0b-2eca-4b1c-a0d1-766d4d1d19d2} - - {14fc0931-acad-46ec-a55e-94f4469d4235} + + {6d3f7657-6e58-4007-acd3-0b787f35406a} - - {947835db-67d6-42c0-870d-62743f85231f} + + {b9c0cfad-46d4-41ea-806e-daef3d66c381} - - {bf0769d8-40fd-4701-85e9-7ed94aab2283} + + {a4f7d529-0b21-48af-82ef-9fb971b802f7} - - {9751b551-5886-45d4-a039-cbd10445263d} + + {7ee4b436-a5d3-4680-bf99-bc6d7640ded3} - - {96101d42-72af-4fd1-8559-8d1d1ff66240} + + {53a84980-9a1c-46b0-9b6d-06ec35781297} - - {ee3ba13a-3061-41d7-981d-328ac2596fd2} + + {01e215c3-9fa2-4de1-a3e0-b6a27396c68c} - - {0c93d909-e0d6-4c35-a8a4-a13f681a1012} + + {0fa069c9-302b-4be5-ba47-37e13e181d78} - - {94259c8c-5411-48bf-af4f-46ca32b7d0bb} + + {08a9ca37-8a6e-458c-9da8-6f3dc917ddf7} - - {44a83740-9d70-480d-9a7a-43b81f8eab9e} + + {2c0b196e-3a11-4655-a158-8a1c84be4471} - - {6bbce8a5-38b4-4763-a7cb-4e98012ec245} + + {122e2230-0605-40ef-8963-6e339d2c725e} - - {826d5193-3ad0-434b-ba7c-dd24ed4bbd0c} + + {4e0d0f0e-c8f2-4fe1-a787-57060c610553} - - {0f1ba4c4-78ee-4b05-afa5-6f598063f5c1} + + {9e67fc99-0e20-4155-a167-5d251ea36581} - - {ca669b16-b8bb-4654-993f-fffa44c914f1} + + {62b3cff9-b2d8-4c1c-a2a6-1e2138c0ff88} - - {26365f16-ff52-4e80-a01b-2ca020376c93} + + {9a819cdd-ebca-4cba-b231-76b6f98ed6ac} - - {7263403a-7550-4aa2-a724-f622ab200eed} + + {1a377c09-bd3d-4757-b3bc-9cd0a1e6ac0d} - - Header Files - - - Header Files - - - Header Files\Client - - - Header Files\Server\Authentication - - - Header Files\Server\Authentication - - - Header Files\Client - - - Header Files\Client - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files\Server\Scripted - - - Header Files\Dedicated Server - - - Header Files - - - Source Files\Exploit Fixes - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files\Math - - - Header Files\Math - - - Header Files\Convar - - - Header Files\Convar - - - Header Files\Convar - - - Header Files\Filesystem - - - Header Files\Hooks - - - Header Files\Exploit Fixes - - - Header Files\Console - - - Header Files\Convar - - - Header Files\Mods - - - Header Files\Console - - - Header Files\Console - - - Header Files\Game Functions - - - Header Files\Game Functions - - - Header Files\Game Functions - - - Header Files\Game Functions - - - Header Files\Filesystem - - - Header Files\Math - - - Header Files\Server - - - Header Files\Hooks - - - Header Files - - - Header Files\Squirrel - - - Header Files\Squirrel - - - Header Files\Math - - - Header Files\Squirrel - - - Header Files - - - Header Files - - - Header Files\Console - - - Header Files\Squirrel - Header Files\include @@ -1116,274 +996,410 @@ Header Files\include\spdlog\sinks - - - Header Files\Squirrel - - - Header Files - - Header Files\include\spdlog\sinks - Header Files\include\spdlog\sinks - - - Header Files\Console - - - Header Files\Squirrel - Header Files\include\spdlog\sinks - - + + Header Files\client + + + Header Files\client + + + Header Files\dedicated + + + Header Files\exploit_fixes + + + Header Files\hooks + + + Header Files\hooks + + + Header Files\hooks + + + Header Files\masterserver + + + Header Files\mods + + + Header Files\plugins + + + Header Files\plugins + + + Header Files\server + + + Header Files\server\auth + + + Header Files\server\auth + + + Header Files\squirrel + + + Header Files\squirrel + + + Header Files\squirrel + + + Header Files\squirrel + + + Header Files\util + + Header Files + + Header Files + + + Header Files + + + Header Files\config + + + Header Files\engine + + + Header Files\hooks + + + Header Files\shared + + + Header Files\shared + + + Header Files\client + + + Header Files\engine + + + Header Files\server + + + Header Files\hooks + + + Header Files\hooks + + + Header Files\core\convar + + + Header Files\core\convar + + + Header Files\core\convar + + + Header Files\core\filesystem + + + Header Files\core\filesystem + + + Header Files\core\math + + + Header Files\core\math + + + Header Files\core\math + + + Header Files\core\math + + + Header Files\core + + + Header Files\core + + + Header Files\core + + + Header Files\core + + + Header Files\core + + + Header Files\logging + + + Header Files\logging + + + Header Files\logging + + + Header Files\logging + + + Header Files\server + + + Header Files\scripts + - - Source Files - - - Source Files\Dedicated Server - - - Source Files\Client - - - Source Files\Mods + + Header Files\include\spdlog\fmt\bundled + + + + + Source Files\client - - Source Files\Mods\Compiled Assets + + Source Files\client - - Source Files\Server\Authentication + + Source Files\client - - Source Files\Mods\Compiled Assets + + Source Files\client - - Source Files\Client + + Source Files\client - - Source Files\Client + + Source Files\client - - Source Files\Dedicated Server + + Source Files\client - - Source Files\Mods\Compiled Assets + + Source Files\client - - Source Files\Client + + Source Files\client - - Source Files\Server + + Source Files\client - - Source Files\Server\Authentication + + Source Files\client - - Source Files\Client + + Source Files\client - - Source Files\Client + + Source Files\dedicated - - Source Files\Client + + Source Files\dedicated - - Source Files\Server + + Source Files\masterserver - - Source Files\Client + + Source Files\mods - - Source Files + + Source Files\mods\compiled - - Source Files\Client + + Source Files\mods\compiled - - Source Files\Client + + Source Files\mods\compiled - - Source Files + + Source Files\mods\compiled - - Source Files\Client + + Source Files\plugins - - Source Files\Client\Scripted + + Source Files\scripts - - Source Files\Client\Scripted + + Source Files\scripts - - Source Files\Client\Scripted + + Source Files\scripts - - Source Files\Client\Scripted + + Source Files\server - - Source Files\Client\Scripted + + Source Files\server - - Source Files\Client\Scripted + + Source Files\server\auth - - Source Files\Server\Scripted + + Source Files\server\auth - - Source Files\Server\Scripted + + Source Files\squirrel - - Source Files\Client + + Source Files\squirrel - - Source Files + + Source Files\util Source Files - + Source Files - - Source Files + + Source Files\config - - Source Files + + Source Files\engine - - Source Files + + Source Files\engine - - Source Files + + Source Files\engine - - Source Files + + Source Files\shared - - Source Files + + Source Files\shared - - Source Files + + Source Files\client - - Source Files\Game Functions + + Source Files\server - - Source Files\Game Functions + + Source Files\engine - - Source Files\Game Functions + + Source Files\logging - - Source Files\Filesystem + + Source Files\logging - - Source Files\Filesystem + + Source Files\logging - - Source Files\Exploit Fixes + + Source Files\logging - - Source Files\Exploit Fixes + + Source Files\scripts\client - - Source Files\Hooks + + Source Files\scripts\client - - Source Files\Math + + Source Files\scripts\client - - Source Files\Convar + + Source Files\scripts\client - - Source Files\Convar + + Source Files\scripts\client - - Source Files\Console + + Source Files\scripts\client - - Source Files\Console + + Source Files\scripts\server - - Source Files\Convar + + Source Files\scripts\server - - Source Files\Convar + + Source Files\scripts\server - - Source Files\Game Functions + + Source Files\server - - Source Files\Console + + Source Files\core\convar - - Source Files\Server + + Source Files\core\convar - - Source Files + + Source Files\core\convar - - Source Files\Hooks + + Source Files\core\filesystem - - Source Files\Exploit Fixes + + Source Files\core\filesystem - - Source Files\Scripted + + Source Files\core\math - - Source Files\Scripted + + Source Files\core\math - - Source Files\Squirrel + + Source Files\core - - Source Files\Scripted + + Source Files\core - - Source Files\Squirrel + + Source Files\core - - Source Files\Mods\Compiled Assets + + Source Files\core - - Source Files\Client + + Source Files\shared\exploit_fixes - - Source Files + + Source Files\shared\exploit_fixes - - Source Files\Server\Scripted + + Source Files\shared\exploit_fixes - - Source Files\Math + + Source Files\shared\exploit_fixes - - Source Files\Console + + Source Files\shared - - Source Files + + Source Files\shared + + + Source Files\util + + + Source Files\util + + + Source Files\scripts - Source Files\Client + Source Files - - - Header Files\include\spdlog\fmt\bundled - - \ No newline at end of file diff --git a/NorthstarDLL/audio.cpp b/NorthstarDLL/audio.cpp deleted file mode 100644 index b1592b6f..00000000 --- a/NorthstarDLL/audio.cpp +++ /dev/null @@ -1,507 +0,0 @@ -#include "pch.h" -#include "audio.h" -#include "dedicated.h" -#include "convar.h" - -#include "rapidjson/error/en.h" -#include -#include -#include -#include - -AUTOHOOK_INIT() - -extern "C" -{ - // should be called only in LoadSampleMetadata_Hook - extern void* __fastcall Audio_GetParentEvent(); -} - -ConVar* Cvar_ns_print_played_sounds; - -CustomAudioManager g_CustomAudioManager; - -EventOverrideData::EventOverrideData() -{ - spdlog::warn("Initialised struct EventOverrideData without any data!"); - LoadedSuccessfully = false; -} - -// Empty stereo 48000 WAVE file -unsigned char EMPTY_WAVE[45] = {0x52, 0x49, 0x46, 0x46, 0x25, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6D, 0x74, - 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x44, 0xAC, 0x00, 0x00, 0x88, 0x58, - 0x01, 0x00, 0x02, 0x00, 0x10, 0x00, 0x64, 0x61, 0x74, 0x61, 0x74, 0x00, 0x00, 0x00, 0x00}; - -EventOverrideData::EventOverrideData(const std::string& data, const fs::path& path) -{ - if (data.length() <= 0) - { - spdlog::error("Failed reading audio override file {}: file is empty", path.string()); - return; - } - - fs::path samplesFolder = path; - samplesFolder = samplesFolder.replace_extension(); - - if (!fs::exists(samplesFolder)) - { - spdlog::error( - "Failed reading audio override file {}: samples folder doesn't exist; should be named the same as the definition file without " - "JSON extension.", - path.string()); - return; - } - - rapidjson_document dataJson; - dataJson.Parse(data); - - // fail if parse error - if (dataJson.HasParseError()) - { - spdlog::error( - "Failed reading audio override file {}: encountered parse error \"{}\" at offset {}", - path.string(), - GetParseError_En(dataJson.GetParseError()), - dataJson.GetErrorOffset()); - return; - } - - // fail if it's not a json obj (could be an array, string, etc) - if (!dataJson.IsObject()) - { - spdlog::error("Failed reading audio override file {}: file is not a JSON object", path.string()); - return; - } - - // fail if no event ids given - if (!dataJson.HasMember("EventId")) - { - spdlog::error("Failed reading audio override file {}: JSON object does not have the EventId property", path.string()); - return; - } - - // array of event ids - if (dataJson["EventId"].IsArray()) - { - for (auto& eventId : dataJson["EventId"].GetArray()) - { - if (!eventId.IsString()) - { - spdlog::error( - "Failed reading audio override file {}: EventId array has a value of invalid type, all must be strings", path.string()); - return; - } - - EventIds.push_back(eventId.GetString()); - } - } - // singular event id - else if (dataJson["EventId"].IsString()) - { - EventIds.push_back(dataJson["EventId"].GetString()); - } - // incorrect type - else - { - spdlog::error( - "Failed reading audio override file {}: EventId property is of invalid type (must be a string or an array of strings)", - path.string()); - return; - } - - if (dataJson.HasMember("EventIdRegex")) - { - // array of event id regex - if (dataJson["EventIdRegex"].IsArray()) - { - for (auto& eventId : dataJson["EventIdRegex"].GetArray()) - { - if (!eventId.IsString()) - { - spdlog::error( - "Failed reading audio override file {}: EventIdRegex array has a value of invalid type, all must be strings", - path.string()); - return; - } - - const std::string& regex = eventId.GetString(); - - try - { - EventIdsRegex.push_back({regex, std::regex(regex)}); - } - catch (...) - { - spdlog::error("Malformed regex \"{}\" in audio override file {}", regex, path.string()); - return; - } - } - } - // singular event id regex - else if (dataJson["EventIdRegex"].IsString()) - { - const std::string& regex = dataJson["EventIdRegex"].GetString(); - try - { - EventIdsRegex.push_back({regex, std::regex(regex)}); - } - catch (...) - { - spdlog::error("Malformed regex \"{}\" in audio override file {}", regex, path.string()); - return; - } - } - // incorrect type - else - { - spdlog::error( - "Failed reading audio override file {}: EventIdRegex property is of invalid type (must be a string or an array of strings)", - path.string()); - return; - } - } - - if (dataJson.HasMember("AudioSelectionStrategy")) - { - if (!dataJson["AudioSelectionStrategy"].IsString()) - { - spdlog::error("Failed reading audio override file {}: AudioSelectionStrategy property must be a string", path.string()); - return; - } - - std::string strategy = dataJson["AudioSelectionStrategy"].GetString(); - - if (strategy == "sequential") - { - Strategy = AudioSelectionStrategy::SEQUENTIAL; - } - else if (strategy == "random") - { - Strategy = AudioSelectionStrategy::RANDOM; - } - else - { - spdlog::error( - "Failed reading audio override file {}: AudioSelectionStrategy string must be either \"sequential\" or \"random\"", - path.string()); - return; - } - } - - // load samples - for (fs::directory_entry file : fs::recursive_directory_iterator(samplesFolder)) - { - if (file.is_regular_file() && file.path().extension().string() == ".wav") - { - std::string pathString = file.path().string(); - - // Open the file. - std::ifstream wavStream(pathString, std::ios::binary); - - if (wavStream.fail()) - { - spdlog::error("Failed reading audio sample {}", file.path().string()); - continue; - } - - // Get file size. - wavStream.seekg(0, std::ios::end); - size_t fileSize = wavStream.tellg(); - wavStream.close(); - - // Allocate enough memory for the file. - // blank out the memory for now, then read it later - uint8_t* data = new uint8_t[fileSize]; - memcpy(data, EMPTY_WAVE, sizeof(EMPTY_WAVE)); - Samples.push_back({fileSize, std::unique_ptr(data)}); - - // thread off the file read - // should we spawn one thread per read? or should there be a cap to the number of reads at once? - std::thread readThread( - [pathString, fileSize, data] - { - std::shared_lock lock(g_CustomAudioManager.m_loadingMutex); - std::ifstream wavStream(pathString, std::ios::binary); - - // would be weird if this got hit, since it would've worked previously - if (wavStream.fail()) - { - spdlog::error("Failed async read of audio sample {}", pathString); - return; - } - - // read from after the header first to preserve the empty header, then read the header last - wavStream.seekg(0, std::ios::beg); - wavStream.read(reinterpret_cast(data), fileSize); - wavStream.close(); - - spdlog::info("Finished async read of audio sample {}", pathString); - }); - - readThread.detach(); - } - } - - /* - if (dataJson.HasMember("EnableOnLoopedSounds")) - { - if (!dataJson["EnableOnLoopedSounds"].IsBool()) - { - spdlog::error("Failed reading audio override file {}: EnableOnLoopedSounds property is of invalid type (must be a bool)", - path.string()); return; - } - - EnableOnLoopedSounds = dataJson["EnableOnLoopedSounds"].GetBool(); - } - */ - - if (Samples.size() == 0) - spdlog::warn("Audio override {} has no valid samples! Sounds will not play for this event.", path.string()); - - spdlog::info("Loaded audio override file {}", path.string()); - - LoadedSuccessfully = true; -} - -bool CustomAudioManager::TryLoadAudioOverride(const fs::path& defPath) -{ - if (IsDedicatedServer()) - return true; // silently fail - - std::ifstream jsonStream(defPath); - std::stringstream jsonStringStream; - - // fail if no audio json - if (jsonStream.fail()) - { - spdlog::warn("Unable to read audio override from file {}", defPath.string()); - return false; - } - - while (jsonStream.peek() != EOF) - jsonStringStream << (char)jsonStream.get(); - - jsonStream.close(); - - std::shared_ptr data = std::make_shared(jsonStringStream.str(), defPath); - - if (!data->LoadedSuccessfully) - return false; // no logging, the constructor has probably already logged - - for (const std::string& eventId : data->EventIds) - { - spdlog::info("Registering sound event {}", eventId); - m_loadedAudioOverrides.insert({eventId, data}); - } - - for (const auto& eventIdRegexData : data->EventIdsRegex) - { - spdlog::info("Registering sound event regex {}", eventIdRegexData.first); - m_loadedAudioOverridesRegex.insert({eventIdRegexData.first, data}); - } - - return true; -} - -typedef void (*MilesStopAll_Type)(); -MilesStopAll_Type MilesStopAll; - -void CustomAudioManager::ClearAudioOverrides() -{ - if (IsDedicatedServer()) - return; - - if (m_loadedAudioOverrides.size() > 0 || m_loadedAudioOverridesRegex.size() > 0) - { - // stop all miles sounds beforehand - // miles_stop_all - - MilesStopAll(); - - // this is cancer but it works - Sleep(50); - } - - // slightly (very) bad - // wait for all audio reads to complete so we don't kill preexisting audio buffers as we're writing to them - std::unique_lock lock(m_loadingMutex); - - m_loadedAudioOverrides.clear(); - m_loadedAudioOverridesRegex.clear(); -} - -template Iter select_randomly(Iter start, Iter end, RandomGenerator& g) -{ - std::uniform_int_distribution<> dis(0, std::distance(start, end) - 1); - std::advance(start, dis(g)); - return start; -} - -template Iter select_randomly(Iter start, Iter end) -{ - static std::random_device rd; - static std::mt19937 gen(rd()); - return select_randomly(start, end, gen); -} - -bool ShouldPlayAudioEvent(const char* eventName, const std::shared_ptr& data) -{ - std::string eventNameString = eventName; - std::string eventNameStringBlacklistEntry = ("!" + eventNameString); - - for (const std::string& name : data->EventIds) - { - if (name == eventNameStringBlacklistEntry) - return false; // event blacklisted - - if (name == "*") - { - // check for bad sounds I guess? - // really feel like this should be an option but whatever - if (!!strstr(eventName, "_amb_") || !!strstr(eventName, "_emit_") || !!strstr(eventName, "amb_")) - return false; // would play static noise, I hate this - } - } - - return true; // good to go -} - -// forward declare -bool __declspec(noinline) __fastcall LoadSampleMetadata_Internal( - uintptr_t parentEvent, void* sample, void* audioBuffer, unsigned int audioBufferLength, int audioType); - -// DO NOT TOUCH THIS FUNCTION -// The actual logic of it in a separate function (forcefully not inlined) to preserve the r12 register, which holds the event pointer. -// clang-format off -AUTOHOOK(LoadSampleMetadata, mileswin64.dll + 0xF110, -bool, __fastcall, (void* sample, void* audioBuffer, unsigned int audioBufferLength, int audioType)) -// clang-format on -{ - uintptr_t parentEvent = (uintptr_t)Audio_GetParentEvent(); - - // Raw source, used for voice data only - if (audioType == 0) - return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType); - - return LoadSampleMetadata_Internal(parentEvent, sample, audioBuffer, audioBufferLength, audioType); -} - -// DO NOT INLINE THIS FUNCTION -// See comment below. -bool __declspec(noinline) __fastcall LoadSampleMetadata_Internal( - uintptr_t parentEvent, void* sample, void* audioBuffer, unsigned int audioBufferLength, int audioType) -{ - char* eventName = (char*)parentEvent + 0x110; - - if (Cvar_ns_print_played_sounds->GetInt() > 0) - spdlog::info("[AUDIO] Playing event {}", eventName); - - auto iter = g_CustomAudioManager.m_loadedAudioOverrides.find(eventName); - std::shared_ptr overrideData; - - if (iter == g_CustomAudioManager.m_loadedAudioOverrides.end()) - { - // override for that specific event not found, try wildcard - iter = g_CustomAudioManager.m_loadedAudioOverrides.find("*"); - - if (iter == g_CustomAudioManager.m_loadedAudioOverrides.end()) - { - // not found - - // try regex - for (const auto& item : g_CustomAudioManager.m_loadedAudioOverridesRegex) - for (const auto& regexData : item.second->EventIdsRegex) - if (std::regex_search(eventName, regexData.second)) - overrideData = item.second; - - if (!overrideData) - // not found either - return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType); - else - { - // cache found pattern to improve performance - g_CustomAudioManager.m_loadedAudioOverrides[eventName] = overrideData; - } - } - else - overrideData = iter->second; - } - else - overrideData = iter->second; - - if (!ShouldPlayAudioEvent(eventName, overrideData)) - return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType); - - void* data = 0; - unsigned int dataLength = 0; - - if (overrideData->Samples.size() == 0) - { - // 0 samples, turn off this particular event. - - // using a dummy empty wave file - data = EMPTY_WAVE; - dataLength = sizeof(EMPTY_WAVE); - } - else - { - std::pair>* dat = NULL; - - switch (overrideData->Strategy) - { - case AudioSelectionStrategy::RANDOM: - dat = &*select_randomly(overrideData->Samples.begin(), overrideData->Samples.end()); - break; - case AudioSelectionStrategy::SEQUENTIAL: - default: - dat = &overrideData->Samples[overrideData->CurrentIndex++]; - if (overrideData->CurrentIndex >= overrideData->Samples.size()) - overrideData->CurrentIndex = 0; // reset back to the first sample entry - break; - } - - if (!dat) - spdlog::warn("Could not get sample data from override struct for event {}! Shouldn't happen", eventName); - else - { - data = dat->second.get(); - dataLength = dat->first; - } - } - - if (!data) - { - spdlog::warn("Could not fetch override sample data for event {}! Using original data instead.", eventName); - return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType); - } - - audioBuffer = data; - audioBufferLength = dataLength; - - // most important change: set the sample class buffer so that the correct audio plays - *(void**)((uintptr_t)sample + 0xE8) = audioBuffer; - *(unsigned int*)((uintptr_t)sample + 0xF0) = audioBufferLength; - - // 64 - Auto-detect sample type - bool res = LoadSampleMetadata(sample, audioBuffer, audioBufferLength, 64); - if (!res) - spdlog::error("LoadSampleMetadata failed! The game will crash :("); - - return res; -} - -// clang-format off -AUTOHOOK(MilesLog, client.dll + 0x57DAD0, -void, __fastcall, (int level, const char* string)) -// clang-format on -{ - spdlog::info("[MSS] {} - {}", level, string); -} - -ON_DLL_LOAD_CLIENT_RELIESON("client.dll", AudioHooks, ConVar, (CModule module)) -{ - AUTOHOOK_DISPATCH() - - Cvar_ns_print_played_sounds = new ConVar("ns_print_played_sounds", "0", FCVAR_NONE, ""); - MilesStopAll = module.Offset(0x580850).As(); -} diff --git a/NorthstarDLL/audio.h b/NorthstarDLL/audio.h deleted file mode 100644 index 26cda205..00000000 --- a/NorthstarDLL/audio.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -enum class AudioSelectionStrategy -{ - INVALID = -1, - SEQUENTIAL, - RANDOM -}; - -class EventOverrideData -{ - public: - EventOverrideData(const std::string&, const fs::path&); - EventOverrideData(); - - public: - bool LoadedSuccessfully = false; - - std::vector EventIds = {}; - std::vector> EventIdsRegex = {}; - - std::vector>> Samples = {}; - - AudioSelectionStrategy Strategy = AudioSelectionStrategy::SEQUENTIAL; - size_t CurrentIndex = 0; - - bool EnableOnLoopedSounds = false; -}; - -class CustomAudioManager -{ - public: - bool TryLoadAudioOverride(const fs::path&); - void ClearAudioOverrides(); - - std::shared_mutex m_loadingMutex; - std::unordered_map> m_loadedAudioOverrides = {}; - std::unordered_map> m_loadedAudioOverridesRegex = {}; -}; - -extern CustomAudioManager g_CustomAudioManager; diff --git a/NorthstarDLL/bansystem.cpp b/NorthstarDLL/bansystem.cpp deleted file mode 100644 index 25c0e6bf..00000000 --- a/NorthstarDLL/bansystem.cpp +++ /dev/null @@ -1,222 +0,0 @@ -#pragma once -#include "pch.h" -#include "bansystem.h" -#include "serverauthentication.h" -#include "maxplayers.h" -#include "concommand.h" -#include "r2server.h" -#include "r2engine.h" -#include "nsprefix.h" - -#include - -const char* BANLIST_PATH_SUFFIX = "/banlist.txt"; -const char BANLIST_COMMENT_CHAR = '#'; - -ServerBanSystem* g_pBanSystem; - -void ServerBanSystem::OpenBanlist() -{ - std::ifstream banlistStream(GetNorthstarPrefix() + "/banlist.txt"); - - if (!banlistStream.fail()) - { - std::string line; - while (std::getline(banlistStream, line)) - { - // ignore line if first char is # or line is empty - if (line == "" || line.front() == BANLIST_COMMENT_CHAR) - continue; - - // remove tabs which shouldnt be there but maybe someone did the funny - line.erase(std::remove(line.begin(), line.end(), '\t'), line.end()); - // remove spaces to allow for spaces before uids - line.erase(std::remove(line.begin(), line.end(), ' '), line.end()); - - // check if line is empty to allow for newlines in the file - if (line == "") - continue; - - // for inline comments like: 123123123 #banned for unfunny - std::string uid = line.substr(0, line.find(BANLIST_COMMENT_CHAR)); - - m_vBannedUids.push_back(strtoull(uid.c_str(), nullptr, 10)); - } - - banlistStream.close(); - } - - // open write stream for banlist // dont do this to allow for all time access - // m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary | std::ofstream::app); -} - -void ServerBanSystem::ReloadBanlist() -{ - std::ifstream fsBanlist(GetNorthstarPrefix() + "/banlist.txt"); - - if (!fsBanlist.fail()) - { - std::string line; - // since we wanna use this as the reload func we need to clear the list - m_vBannedUids.clear(); - while (std::getline(fsBanlist, line)) - m_vBannedUids.push_back(strtoull(line.c_str(), nullptr, 10)); - - fsBanlist.close(); - } -} - -void ServerBanSystem::ClearBanlist() -{ - m_vBannedUids.clear(); - - // reopen the file, don't provide std::ofstream::app so it clears on open - m_sBanlistStream.close(); - m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary); - m_sBanlistStream.close(); -} - -void ServerBanSystem::BanUID(uint64_t uid) -{ - // checking if last char is \n to make sure uids arent getting fucked - std::ifstream fsBanlist(GetNorthstarPrefix() + "/banlist.txt"); - std::string content((std::istreambuf_iterator(fsBanlist)), (std::istreambuf_iterator())); - fsBanlist.close(); - - m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary | std::ofstream::app); - if (content.back() != '\n') - m_sBanlistStream << std::endl; - - m_vBannedUids.push_back(uid); - m_sBanlistStream << std::to_string(uid) << std::endl; - m_sBanlistStream.close(); - spdlog::info("{} was banned", uid); -} - -void ServerBanSystem::UnbanUID(uint64_t uid) -{ - auto findResult = std::find(m_vBannedUids.begin(), m_vBannedUids.end(), uid); - if (findResult == m_vBannedUids.end()) - return; - - m_vBannedUids.erase(findResult); - - std::vector banlistText; - std::ifstream fs_readBanlist(GetNorthstarPrefix() + "/banlist.txt"); - - if (!fs_readBanlist.fail()) - { - std::string line; - while (std::getline(fs_readBanlist, line)) - { - // support for comments and newlines added in https://github.com/R2Northstar/NorthstarLauncher/pull/227 - - std::string modLine = line; // copy the line into a free var that we can fuck with, line will be the original - - // remove tabs which shouldnt be there but maybe someone did the funny - modLine.erase(std::remove(modLine.begin(), modLine.end(), '\t'), modLine.end()); - // remove spaces to allow for spaces before uids - modLine.erase(std::remove(modLine.begin(), modLine.end(), ' '), modLine.end()); - - // ignore line if first char is # or empty line, just add it - if (line.front() == BANLIST_COMMENT_CHAR || modLine == "") - { - banlistText.push_back(line); - continue; - } - - // for inline comments like: 123123123 #banned for unfunny - std::string lineUid = line.substr(0, line.find(BANLIST_COMMENT_CHAR)); - // have to erase spaces or else inline comments will fuck up the uid finding - lineUid.erase(std::remove(lineUid.begin(), lineUid.end(), '\t'), lineUid.end()); - lineUid.erase(std::remove(lineUid.begin(), lineUid.end(), ' '), lineUid.end()); - - // if the uid in the line is the uid we wanna unban - if (std::to_string(uid) == lineUid) - { - // comment the uid out - line.insert(0, "# "); - - // add a comment with unban date - // not necessary but i feel like this makes it better - std::time_t t = std::time(0); - std::tm* now = std::localtime(&t); - - std::ostringstream unbanComment; - - //{y}/{m}/{d} {h}:{m} - unbanComment << " # unban date: "; - unbanComment << now->tm_year + 1900 << "-"; // this lib is so fucking awful - unbanComment << std::setw(2) << std::setfill('0') << now->tm_mon + 1 << "-"; - unbanComment << std::setw(2) << std::setfill('0') << now->tm_mday << " "; - unbanComment << std::setw(2) << std::setfill('0') << now->tm_hour << ":"; - unbanComment << std::setw(2) << std::setfill('0') << now->tm_min; - - line.append(unbanComment.str()); - } - - banlistText.push_back(line); - } - - fs_readBanlist.close(); - } - - // open write stream for banlist // without append so we clear the file - if (m_sBanlistStream.is_open()) - m_sBanlistStream.close(); - m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary); - - for (std::string updatedLine : banlistText) - m_sBanlistStream << updatedLine << std::endl; - - m_sBanlistStream.close(); - spdlog::info("{} was unbanned", uid); -} - -bool ServerBanSystem::IsUIDAllowed(uint64_t uid) -{ - ReloadBanlist(); // Reload to have up to date list on join - return std::find(m_vBannedUids.begin(), m_vBannedUids.end(), uid) == m_vBannedUids.end(); -} - -void ConCommand_ban(const CCommand& args) -{ - if (args.ArgC() < 2) - return; - - for (int i = 0; i < R2::GetMaxPlayers(); i++) - { - R2::CBaseClient* player = &R2::g_pClientArray[i]; - - if (!strcmp(player->m_Name, args.Arg(1)) || !strcmp(player->m_UID, args.Arg(1))) - { - g_pBanSystem->BanUID(strtoull(player->m_UID, nullptr, 10)); - R2::CBaseClient__Disconnect(player, 1, "Banned from server"); - break; - } - } -} - -void ConCommand_unban(const CCommand& args) -{ - if (args.ArgC() < 2) - return; - - // assumedly the player being unbanned here wasn't already connected, so don't need to iterate over players or anything - g_pBanSystem->UnbanUID(strtoull(args.Arg(1), nullptr, 10)); -} - -void ConCommand_clearbanlist(const CCommand& args) -{ - g_pBanSystem->ClearBanlist(); -} - -ON_DLL_LOAD_RELIESON("engine.dll", BanSystem, ConCommand, (CModule module)) -{ - 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_GAMEDLL); - RegisterConCommand("clearbanlist", ConCommand_clearbanlist, "clears all uids on the banlist", FCVAR_GAMEDLL); -} diff --git a/NorthstarDLL/bansystem.h b/NorthstarDLL/bansystem.h deleted file mode 100644 index 6f180126..00000000 --- a/NorthstarDLL/bansystem.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once -#include - -class ServerBanSystem -{ - private: - std::ofstream m_sBanlistStream; - std::vector m_vBannedUids; - - public: - void OpenBanlist(); - void ReloadBanlist(); - void ClearBanlist(); - void BanUID(uint64_t uid); - void UnbanUID(uint64_t uid); - bool IsUIDAllowed(uint64_t uid); -}; - -extern ServerBanSystem* g_pBanSystem; diff --git a/NorthstarDLL/bitbuf.h b/NorthstarDLL/bitbuf.h deleted file mode 100644 index 8e8e216f..00000000 --- a/NorthstarDLL/bitbuf.h +++ /dev/null @@ -1,1148 +0,0 @@ -#pragma once - -#define INLINE inline - -#define BITS_PER_INT 32 - -INLINE int GetBitForBitnum(int bitNum) -{ - static int bitsForBitnum[] = { - (1 << 0), (1 << 1), (1 << 2), (1 << 3), (1 << 4), (1 << 5), (1 << 6), (1 << 7), (1 << 8), (1 << 9), (1 << 10), - (1 << 11), (1 << 12), (1 << 13), (1 << 14), (1 << 15), (1 << 16), (1 << 17), (1 << 18), (1 << 19), (1 << 20), (1 << 21), - (1 << 22), (1 << 23), (1 << 24), (1 << 25), (1 << 26), (1 << 27), (1 << 28), (1 << 29), (1 << 30), (1 << 31), - }; - - return bitsForBitnum[(bitNum) & (BITS_PER_INT - 1)]; -} - -#undef BITS_PER_INT - -using u8 = uint8_t; -using u16 = uint16_t; -using u32 = uint32_t; -using u64 = uint64_t; -using uptr = uintptr_t; - -using i8 = int8_t; -using i16 = int16_t; -using i32 = int32_t; -using i64 = int64_t; -using iptr = intptr_t; - -// Endianess, don't use on PPC64 nor ARM64BE -#define LittleDWord(val) (val) - -static INLINE void StoreLittleDWord(u32* base, size_t dwordIndex, u32 dword) -{ - base[dwordIndex] = LittleDWord(dword); -} - -static INLINE u32 LoadLittleDWord(u32* base, size_t dwordIndex) -{ - return LittleDWord(base[dwordIndex]); -} - -#include - -static inline const u32 s_nMaskTable[33] = { - 0, - (1 << 1) - 1, - (1 << 2) - 1, - (1 << 3) - 1, - (1 << 4) - 1, - (1 << 5) - 1, - (1 << 6) - 1, - (1 << 7) - 1, - (1 << 8) - 1, - (1 << 9) - 1, - (1 << 10) - 1, - (1 << 11) - 1, - (1 << 12) - 1, - (1 << 13) - 1, - (1 << 14) - 1, - (1 << 15) - 1, - (1 << 16) - 1, - (1 << 17) - 1, - (1 << 18) - 1, - (1 << 19) - 1, - (1 << 20) - 1, - (1 << 21) - 1, - (1 << 22) - 1, - (1 << 23) - 1, - (1 << 24) - 1, - (1 << 25) - 1, - (1 << 26) - 1, - (1 << 27) - 1, - (1 << 28) - 1, - (1 << 29) - 1, - (1 << 30) - 1, - 0x7fffffff, - 0xffffffff, -}; - -enum EBitCoordType -{ - kCW_None, - kCW_LowPrecision, - kCW_Integral -}; - -class BitBufferBase -{ - protected: - INLINE void SetName(const char* name) - { - m_BufferName = name; - } - - public: - INLINE bool IsOverflowed() - { - return m_Overflow; - } - INLINE void SetOverflowed() - { - m_Overflow = true; - } - - INLINE const char* GetName() - { - return m_BufferName; - } - - private: - const char* m_BufferName = ""; - - protected: - u8 m_Overflow = false; -}; - -class BFRead : public BitBufferBase -{ - public: - BFRead() = default; - - INLINE BFRead(uptr data, size_t byteLength, size_t startPos = 0, const char* bufferName = 0) - { - StartReading(data, byteLength, startPos); - - if (bufferName) - SetName(bufferName); - } - - public: - INLINE void StartReading(uptr data, size_t byteLength, size_t startPos = 0) - { - m_Data = reinterpret_cast(data); - m_DataIn = m_Data; - - m_DataBytes = byteLength; - m_DataBits = byteLength << 3; - - m_DataEnd = reinterpret_cast(reinterpret_cast(m_Data) + m_DataBytes); - - Seek(startPos); - } - - INLINE void GrabNextDWord(bool overflow = false) - { - if (m_Data == m_DataEnd) - { - m_CachedBitsLeft = 1; - m_CachedBufWord = 0; - - m_DataIn++; - - if (overflow) - SetOverflowed(); - } - else - { - if (m_DataIn > m_DataEnd) - { - SetOverflowed(); - m_CachedBufWord = 0; - } - else - { - m_CachedBufWord = LittleDWord(*(m_DataIn++)); - } - } - } - - INLINE void FetchNext() - { - m_CachedBitsLeft = 32; - GrabNextDWord(false); - } - - INLINE i32 ReadOneBit() - { - i32 ret = m_CachedBufWord & 1; - - if (--m_CachedBitsLeft == 0) - FetchNext(); - else - m_CachedBufWord >>= 1; - - return ret; - } - - INLINE u32 ReadUBitLong(i32 numBits) - { - if (m_CachedBitsLeft >= numBits) - { - u32 ret = m_CachedBufWord & s_nMaskTable[numBits]; - - m_CachedBitsLeft -= numBits; - - if (m_CachedBitsLeft) - m_CachedBufWord >>= numBits; - else - FetchNext(); - - return ret; - } - else - { - // need to merge words - u32 ret = m_CachedBufWord; - numBits -= m_CachedBitsLeft; - - GrabNextDWord(true); - - if (IsOverflowed()) - return 0; - - ret |= ((m_CachedBufWord & s_nMaskTable[numBits]) << m_CachedBitsLeft); - - m_CachedBitsLeft = 32 - numBits; - m_CachedBufWord >>= numBits; - - return ret; - } - } - - INLINE i32 ReadSBitLong(int numBits) - { - i32 ret = ReadUBitLong(numBits); - return (ret << (32 - numBits)) >> (32 - numBits); - } - - INLINE u32 ReadUBitVar() - { - u32 ret = ReadUBitLong(6); - - switch (ret & (16 | 32)) - { - case 16: - ret = (ret & 15) | (ReadUBitLong(4) << 4); - // Assert(ret >= 16); - break; - case 32: - ret = (ret & 15) | (ReadUBitLong(8) << 4); - // Assert(ret >= 256); - break; - case 48: - ret = (ret & 15) | (ReadUBitLong(32 - 4) << 4); - // Assert(ret >= 4096); - break; - } - - return ret; - } - - INLINE u32 PeekUBitLong(i32 numBits) - { - i32 nSaveBA = m_CachedBitsLeft; - i32 nSaveW = m_CachedBufWord; - u32 const* pSaveP = m_DataIn; - u32 nRet = ReadUBitLong(numBits); - - m_CachedBitsLeft = nSaveBA; - m_CachedBufWord = nSaveW; - m_DataIn = pSaveP; - - return nRet; - } - - INLINE float ReadBitFloat() - { - u32 value = ReadUBitLong(32); - return *reinterpret_cast(&value); - } - - /*INLINE float ReadBitCoord() { - i32 intval = 0, fractval = 0, signbit = 0; - float value = 0.0; - - // Read the required integer and fraction flags - intval = ReadOneBit(); - fractval = ReadOneBit(); - - // If we got either parse them, otherwise it's a zero. - if (intval || fractval) { - // Read the sign bit - signbit = ReadOneBit(); - - // If there's an integer, read it in - if (intval) { - // Adjust the integers from [0..MAX_COORD_VALUE-1] to [1..MAX_COORD_VALUE] - intval = ReadUBitLong(COORD_INTEGER_BITS) + 1; - } - - // If there's a fraction, read it in - if (fractval) { - fractval = ReadUBitLong(COORD_FRACTIONAL_BITS); - } - - // Calculate the correct floating point value - value = intval + ((float)fractval * COORD_RESOLUTION); - - // Fixup the sign if negative. - if (signbit) - value = -value; - } - - return value; - } - - INLINE float ReadBitCoordMP() { - i32 intval = 0, fractval = 0, signbit = 0; - float value = 0.0; - - bool inBounds = ReadOneBit() ? true : false; - - // Read the required integer and fraction flags - intval = ReadOneBit(); - - // If we got either parse them, otherwise it's a zero. - if (intval) { - // Read the sign bit - signbit = ReadOneBit(); - - // If there's an integer, read it in - // Adjust the integers from [0..MAX_COORD_VALUE-1] to [1..MAX_COORD_VALUE] - if (inBounds) - value = ReadUBitLong(COORD_INTEGER_BITS_MP) + 1; - else - value = ReadUBitLong(COORD_INTEGER_BITS) + 1; - } - - // Fixup the sign if negative. - if (signbit) - value = -value; - - return value; - } - - INLINE float ReadBitCellCoord(int bits, EBitCoordType coordType) { - bool bIntegral = (coordType == kCW_Integral); - bool bLowPrecision = (coordType == kCW_LowPrecision); - - int intval = 0, fractval = 0; - float value = 0.0; - - if (bIntegral) - value = ReadUBitLong(bits); - else { - intval = ReadUBitLong(bits); - - // If there's a fraction, read it in - fractval = ReadUBitLong(bLowPrecision ? COORD_FRACTIONAL_BITS_MP_LOWPRECISION : COORD_FRACTIONAL_BITS); - - // Calculate the correct floating point value - value = intval + ((float)fractval * (bLowPrecision ? COORD_RESOLUTION_LOWPRECISION : COORD_RESOLUTION)); - } - - return value; - } - - INLINE float ReadBitNormal() { - // Read the sign bit - i32 signbit = ReadOneBit(); - - // Read the fractional part - u32 fractval = ReadUBitLong(NORMAL_FRACTIONAL_BITS); - - // Calculate the correct floating point value - float value = (float)fractval * NORMAL_RESOLUTION; - - // Fixup the sign if negative. - if (signbit) - value = -value; - - return value; - } - - INLINE void ReadBitVec3Coord(Vector& fa) { - i32 xflag, yflag, zflag; - - // This vector must be initialized! Otherwise, If any of the flags aren't set, - // the corresponding component will not be read and will be stack garbage. - fa.Init(0, 0, 0); - - xflag = ReadOneBit(); - yflag = ReadOneBit(); - zflag = ReadOneBit(); - - if (xflag) - fa[0] = ReadBitCoord(); - if (yflag) - fa[1] = ReadBitCoord(); - if (zflag) - fa[2] = ReadBitCoord(); - } - - INLINE void ReadBitVec3Normal(Vector& fa) { - i32 xflag = ReadOneBit(); - i32 yflag = ReadOneBit(); - - if (xflag) - fa[0] = ReadBitNormal(); - else - fa[0] = 0.0f; - - if (yflag) - fa[1] = ReadBitNormal(); - else - fa[1] = 0.0f; - - // The first two imply the third (but not its sign) - i32 znegative = ReadOneBit(); - - float fafafbfb = fa[0] * fa[0] + fa[1] * fa[1]; - if (fafafbfb < 1.0f) - fa[2] = sqrt(1.0f - fafafbfb); - else - fa[2] = 0.0f; - - if (znegative) - fa[2] = -fa[2]; - } - - INLINE void ReadBitAngles(QAngle& fa) { - Vector tmp; - ReadBitVec3Coord(tmp); - fa.Init(tmp.x, tmp.y, tmp.z); - }*/ - - INLINE float ReadBitAngle(int numBits) - { - float shift = (float)(GetBitForBitnum(numBits)); - - i32 i = ReadUBitLong(numBits); - float fReturn = (float)i * (360.0 / shift); - - return fReturn; - } - - INLINE i32 ReadChar() - { - return ReadSBitLong(sizeof(char) << 3); - } - INLINE u32 ReadByte() - { - return ReadUBitLong(sizeof(unsigned char) << 3); - } - - INLINE i32 ReadShort() - { - return ReadSBitLong(sizeof(short) << 3); - } - INLINE u32 ReadWord() - { - return ReadUBitLong(sizeof(unsigned short) << 3); - } - - INLINE i32 ReadLong() - { - return (i32)(ReadUBitLong(sizeof(i32) << 3)); - } - INLINE float ReadFloat() - { - u32 temp = ReadUBitLong(sizeof(float) << 3); - return *reinterpret_cast(&temp); - } - - INLINE u32 ReadVarInt32() - { - constexpr int kMaxVarint32Bytes = 5; - - u32 result = 0; - int count = 0; - u32 b; - - do - { - if (count == kMaxVarint32Bytes) - return result; - - b = ReadUBitLong(8); - result |= (b & 0x7F) << (7 * count); - ++count; - } while (b & 0x80); - - return result; - } - - INLINE u64 ReadVarInt64() - { - constexpr int kMaxVarintBytes = 10; - - u64 result = 0; - int count = 0; - u64 b; - - do - { - if (count == kMaxVarintBytes) - return result; - - b = ReadUBitLong(8); - result |= static_cast(b & 0x7F) << (7 * count); - ++count; - } while (b & 0x80); - - return result; - } - - INLINE void ReadBits(uptr outData, u32 bitLength) - { - u8* out = reinterpret_cast(outData); - int bitsLeft = bitLength; - - // align output to dword boundary - while (((uptr)out & 3) != 0 && bitsLeft >= 8) - { - *out = (unsigned char)ReadUBitLong(8); - ++out; - bitsLeft -= 8; - } - - // read dwords - while (bitsLeft >= 32) - { - *((u32*)out) = ReadUBitLong(32); - out += sizeof(u32); - bitsLeft -= 32; - } - - // read remaining bytes - while (bitsLeft >= 8) - { - *out = ReadUBitLong(8); - ++out; - bitsLeft -= 8; - } - - // read remaining bits - if (bitsLeft) - *out = ReadUBitLong(bitsLeft); - } - - INLINE bool ReadBytes(uptr outData, u32 byteLength) - { - ReadBits(outData, byteLength << 3); - return !IsOverflowed(); - } - - INLINE bool ReadString(char* str, i32 maxLength, bool stopAtLineTermination = false, i32* outNumChars = 0) - { - bool tooSmall = false; - int iChar = 0; - - while (1) - { - char val = ReadChar(); - - if (val == 0) - break; - else if (stopAtLineTermination && val == '\n') - break; - - if (iChar < (maxLength - 1)) - { - str[iChar] = val; - ++iChar; - } - else - { - tooSmall = true; - } - } - - // Make sure it's null-terminated. - // Assert(iChar < maxLength); - str[iChar] = 0; - - if (outNumChars) - *outNumChars = iChar; - - return !IsOverflowed() && !tooSmall; - } - - INLINE char* ReadAndAllocateString(bool* hasOverflowed = 0) - { - char str[2048]; - - int chars = 0; - bool overflowed = !ReadString(str, sizeof(str), false, &chars); - - if (hasOverflowed) - *hasOverflowed = overflowed; - - // Now copy into the output and return it; - char* ret = new char[chars + 1]; - for (u32 i = 0; i <= chars; i++) - ret[i] = str[i]; - - return ret; - } - - INLINE i64 ReadLongLong() - { - i64 retval; - u32* longs = (u32*)&retval; - - // Read the two DWORDs according to network endian - const short endianIndex = 0x0100; - u8* idx = (u8*)&endianIndex; - - longs[*idx++] = ReadUBitLong(sizeof(i32) << 3); - longs[*idx] = ReadUBitLong(sizeof(i32) << 3); - - return retval; - } - - INLINE bool Seek(size_t startPos) - { - bool bSucc = true; - - if (startPos < 0 || startPos > m_DataBits) - { - SetOverflowed(); - bSucc = false; - startPos = m_DataBits; - } - - // non-multiple-of-4 bytes at head of buffer. We put the "round off" - // at the head to make reading and detecting the end efficient. - int nHead = m_DataBytes & 3; - - int posBytes = startPos / 8; - if ((m_DataBytes < 4) || (nHead && (posBytes < nHead))) - { - // partial first dword - u8 const* partial = (u8 const*)m_Data; - - if (m_Data) - { - m_CachedBufWord = *(partial++); - if (nHead > 1) - m_CachedBufWord |= (*partial++) << 8; - if (nHead > 2) - m_CachedBufWord |= (*partial++) << 16; - } - - m_DataIn = (u32 const*)partial; - - m_CachedBufWord >>= (startPos & 31); - m_CachedBitsLeft = (nHead << 3) - (startPos & 31); - } - else - { - int adjustedPos = startPos - (nHead << 3); - - m_DataIn = reinterpret_cast(reinterpret_cast(m_Data) + ((adjustedPos / 32) << 2) + nHead); - - if (m_Data) - { - m_CachedBitsLeft = 32; - GrabNextDWord(); - } - else - { - m_CachedBufWord = 0; - m_CachedBitsLeft = 1; - } - - m_CachedBufWord >>= (adjustedPos & 31); - m_CachedBitsLeft = std::min(m_CachedBitsLeft, u32(32 - (adjustedPos & 31))); // in case grabnextdword overflowed - } - - return bSucc; - } - - INLINE size_t GetNumBitsRead() - { - if (!m_Data) - return 0; - - size_t nCurOfs = size_t(((iptr(m_DataIn) - iptr(m_Data)) / 4) - 1); - nCurOfs *= 32; - nCurOfs += (32 - m_CachedBitsLeft); - - size_t nAdjust = 8 * (m_DataBytes & 3); - return std::min(nCurOfs + nAdjust, m_DataBits); - } - - INLINE bool SeekRelative(size_t offset) - { - return Seek(GetNumBitsRead() + offset); - } - - INLINE size_t TotalBytesAvailable() - { - return m_DataBytes; - } - - INLINE size_t GetNumBitsLeft() - { - return m_DataBits - GetNumBitsRead(); - } - INLINE size_t GetNumBytesLeft() - { - return GetNumBitsLeft() >> 3; - } - - private: - size_t m_DataBits; // 0x0010 - size_t m_DataBytes; // 0x0018 - - u32 m_CachedBufWord; // 0x0020 - u32 m_CachedBitsLeft; // 0x0024 - - const u32* m_DataIn; // 0x0028 - const u32* m_DataEnd; // 0x0030 - const u32* m_Data; // 0x0038 -}; - -class BFWrite : public BitBufferBase -{ - public: - BFWrite() = default; - - INLINE BFWrite(uptr data, size_t byteLength, const char* bufferName = 0) - { - StartWriting(data, byteLength); - - if (bufferName) - SetName(bufferName); - } - - public: - INLINE void StartWriting(uptr data, size_t byteLength) - { - m_Data = reinterpret_cast(data); - m_DataOut = m_Data; - - m_DataBytes = byteLength; - m_DataBits = byteLength << 3; - - m_DataEnd = reinterpret_cast(reinterpret_cast(m_Data) + m_DataBytes); - } - - INLINE int GetNumBitsLeft() - { - return m_OutBitsLeft + (32 * (m_DataEnd - m_DataOut - 1)); - } - - INLINE void Reset() - { - m_Overflow = false; - m_OutBufWord = 0; - m_OutBitsLeft = 32; - m_DataOut = m_Data; - } - - INLINE void TempFlush() - { - if (m_OutBitsLeft != 32) - { - if (m_DataOut == m_DataEnd) - SetOverflowed(); - else - StoreLittleDWord(m_DataOut, 0, LoadLittleDWord(m_DataOut, 0) & ~s_nMaskTable[32 - m_OutBitsLeft] | m_OutBufWord); - } - - m_Flushed = true; - } - - INLINE u8* GetBasePointer() - { - TempFlush(); - return reinterpret_cast(m_Data); - } - - INLINE u8* GetData() - { - return GetBasePointer(); - } - - INLINE void Finish() - { - if (m_OutBitsLeft != 32) - { - if (m_DataOut == m_DataEnd) - SetOverflowed(); - - StoreLittleDWord(m_DataOut, 0, m_OutBufWord); - } - } - - INLINE void FlushNoCheck() - { - StoreLittleDWord(m_DataOut++, 0, m_OutBufWord); - - m_OutBitsLeft = 32; - m_OutBufWord = 0; - } - - INLINE void Flush() - { - if (m_DataOut == m_DataEnd) - SetOverflowed(); - else - StoreLittleDWord(m_DataOut++, 0, m_OutBufWord); - - m_OutBitsLeft = 32; - m_OutBufWord = 0; - } - - INLINE void WriteOneBitNoCheck(i32 value) - { - m_OutBufWord |= (value & 1) << (32 - m_OutBitsLeft); - - if (--m_OutBitsLeft == 0) - FlushNoCheck(); - } - - INLINE void WriteOneBit(i32 value) - { - m_OutBufWord |= (value & 1) << (32 - m_OutBitsLeft); - - if (--m_OutBitsLeft == 0) - Flush(); - } - - INLINE void WriteUBitLong(u32 data, i32 numBits, bool checkRange = true) - { - if (numBits <= m_OutBitsLeft) - { - if (checkRange) - m_OutBufWord |= (data) << (32 - m_OutBitsLeft); - else - m_OutBufWord |= (data & s_nMaskTable[numBits]) << (32 - m_OutBitsLeft); - - m_OutBitsLeft -= numBits; - - if (m_OutBitsLeft == 0) - Flush(); - } - else - { - // split dwords case - i32 overflowBits = (numBits - m_OutBitsLeft); - m_OutBufWord |= (data & s_nMaskTable[m_OutBitsLeft]) << (32 - m_OutBitsLeft); - Flush(); - m_OutBufWord = (data >> (numBits - overflowBits)); - m_OutBitsLeft = 32 - overflowBits; - } - } - - INLINE void WriteSBitLong(i32 data, i32 numBits) - { - WriteUBitLong((u32)data, numBits, false); - } - - INLINE void WriteUBitVar(u32 n) - { - if (n < 16) - WriteUBitLong(n, 6); - else if (n < 256) - WriteUBitLong((n & 15) | 16 | ((n & (128 | 64 | 32 | 16)) << 2), 10); - else if (n < 4096) - WriteUBitLong((n & 15) | 32 | ((n & (2048 | 1024 | 512 | 256 | 128 | 64 | 32 | 16)) << 2), 14); - else - { - WriteUBitLong((n & 15) | 48, 6); - WriteUBitLong((n >> 4), 32 - 4); - } - } - - INLINE void WriteBitFloat(float value) - { - auto temp = &value; - WriteUBitLong(*reinterpret_cast(temp), 32); - } - - INLINE void WriteFloat(float value) - { - auto temp = &value; - WriteUBitLong(*reinterpret_cast(temp), 32); - } - - INLINE bool WriteBits(const uptr data, i32 numBits) - { - u8* out = (u8*)data; - i32 numBitsLeft = numBits; - - // Bounds checking.. - if ((GetNumBitsWritten() + numBits) > m_DataBits) - { - SetOverflowed(); - return false; - } - - // !! speed!! need fast paths - // write remaining bytes - while (numBitsLeft >= 8) - { - WriteUBitLong(*out, 8, false); - ++out; - numBitsLeft -= 8; - } - - // write remaining bits - if (numBitsLeft) - WriteUBitLong(*out, numBitsLeft, false); - - return !IsOverflowed(); - } - - INLINE bool WriteBytes(const uptr data, i32 numBytes) - { - return WriteBits(data, numBytes << 3); - } - - INLINE i32 GetNumBitsWritten() - { - return (32 - m_OutBitsLeft) + (32 * (m_DataOut - m_Data)); - } - - INLINE i32 GetNumBytesWritten() - { - return (GetNumBitsWritten() + 7) >> 3; - } - - INLINE void WriteChar(i32 val) - { - WriteSBitLong(val, sizeof(char) << 3); - } - - INLINE void WriteByte(i32 val) - { - WriteUBitLong(val, sizeof(unsigned char) << 3, false); - } - - INLINE void WriteShort(i32 val) - { - WriteSBitLong(val, sizeof(short) << 3); - } - - INLINE void WriteWord(i32 val) - { - WriteUBitLong(val, sizeof(unsigned short) << 3); - } - - INLINE bool WriteString(const char* str) - { - if (str) - while (*str) - WriteChar(*(str++)); - - WriteChar(0); - - return !IsOverflowed(); - } - - INLINE void WriteLongLong(i64 val) - { - u32* pLongs = (u32*)&val; - - // Insert the two DWORDS according to network endian - const short endianIndex = 0x0100; - u8* idx = (u8*)&endianIndex; - - WriteUBitLong(pLongs[*idx++], sizeof(i32) << 3); - WriteUBitLong(pLongs[*idx], sizeof(i32) << 3); - } - - /*INLINE void WriteBitCoord(const float f) { - i32 signbit = (f <= -COORD_RESOLUTION); - i32 intval = (i32)abs(f); - i32 fractval = abs((i32)(f * COORD_DENOMINATOR)) & (COORD_DENOMINATOR - 1); - - // Send the bit flags that indicate whether we have an integer part and/or a fraction part. - WriteOneBit(intval); - WriteOneBit(fractval); - - if (intval || fractval) { - // Send the sign bit - WriteOneBit(signbit); - - // Send the integer if we have one. - if (intval) { - // Adjust the integers from [1..MAX_COORD_VALUE] to [0..MAX_COORD_VALUE-1] - intval--; - WriteUBitLong((u32)intval, COORD_INTEGER_BITS); - } - - // Send the fraction if we have one - if (fractval) { - WriteUBitLong((u32)fractval, COORD_FRACTIONAL_BITS); - } - } - } - - INLINE void WriteBitCoordMP(const float f) { - i32 signbit = (f <= -COORD_RESOLUTION); - i32 intval = (i32)abs(f); - i32 fractval = (abs((i32)(f * COORD_DENOMINATOR)) & (COORD_DENOMINATOR - 1)); - - bool bInBounds = intval < (1 << COORD_INTEGER_BITS_MP); - - WriteOneBit(bInBounds); - - // Send the sign bit - WriteOneBit(intval); - - if (intval) { - WriteOneBit(signbit); - - // Send the integer if we have one. - // Adjust the integers from [1..MAX_COORD_VALUE] to [0..MAX_COORD_VALUE-1] - intval--; - - if (bInBounds) - WriteUBitLong((u32)intval, COORD_INTEGER_BITS_MP); - else - WriteUBitLong((u32)intval, COORD_INTEGER_BITS); - } - } - - INLINE void WriteBitCellCoord(const float f, int bits, EBitCoordType coordType) { - bool bIntegral = (coordType == kCW_Integral); - bool bLowPrecision = (coordType == kCW_LowPrecision); - - i32 intval = (i32)abs(f); - i32 fractval = bLowPrecision ? (abs((i32)(f * COORD_DENOMINATOR_LOWPRECISION)) & (COORD_DENOMINATOR_LOWPRECISION - 1)) : - (abs((i32)(f * COORD_DENOMINATOR)) & (COORD_DENOMINATOR - 1)); - - if (bIntegral) - WriteUBitLong((u32)intval, bits); - else { - WriteUBitLong((u32)intval, bits); - WriteUBitLong((u32)fractval, bLowPrecision ? COORD_FRACTIONAL_BITS_MP_LOWPRECISION : COORD_FRACTIONAL_BITS); - } - }*/ - - INLINE void SeekToBit(int bit) - { - TempFlush(); - - m_DataOut = m_Data + (bit / 32); - m_OutBufWord = LoadLittleDWord(m_DataOut, 0); - m_OutBitsLeft = 32 - (bit & 31); - } - - /*INLINE void WriteBitVec3Coord(const Vector& fa) { - i32 xflag, yflag, zflag; - - xflag = (fa[0] >= COORD_RESOLUTION) || (fa[0] <= -COORD_RESOLUTION); - yflag = (fa[1] >= COORD_RESOLUTION) || (fa[1] <= -COORD_RESOLUTION); - zflag = (fa[2] >= COORD_RESOLUTION) || (fa[2] <= -COORD_RESOLUTION); - - WriteOneBit(xflag); - WriteOneBit(yflag); - WriteOneBit(zflag); - - if (xflag) - WriteBitCoord(fa[0]); - if (yflag) - WriteBitCoord(fa[1]); - if (zflag) - WriteBitCoord(fa[2]); - } - - INLINE void WriteBitNormal(float f) { - i32 signbit = (f <= -NORMAL_RESOLUTION); - - // NOTE: Since +/-1 are valid values for a normal, I'm going to encode that as all ones - u32 fractval = abs((i32)(f * NORMAL_DENOMINATOR)); - - // clamp.. - if (fractval > NORMAL_DENOMINATOR) - fractval = NORMAL_DENOMINATOR; - - // Send the sign bit - WriteOneBit(signbit); - - // Send the fractional component - WriteUBitLong(fractval, NORMAL_FRACTIONAL_BITS); - } - - INLINE void WriteBitVec3Normal(const Vector& fa) { - i32 xflag, yflag; - - xflag = (fa[0] >= NORMAL_RESOLUTION) || (fa[0] <= -NORMAL_RESOLUTION); - yflag = (fa[1] >= NORMAL_RESOLUTION) || (fa[1] <= -NORMAL_RESOLUTION); - - WriteOneBit(xflag); - WriteOneBit(yflag); - - if (xflag) - WriteBitNormal(fa[0]); - if (yflag) - WriteBitNormal(fa[1]); - - // Write z sign bit - i32 signbit = (fa[2] <= -NORMAL_RESOLUTION); - WriteOneBit(signbit); - }*/ - - INLINE void WriteBitAngle(float angle, int numBits) - { - u32 shift = GetBitForBitnum(numBits); - u32 mask = shift - 1; - - i32 d = (i32)((angle / 360.0) * shift); - d &= mask; - - WriteUBitLong((u32)d, numBits); - } - - INLINE bool WriteBitsFromBuffer(BFRead* in, int numBits) - { - while (numBits > 32) - { - WriteUBitLong(in->ReadUBitLong(32), 32); - numBits -= 32; - } - - WriteUBitLong(in->ReadUBitLong(numBits), numBits); - return !IsOverflowed() && !in->IsOverflowed(); - } - - /*INLINE void WriteBitAngles(const QAngle& fa) { - // FIXME: - Vector tmp(fa.x, fa.y, fa.z); - WriteBitVec3Coord(tmp); - }*/ - - private: - size_t m_DataBits = 0; - size_t m_DataBytes = 0; - - u32 m_OutBufWord = 0; - u32 m_OutBitsLeft = 32; - - u32* m_DataOut = nullptr; - u32* m_DataEnd = nullptr; - u32* m_Data = nullptr; - - bool m_Flushed = false; // :flushed: -}; - -#undef INLINE diff --git a/NorthstarDLL/bits.cpp b/NorthstarDLL/bits.cpp deleted file mode 100644 index 014899f2..00000000 --- a/NorthstarDLL/bits.cpp +++ /dev/null @@ -1,46 +0,0 @@ -//=============================================================================// -// -// Purpose: look for NANs, infinities, and underflows. -// -//=============================================================================// - -#include "pch.h" -#include "bits.h" - -//----------------------------------------------------------------------------- -// This follows the ANSI/IEEE 754-1985 standard -//----------------------------------------------------------------------------- -unsigned long& FloatBits(float& f) -{ - return *reinterpret_cast(&f); -} - -unsigned long const& FloatBits(float const& f) -{ - return *reinterpret_cast(&f); -} - -float BitsToFloat(unsigned long i) -{ - return *reinterpret_cast(&i); -} - -bool IsFinite(float f) -{ - return ((FloatBits(f) & 0x7F800000) != 0x7F800000); -} - -unsigned long FloatAbsBits(float f) -{ - return FloatBits(f) & 0x7FFFFFFF; -} - -float FloatMakePositive(float f) -{ - return fabsf(f); -} - -float FloatNegate(float f) -{ - return -f; // BitsToFloat( FloatBits(f) ^ 0x80000000 ); -} diff --git a/NorthstarDLL/bits.h b/NorthstarDLL/bits.h deleted file mode 100644 index 0532a9bd..00000000 --- a/NorthstarDLL/bits.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -unsigned long& FloatBits(float& f); -unsigned long const& FloatBits(float const& f); -float BitsToFloat(unsigned long i); -bool IsFinite(float f); -unsigned long FloatAbsBits(float f); - -#define FLOAT32_NAN_BITS (std::uint32_t)0x7FC00000 // NaN! -#define FLOAT32_NAN BitsToFloat(FLOAT32_NAN_BITS) diff --git a/NorthstarDLL/buildainfile.cpp b/NorthstarDLL/buildainfile.cpp deleted file mode 100644 index 8190adba..00000000 --- a/NorthstarDLL/buildainfile.cpp +++ /dev/null @@ -1,400 +0,0 @@ -#include "pch.h" -#include "convar.h" -#include "hoststate.h" -#include "r2engine.h" - -#include -#include - -AUTOHOOK_INIT() - -const int AINET_VERSION_NUMBER = 57; -const int AINET_SCRIPT_VERSION_NUMBER = 21; -const int PLACEHOLDER_CRC = 0; -const int MAX_HULLS = 5; - -#pragma pack(push, 1) -struct CAI_NodeLink -{ - short srcId; - short destId; - bool hulls[MAX_HULLS]; - char unk0; - char unk1; // maps => unk0 on disk - char unk2[5]; - int64_t flags; -}; -#pragma pack(pop) - -#pragma pack(push, 1) -struct CAI_NodeLinkDisk -{ - short srcId; - short destId; - char unk0; - bool hulls[MAX_HULLS]; -}; -#pragma pack(pop) - -#pragma pack(push, 1) -struct CAI_Node -{ - int index; // not present on disk - float x; - float y; - float z; - float hulls[MAX_HULLS]; - float yaw; - - int unk0; // always 2 in buildainfile, maps directly to unk0 in disk struct - int unk1; // maps directly to unk1 in disk struct - int unk2[MAX_HULLS]; // maps directly to unk2 in disk struct, despite being ints rather than shorts - - // view server.dll+393672 for context and death wish - char unk3[MAX_HULLS]; // hell on earth, should map to unk3 on disk - char pad[3]; // aligns next bytes - float unk4[MAX_HULLS]; // i have no fucking clue, calculated using some kind of demon hell function float magic - - CAI_NodeLink** links; - char unk5[16]; - int linkcount; - int unk11; // bad name lmao - short unk6; // should match up to unk4 on disk - char unk7[16]; // padding until next bit - short unk8; // should match up to unk5 on disk - 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) -struct CAI_NodeDisk -{ - float x; - float y; - float z; - float yaw; - float hulls[MAX_HULLS]; - - char unk0; - int unk1; - short unk2[MAX_HULLS]; - char unk3[MAX_HULLS]; - short unk4; - short unk5; - char unk6[8]; -}; // total size of 68 bytes -#pragma pack(pop) - -#pragma pack(push, 1) -struct UnkNodeStruct0 -{ - int index; - char unk0; - char unk1; // maps to unk1 on disk - char pad0[2]; // padding to +8 - - float x; - float y; - float z; - - char pad5[4]; - int* unk2; // maps to unk5 on disk; - char pad1[16]; // pad to +48 - int unkcount0; // maps to unkcount0 on disk - - char pad2[4]; // pad to +56 - int* unk3; - char pad3[16]; // pad to +80 - int unkcount1; - - char pad4[132]; - char unk5; -}; -#pragma pack(pop) - -int* pUnkStruct0Count; -UnkNodeStruct0*** pppUnkNodeStruct0s; - -#pragma pack(push, 1) -struct UnkLinkStruct1 -{ - short unk0; - short unk1; - int unk2; - char unk3; - char unk4; - char unk5; -}; -#pragma pack(pop) - -int* pUnkLinkStruct1Count; -UnkLinkStruct1*** pppUnkStruct1s; - -#pragma pack(push, 1) -struct CAI_ScriptNode -{ - float x; - float y; - float z; - uint64_t scriptdata; -}; -#pragma pack(pop) - -#pragma pack(push, 1) -struct CAI_Network -{ - // +0 - char unk0[8]; - // +8 - int linkcount; // this is uninitialised and never set on ain build, fun! - // +12 - char unk1[124]; - // +136 - int zonecount; - // +140 - char unk2[16]; - // +156 - int unk5; // unk8 on disk - // +160 - char unk6[4]; - // +164 - int hintcount; - // +168 - short hints[2000]; // these probably aren't actually hints, but there's 1 of them per hint so idk - // +4168 - int scriptnodecount; - // +4172 - CAI_ScriptNode scriptnodes[4000]; - // +84172 - int nodecount; - // +84176 - CAI_Node** nodes; -}; -#pragma pack(pop) - -char** pUnkServerMapversionGlobal; - -ConVar* Cvar_ns_ai_dumpAINfileFromLoad; - -void DumpAINInfo(CAI_Network* aiNetwork) -{ - fs::path writePath(fmt::format("{}/maps/graphs", R2::g_pModName)); - writePath /= R2::g_pHostState->m_levelName; - writePath += ".ain"; - - // dump from memory - spdlog::info("writing ain file {}", writePath.string()); - spdlog::info(""); - spdlog::info(""); - spdlog::info(""); - spdlog::info(""); - spdlog::info(""); - - std::ofstream writeStream(writePath, std::ofstream::binary); - spdlog::info("writing ainet version: {}", AINET_VERSION_NUMBER); - writeStream.write((char*)&AINET_VERSION_NUMBER, sizeof(int)); - - // could probably be cleaner but whatever - int mapVersion = *(int*)(*pUnkServerMapversionGlobal + 104); - spdlog::info("writing map version: {}", mapVersion); // temp - writeStream.write((char*)&mapVersion, sizeof(int)); - spdlog::info("writing placeholder crc: {}", PLACEHOLDER_CRC); - writeStream.write((char*)&PLACEHOLDER_CRC, sizeof(int)); - - int calculatedLinkcount = 0; - - // path nodes - spdlog::info("writing nodecount: {}", aiNetwork->nodecount); - writeStream.write((char*)&aiNetwork->nodecount, sizeof(int)); - - for (int i = 0; i < aiNetwork->nodecount; i++) - { - // construct on-disk node struct - CAI_NodeDisk diskNode; - diskNode.x = aiNetwork->nodes[i]->x; - diskNode.y = aiNetwork->nodes[i]->y; - diskNode.z = aiNetwork->nodes[i]->z; - diskNode.yaw = aiNetwork->nodes[i]->yaw; - memcpy(diskNode.hulls, aiNetwork->nodes[i]->hulls, sizeof(diskNode.hulls)); - diskNode.unk0 = (char)aiNetwork->nodes[i]->unk0; - diskNode.unk1 = aiNetwork->nodes[i]->unk1; - - for (int j = 0; j < MAX_HULLS; j++) - { - diskNode.unk2[j] = (short)aiNetwork->nodes[i]->unk2[j]; - spdlog::info((short)aiNetwork->nodes[i]->unk2[j]); - } - - memcpy(diskNode.unk3, aiNetwork->nodes[i]->unk3, sizeof(diskNode.unk3)); - diskNode.unk4 = aiNetwork->nodes[i]->unk6; - diskNode.unk5 = - -1; // aiNetwork->nodes[i]->unk8; // this field is wrong, however, it's always -1 in vanilla navmeshes anyway, so no biggie - memcpy(diskNode.unk6, aiNetwork->nodes[i]->unk10, sizeof(diskNode.unk6)); - - spdlog::info("writing node {} from {} to {:x}", aiNetwork->nodes[i]->index, (void*)aiNetwork->nodes[i], writeStream.tellp()); - writeStream.write((char*)&diskNode, sizeof(CAI_NodeDisk)); - - calculatedLinkcount += aiNetwork->nodes[i]->linkcount; - } - - // links - spdlog::info("linkcount: {}", aiNetwork->linkcount); - spdlog::info("calculated total linkcount: {}", calculatedLinkcount); - - calculatedLinkcount /= 2; - if (Cvar_ns_ai_dumpAINfileFromLoad->GetBool()) - { - if (aiNetwork->linkcount == calculatedLinkcount) - spdlog::info("caculated linkcount is normal!"); - else - spdlog::warn("calculated linkcount has weird value! this is expected on build!"); - } - - spdlog::info("writing linkcount: {}", calculatedLinkcount); - writeStream.write((char*)&calculatedLinkcount, sizeof(int)); - - for (int i = 0; i < aiNetwork->nodecount; i++) - { - for (int j = 0; j < aiNetwork->nodes[i]->linkcount; j++) - { - // skip links that don't originate from current node - if (aiNetwork->nodes[i]->links[j]->srcId != aiNetwork->nodes[i]->index) - continue; - - CAI_NodeLinkDisk diskLink; - diskLink.srcId = aiNetwork->nodes[i]->links[j]->srcId; - diskLink.destId = aiNetwork->nodes[i]->links[j]->destId; - diskLink.unk0 = aiNetwork->nodes[i]->links[j]->unk1; - memcpy(diskLink.hulls, aiNetwork->nodes[i]->links[j]->hulls, sizeof(diskLink.hulls)); - - spdlog::info("writing link {} => {} to {:x}", diskLink.srcId, diskLink.destId, writeStream.tellp()); - writeStream.write((char*)&diskLink, sizeof(CAI_NodeLinkDisk)); - } - } - - // don't know what this is, it's likely a block from tf1 that got deprecated? should just be 1 int per node - spdlog::info("writing {:x} bytes for unknown block at {:x}", aiNetwork->nodecount * sizeof(uint32_t), writeStream.tellp()); - uint32_t* unkNodeBlock = new uint32_t[aiNetwork->nodecount]; - memset(unkNodeBlock, 0, aiNetwork->nodecount * sizeof(uint32_t)); - writeStream.write((char*)unkNodeBlock, aiNetwork->nodecount * sizeof(uint32_t)); - delete[] unkNodeBlock; - - // TODO: this is traverse nodes i think? these aren't used in tf2 ains so we can get away with just writing count=0 and skipping - // but ideally should actually dump these - spdlog::info("writing {} traversal nodes at {:x}...", 0, writeStream.tellp()); - short traverseNodeCount = 0; - writeStream.write((char*)&traverseNodeCount, sizeof(short)); - // only write count since count=0 means we don't have to actually do anything here - - // TODO: ideally these should be actually dumped, but they're always 0 in tf2 from what i can tell - spdlog::info("writing {} bytes for unknown hull block at {:x}", MAX_HULLS * 8, writeStream.tellp()); - char* unkHullBlock = new char[MAX_HULLS * 8]; - memset(unkHullBlock, 0, MAX_HULLS * 8); - writeStream.write(unkHullBlock, MAX_HULLS * 8); - delete[] unkHullBlock; - - // unknown struct that's seemingly node-related - spdlog::info("writing {} unknown node structs at {:x}", *pUnkStruct0Count, writeStream.tellp()); - writeStream.write((char*)pUnkStruct0Count, sizeof(*pUnkStruct0Count)); - for (int i = 0; i < *pUnkStruct0Count; i++) - { - spdlog::info("writing unknown node struct {} at {:x}", i, writeStream.tellp()); - UnkNodeStruct0* nodeStruct = (*pppUnkNodeStruct0s)[i]; - - writeStream.write((char*)&nodeStruct->index, sizeof(nodeStruct->index)); - writeStream.write((char*)&nodeStruct->unk1, sizeof(nodeStruct->unk1)); - - writeStream.write((char*)&nodeStruct->x, sizeof(nodeStruct->x)); - writeStream.write((char*)&nodeStruct->y, sizeof(nodeStruct->y)); - writeStream.write((char*)&nodeStruct->z, sizeof(nodeStruct->z)); - - writeStream.write((char*)&nodeStruct->unkcount0, sizeof(nodeStruct->unkcount0)); - for (int j = 0; j < nodeStruct->unkcount0; j++) - { - short unk2Short = (short)nodeStruct->unk2[j]; - writeStream.write((char*)&unk2Short, sizeof(unk2Short)); - } - - writeStream.write((char*)&nodeStruct->unkcount1, sizeof(nodeStruct->unkcount1)); - for (int j = 0; j < nodeStruct->unkcount1; j++) - { - short unk3Short = (short)nodeStruct->unk3[j]; - writeStream.write((char*)&unk3Short, sizeof(unk3Short)); - } - - writeStream.write((char*)&nodeStruct->unk5, sizeof(nodeStruct->unk5)); - } - - // unknown struct that's seemingly link-related - spdlog::info("writing {} unknown link structs at {:x}", *pUnkLinkStruct1Count, writeStream.tellp()); - writeStream.write((char*)pUnkLinkStruct1Count, sizeof(*pUnkLinkStruct1Count)); - for (int i = 0; i < *pUnkLinkStruct1Count; i++) - { - // disk and memory structs are literally identical here so just directly write - spdlog::info("writing unknown link struct {} at {:x}", i, writeStream.tellp()); - writeStream.write((char*)(*pppUnkStruct1s)[i], sizeof(*(*pppUnkStruct1s)[i])); - } - - // some weird int idk what this is used for - writeStream.write((char*)&aiNetwork->unk5, sizeof(aiNetwork->unk5)); - - // tf2-exclusive stuff past this point, i.e. ain v57 only - spdlog::info("writing {} script nodes at {:x}", aiNetwork->scriptnodecount, writeStream.tellp()); - writeStream.write((char*)&aiNetwork->scriptnodecount, sizeof(aiNetwork->scriptnodecount)); - for (int i = 0; i < aiNetwork->scriptnodecount; i++) - { - // disk and memory structs are literally identical here so just directly write - spdlog::info("writing script node {} at {:x}", i, writeStream.tellp()); - writeStream.write((char*)&aiNetwork->scriptnodes[i], sizeof(aiNetwork->scriptnodes[i])); - } - - spdlog::info("writing {} hints at {:x}", aiNetwork->hintcount, writeStream.tellp()); - writeStream.write((char*)&aiNetwork->hintcount, sizeof(aiNetwork->hintcount)); - for (int i = 0; i < aiNetwork->hintcount; i++) - { - spdlog::info("writing hint data {} at {:x}", i, writeStream.tellp()); - writeStream.write((char*)&aiNetwork->hints[i], sizeof(aiNetwork->hints[i])); - } - - writeStream.close(); -} - -// 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); -} - -// clang-format off -AUTOHOOK(LoadAINFile, server.dll + 0x3933A0, -void, __fastcall, (void* aimanager, void* buf, const char* filename)) -// clang-format on -{ - LoadAINFile(aimanager, buf, filename); - - if (Cvar_ns_ai_dumpAINfileFromLoad->GetBool()) - { - spdlog::info("running DumpAINInfo for loaded file {}", filename); - DumpAINInfo(*(CAI_Network**)((char*)aimanager + 2536)); - } -} - -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"); - - pUnkStruct0Count = module.Offset(0x1063BF8).As(); - pppUnkNodeStruct0s = module.Offset(0x1063BE0).As(); - pUnkLinkStruct1Count = module.Offset(0x1063AA8).As(); - pppUnkStruct1s = module.Offset(0x1063A90).As(); - pUnkServerMapversionGlobal = module.Offset(0xBFBE08).As(); -} diff --git a/NorthstarDLL/chatcommand.cpp b/NorthstarDLL/chatcommand.cpp deleted file mode 100644 index 37c438f3..00000000 --- a/NorthstarDLL/chatcommand.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "pch.h" -#include "convar.h" -#include "concommand.h" -#include "localchatwriter.h" - -// note: isIngameChat is an int64 because the whole register the arg is stored in needs to be 0'd out to work -// if isIngameChat is false, we use network chat instead -void(__fastcall* ClientSayText)(void* a1, const char* message, uint64_t isIngameChat, bool isTeamChat); - -void ConCommand_say(const CCommand& args) -{ - if (args.ArgC() >= 2) - ClientSayText(nullptr, args.ArgS(), true, false); -} - -void ConCommand_say_team(const CCommand& args) -{ - if (args.ArgC() >= 2) - ClientSayText(nullptr, args.ArgS(), true, true); -} - -void ConCommand_log(const CCommand& args) -{ - if (args.ArgC() >= 2) - { - LocalChatWriter(LocalChatWriter::GameContext).WriteLine(args.ArgS()); - } -} - -ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", ClientChatCommand, ConCommand, (CModule module)) -{ - ClientSayText = module.Offset(0x54780).As(); - RegisterConCommand("say", ConCommand_say, "Enters a message in public chat", FCVAR_CLIENTDLL); - RegisterConCommand("say_team", ConCommand_say_team, "Enters a message in team chat", FCVAR_CLIENTDLL); - RegisterConCommand("log", ConCommand_log, "Log a message to the local chat window", FCVAR_CLIENTDLL); -} diff --git a/NorthstarDLL/client/audio.cpp b/NorthstarDLL/client/audio.cpp new file mode 100644 index 00000000..f0bc385b --- /dev/null +++ b/NorthstarDLL/client/audio.cpp @@ -0,0 +1,507 @@ +#include "pch.h" +#include "audio.h" +#include "dedicated/dedicated.h" +#include "core/convar/convar.h" + +#include "rapidjson/error/en.h" +#include +#include +#include +#include + +AUTOHOOK_INIT() + +extern "C" +{ + // should be called only in LoadSampleMetadata_Hook + extern void* __fastcall Audio_GetParentEvent(); +} + +ConVar* Cvar_ns_print_played_sounds; + +CustomAudioManager g_CustomAudioManager; + +EventOverrideData::EventOverrideData() +{ + spdlog::warn("Initialised struct EventOverrideData without any data!"); + LoadedSuccessfully = false; +} + +// Empty stereo 48000 WAVE file +unsigned char EMPTY_WAVE[45] = {0x52, 0x49, 0x46, 0x46, 0x25, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6D, 0x74, + 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00, 0x44, 0xAC, 0x00, 0x00, 0x88, 0x58, + 0x01, 0x00, 0x02, 0x00, 0x10, 0x00, 0x64, 0x61, 0x74, 0x61, 0x74, 0x00, 0x00, 0x00, 0x00}; + +EventOverrideData::EventOverrideData(const std::string& data, const fs::path& path) +{ + if (data.length() <= 0) + { + spdlog::error("Failed reading audio override file {}: file is empty", path.string()); + return; + } + + fs::path samplesFolder = path; + samplesFolder = samplesFolder.replace_extension(); + + if (!fs::exists(samplesFolder)) + { + spdlog::error( + "Failed reading audio override file {}: samples folder doesn't exist; should be named the same as the definition file without " + "JSON extension.", + path.string()); + return; + } + + rapidjson_document dataJson; + dataJson.Parse(data); + + // fail if parse error + if (dataJson.HasParseError()) + { + spdlog::error( + "Failed reading audio override file {}: encountered parse error \"{}\" at offset {}", + path.string(), + GetParseError_En(dataJson.GetParseError()), + dataJson.GetErrorOffset()); + return; + } + + // fail if it's not a json obj (could be an array, string, etc) + if (!dataJson.IsObject()) + { + spdlog::error("Failed reading audio override file {}: file is not a JSON object", path.string()); + return; + } + + // fail if no event ids given + if (!dataJson.HasMember("EventId")) + { + spdlog::error("Failed reading audio override file {}: JSON object does not have the EventId property", path.string()); + return; + } + + // array of event ids + if (dataJson["EventId"].IsArray()) + { + for (auto& eventId : dataJson["EventId"].GetArray()) + { + if (!eventId.IsString()) + { + spdlog::error( + "Failed reading audio override file {}: EventId array has a value of invalid type, all must be strings", path.string()); + return; + } + + EventIds.push_back(eventId.GetString()); + } + } + // singular event id + else if (dataJson["EventId"].IsString()) + { + EventIds.push_back(dataJson["EventId"].GetString()); + } + // incorrect type + else + { + spdlog::error( + "Failed reading audio override file {}: EventId property is of invalid type (must be a string or an array of strings)", + path.string()); + return; + } + + if (dataJson.HasMember("EventIdRegex")) + { + // array of event id regex + if (dataJson["EventIdRegex"].IsArray()) + { + for (auto& eventId : dataJson["EventIdRegex"].GetArray()) + { + if (!eventId.IsString()) + { + spdlog::error( + "Failed reading audio override file {}: EventIdRegex array has a value of invalid type, all must be strings", + path.string()); + return; + } + + const std::string& regex = eventId.GetString(); + + try + { + EventIdsRegex.push_back({regex, std::regex(regex)}); + } + catch (...) + { + spdlog::error("Malformed regex \"{}\" in audio override file {}", regex, path.string()); + return; + } + } + } + // singular event id regex + else if (dataJson["EventIdRegex"].IsString()) + { + const std::string& regex = dataJson["EventIdRegex"].GetString(); + try + { + EventIdsRegex.push_back({regex, std::regex(regex)}); + } + catch (...) + { + spdlog::error("Malformed regex \"{}\" in audio override file {}", regex, path.string()); + return; + } + } + // incorrect type + else + { + spdlog::error( + "Failed reading audio override file {}: EventIdRegex property is of invalid type (must be a string or an array of strings)", + path.string()); + return; + } + } + + if (dataJson.HasMember("AudioSelectionStrategy")) + { + if (!dataJson["AudioSelectionStrategy"].IsString()) + { + spdlog::error("Failed reading audio override file {}: AudioSelectionStrategy property must be a string", path.string()); + return; + } + + std::string strategy = dataJson["AudioSelectionStrategy"].GetString(); + + if (strategy == "sequential") + { + Strategy = AudioSelectionStrategy::SEQUENTIAL; + } + else if (strategy == "random") + { + Strategy = AudioSelectionStrategy::RANDOM; + } + else + { + spdlog::error( + "Failed reading audio override file {}: AudioSelectionStrategy string must be either \"sequential\" or \"random\"", + path.string()); + return; + } + } + + // load samples + for (fs::directory_entry file : fs::recursive_directory_iterator(samplesFolder)) + { + if (file.is_regular_file() && file.path().extension().string() == ".wav") + { + std::string pathString = file.path().string(); + + // Open the file. + std::ifstream wavStream(pathString, std::ios::binary); + + if (wavStream.fail()) + { + spdlog::error("Failed reading audio sample {}", file.path().string()); + continue; + } + + // Get file size. + wavStream.seekg(0, std::ios::end); + size_t fileSize = wavStream.tellg(); + wavStream.close(); + + // Allocate enough memory for the file. + // blank out the memory for now, then read it later + uint8_t* data = new uint8_t[fileSize]; + memcpy(data, EMPTY_WAVE, sizeof(EMPTY_WAVE)); + Samples.push_back({fileSize, std::unique_ptr(data)}); + + // thread off the file read + // should we spawn one thread per read? or should there be a cap to the number of reads at once? + std::thread readThread( + [pathString, fileSize, data] + { + std::shared_lock lock(g_CustomAudioManager.m_loadingMutex); + std::ifstream wavStream(pathString, std::ios::binary); + + // would be weird if this got hit, since it would've worked previously + if (wavStream.fail()) + { + spdlog::error("Failed async read of audio sample {}", pathString); + return; + } + + // read from after the header first to preserve the empty header, then read the header last + wavStream.seekg(0, std::ios::beg); + wavStream.read(reinterpret_cast(data), fileSize); + wavStream.close(); + + spdlog::info("Finished async read of audio sample {}", pathString); + }); + + readThread.detach(); + } + } + + /* + if (dataJson.HasMember("EnableOnLoopedSounds")) + { + if (!dataJson["EnableOnLoopedSounds"].IsBool()) + { + spdlog::error("Failed reading audio override file {}: EnableOnLoopedSounds property is of invalid type (must be a bool)", + path.string()); return; + } + + EnableOnLoopedSounds = dataJson["EnableOnLoopedSounds"].GetBool(); + } + */ + + if (Samples.size() == 0) + spdlog::warn("Audio override {} has no valid samples! Sounds will not play for this event.", path.string()); + + spdlog::info("Loaded audio override file {}", path.string()); + + LoadedSuccessfully = true; +} + +bool CustomAudioManager::TryLoadAudioOverride(const fs::path& defPath) +{ + if (IsDedicatedServer()) + return true; // silently fail + + std::ifstream jsonStream(defPath); + std::stringstream jsonStringStream; + + // fail if no audio json + if (jsonStream.fail()) + { + spdlog::warn("Unable to read audio override from file {}", defPath.string()); + return false; + } + + while (jsonStream.peek() != EOF) + jsonStringStream << (char)jsonStream.get(); + + jsonStream.close(); + + std::shared_ptr data = std::make_shared(jsonStringStream.str(), defPath); + + if (!data->LoadedSuccessfully) + return false; // no logging, the constructor has probably already logged + + for (const std::string& eventId : data->EventIds) + { + spdlog::info("Registering sound event {}", eventId); + m_loadedAudioOverrides.insert({eventId, data}); + } + + for (const auto& eventIdRegexData : data->EventIdsRegex) + { + spdlog::info("Registering sound event regex {}", eventIdRegexData.first); + m_loadedAudioOverridesRegex.insert({eventIdRegexData.first, data}); + } + + return true; +} + +typedef void (*MilesStopAll_Type)(); +MilesStopAll_Type MilesStopAll; + +void CustomAudioManager::ClearAudioOverrides() +{ + if (IsDedicatedServer()) + return; + + if (m_loadedAudioOverrides.size() > 0 || m_loadedAudioOverridesRegex.size() > 0) + { + // stop all miles sounds beforehand + // miles_stop_all + + MilesStopAll(); + + // this is cancer but it works + Sleep(50); + } + + // slightly (very) bad + // wait for all audio reads to complete so we don't kill preexisting audio buffers as we're writing to them + std::unique_lock lock(m_loadingMutex); + + m_loadedAudioOverrides.clear(); + m_loadedAudioOverridesRegex.clear(); +} + +template Iter select_randomly(Iter start, Iter end, RandomGenerator& g) +{ + std::uniform_int_distribution<> dis(0, std::distance(start, end) - 1); + std::advance(start, dis(g)); + return start; +} + +template Iter select_randomly(Iter start, Iter end) +{ + static std::random_device rd; + static std::mt19937 gen(rd()); + return select_randomly(start, end, gen); +} + +bool ShouldPlayAudioEvent(const char* eventName, const std::shared_ptr& data) +{ + std::string eventNameString = eventName; + std::string eventNameStringBlacklistEntry = ("!" + eventNameString); + + for (const std::string& name : data->EventIds) + { + if (name == eventNameStringBlacklistEntry) + return false; // event blacklisted + + if (name == "*") + { + // check for bad sounds I guess? + // really feel like this should be an option but whatever + if (!!strstr(eventName, "_amb_") || !!strstr(eventName, "_emit_") || !!strstr(eventName, "amb_")) + return false; // would play static noise, I hate this + } + } + + return true; // good to go +} + +// forward declare +bool __declspec(noinline) __fastcall LoadSampleMetadata_Internal( + uintptr_t parentEvent, void* sample, void* audioBuffer, unsigned int audioBufferLength, int audioType); + +// DO NOT TOUCH THIS FUNCTION +// The actual logic of it in a separate function (forcefully not inlined) to preserve the r12 register, which holds the event pointer. +// clang-format off +AUTOHOOK(LoadSampleMetadata, mileswin64.dll + 0xF110, +bool, __fastcall, (void* sample, void* audioBuffer, unsigned int audioBufferLength, int audioType)) +// clang-format on +{ + uintptr_t parentEvent = (uintptr_t)Audio_GetParentEvent(); + + // Raw source, used for voice data only + if (audioType == 0) + return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType); + + return LoadSampleMetadata_Internal(parentEvent, sample, audioBuffer, audioBufferLength, audioType); +} + +// DO NOT INLINE THIS FUNCTION +// See comment below. +bool __declspec(noinline) __fastcall LoadSampleMetadata_Internal( + uintptr_t parentEvent, void* sample, void* audioBuffer, unsigned int audioBufferLength, int audioType) +{ + char* eventName = (char*)parentEvent + 0x110; + + if (Cvar_ns_print_played_sounds->GetInt() > 0) + spdlog::info("[AUDIO] Playing event {}", eventName); + + auto iter = g_CustomAudioManager.m_loadedAudioOverrides.find(eventName); + std::shared_ptr overrideData; + + if (iter == g_CustomAudioManager.m_loadedAudioOverrides.end()) + { + // override for that specific event not found, try wildcard + iter = g_CustomAudioManager.m_loadedAudioOverrides.find("*"); + + if (iter == g_CustomAudioManager.m_loadedAudioOverrides.end()) + { + // not found + + // try regex + for (const auto& item : g_CustomAudioManager.m_loadedAudioOverridesRegex) + for (const auto& regexData : item.second->EventIdsRegex) + if (std::regex_search(eventName, regexData.second)) + overrideData = item.second; + + if (!overrideData) + // not found either + return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType); + else + { + // cache found pattern to improve performance + g_CustomAudioManager.m_loadedAudioOverrides[eventName] = overrideData; + } + } + else + overrideData = iter->second; + } + else + overrideData = iter->second; + + if (!ShouldPlayAudioEvent(eventName, overrideData)) + return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType); + + void* data = 0; + unsigned int dataLength = 0; + + if (overrideData->Samples.size() == 0) + { + // 0 samples, turn off this particular event. + + // using a dummy empty wave file + data = EMPTY_WAVE; + dataLength = sizeof(EMPTY_WAVE); + } + else + { + std::pair>* dat = NULL; + + switch (overrideData->Strategy) + { + case AudioSelectionStrategy::RANDOM: + dat = &*select_randomly(overrideData->Samples.begin(), overrideData->Samples.end()); + break; + case AudioSelectionStrategy::SEQUENTIAL: + default: + dat = &overrideData->Samples[overrideData->CurrentIndex++]; + if (overrideData->CurrentIndex >= overrideData->Samples.size()) + overrideData->CurrentIndex = 0; // reset back to the first sample entry + break; + } + + if (!dat) + spdlog::warn("Could not get sample data from override struct for event {}! Shouldn't happen", eventName); + else + { + data = dat->second.get(); + dataLength = dat->first; + } + } + + if (!data) + { + spdlog::warn("Could not fetch override sample data for event {}! Using original data instead.", eventName); + return LoadSampleMetadata(sample, audioBuffer, audioBufferLength, audioType); + } + + audioBuffer = data; + audioBufferLength = dataLength; + + // most important change: set the sample class buffer so that the correct audio plays + *(void**)((uintptr_t)sample + 0xE8) = audioBuffer; + *(unsigned int*)((uintptr_t)sample + 0xF0) = audioBufferLength; + + // 64 - Auto-detect sample type + bool res = LoadSampleMetadata(sample, audioBuffer, audioBufferLength, 64); + if (!res) + spdlog::error("LoadSampleMetadata failed! The game will crash :("); + + return res; +} + +// clang-format off +AUTOHOOK(MilesLog, client.dll + 0x57DAD0, +void, __fastcall, (int level, const char* string)) +// clang-format on +{ + spdlog::info("[MSS] {} - {}", level, string); +} + +ON_DLL_LOAD_CLIENT_RELIESON("client.dll", AudioHooks, ConVar, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + Cvar_ns_print_played_sounds = new ConVar("ns_print_played_sounds", "0", FCVAR_NONE, ""); + MilesStopAll = module.Offset(0x580850).As(); +} diff --git a/NorthstarDLL/client/audio.h b/NorthstarDLL/client/audio.h new file mode 100644 index 00000000..26cda205 --- /dev/null +++ b/NorthstarDLL/client/audio.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include + +enum class AudioSelectionStrategy +{ + INVALID = -1, + SEQUENTIAL, + RANDOM +}; + +class EventOverrideData +{ + public: + EventOverrideData(const std::string&, const fs::path&); + EventOverrideData(); + + public: + bool LoadedSuccessfully = false; + + std::vector EventIds = {}; + std::vector> EventIdsRegex = {}; + + std::vector>> Samples = {}; + + AudioSelectionStrategy Strategy = AudioSelectionStrategy::SEQUENTIAL; + size_t CurrentIndex = 0; + + bool EnableOnLoopedSounds = false; +}; + +class CustomAudioManager +{ + public: + bool TryLoadAudioOverride(const fs::path&); + void ClearAudioOverrides(); + + std::shared_mutex m_loadingMutex; + std::unordered_map> m_loadedAudioOverrides = {}; + std::unordered_map> m_loadedAudioOverridesRegex = {}; +}; + +extern CustomAudioManager g_CustomAudioManager; diff --git a/NorthstarDLL/client/chatcommand.cpp b/NorthstarDLL/client/chatcommand.cpp new file mode 100644 index 00000000..76ed9784 --- /dev/null +++ b/NorthstarDLL/client/chatcommand.cpp @@ -0,0 +1,36 @@ +#include "pch.h" +#include "core/convar/convar.h" +#include "core/convar/concommand.h" +#include "localchatwriter.h" + +// note: isIngameChat is an int64 because the whole register the arg is stored in needs to be 0'd out to work +// if isIngameChat is false, we use network chat instead +void(__fastcall* ClientSayText)(void* a1, const char* message, uint64_t isIngameChat, bool isTeamChat); + +void ConCommand_say(const CCommand& args) +{ + if (args.ArgC() >= 2) + ClientSayText(nullptr, args.ArgS(), true, false); +} + +void ConCommand_say_team(const CCommand& args) +{ + if (args.ArgC() >= 2) + ClientSayText(nullptr, args.ArgS(), true, true); +} + +void ConCommand_log(const CCommand& args) +{ + if (args.ArgC() >= 2) + { + LocalChatWriter(LocalChatWriter::GameContext).WriteLine(args.ArgS()); + } +} + +ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", ClientChatCommand, ConCommand, (CModule module)) +{ + ClientSayText = module.Offset(0x54780).As(); + RegisterConCommand("say", ConCommand_say, "Enters a message in public chat", FCVAR_CLIENTDLL); + RegisterConCommand("say_team", ConCommand_say_team, "Enters a message in team chat", FCVAR_CLIENTDLL); + RegisterConCommand("log", ConCommand_log, "Log a message to the local chat window", FCVAR_CLIENTDLL); +} diff --git a/NorthstarDLL/client/clientauthhooks.cpp b/NorthstarDLL/client/clientauthhooks.cpp new file mode 100644 index 00000000..904ecb2f --- /dev/null +++ b/NorthstarDLL/client/clientauthhooks.cpp @@ -0,0 +1,64 @@ +#include "pch.h" +#include "masterserver/masterserver.h" +#include "core/convar/convar.h" +#include "client/r2client.h" + +AUTOHOOK_INIT() + +ConVar* Cvar_ns_has_agreed_to_send_token; + +// mirrored in script +const int NOT_DECIDED_TO_SEND_TOKEN = 0; +const int AGREED_TO_SEND_TOKEN = 1; +const int DISAGREED_TO_SEND_TOKEN = 2; + +// clang-format off +AUTOHOOK(AuthWithStryder, engine.dll + 0x1843A0, +void, __fastcall, (void* a1)) +// clang-format on +{ + // game will call this forever, until it gets a valid auth key + // so, we need to manually invalidate our key until we're authed with northstar, then we'll allow game to auth with stryder + if (!g_pMasterServerManager->m_bOriginAuthWithMasterServerDone && Cvar_ns_has_agreed_to_send_token->GetInt() != DISAGREED_TO_SEND_TOKEN) + { + // if player has agreed to send token and we aren't already authing, try to auth + if (Cvar_ns_has_agreed_to_send_token->GetInt() == AGREED_TO_SEND_TOKEN && + !g_pMasterServerManager->m_bOriginAuthWithMasterServerInProgress) + g_pMasterServerManager->AuthenticateOriginWithMasterServer(R2::g_pLocalPlayerUserID, R2::g_pLocalPlayerOriginToken); + + // invalidate key so auth will fail + *R2::g_pLocalPlayerOriginToken = 0; + } + + AuthWithStryder(a1); +} + +char* p3PToken; + +// clang-format off +AUTOHOOK(Auth3PToken, engine.dll + 0x183760, +char*, __fastcall, ()) +// clang-format on +{ + if (g_pMasterServerManager->m_sOwnClientAuthToken[0]) + { + memset(p3PToken, 0x0, 1024); + strcpy(p3PToken, "Protocol 3: Protect the Pilot"); + } + + return Auth3PToken(); +} + +ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", ClientAuthHooks, ConVar, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + p3PToken = module.Offset(0x13979D80).As(); + + // this cvar will save to cfg once initially agreed with + Cvar_ns_has_agreed_to_send_token = new ConVar( + "ns_has_agreed_to_send_token", + "0", + FCVAR_ARCHIVE_PLAYERPROFILE, + "whether the user has agreed to send their origin token to the northstar masterserver"); +} diff --git a/NorthstarDLL/client/clientruihooks.cpp b/NorthstarDLL/client/clientruihooks.cpp new file mode 100644 index 00000000..7896daa8 --- /dev/null +++ b/NorthstarDLL/client/clientruihooks.cpp @@ -0,0 +1,24 @@ +#include "pch.h" +#include "core/convar/convar.h" + +AUTOHOOK_INIT() + +ConVar* Cvar_rui_drawEnable; + +// clang-format off +AUTOHOOK(DrawRUIFunc, engine.dll + 0xFC500, +bool, __fastcall, (void* a1, float* a2)) +// clang-format on +{ + if (!Cvar_rui_drawEnable->GetBool()) + return 0; + + return DrawRUIFunc(a1, a2); +} + +ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", RUI, ConVar, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + Cvar_rui_drawEnable = new ConVar("rui_drawEnable", "1", FCVAR_CLIENTDLL, "Controls whether RUI should be drawn"); +} diff --git a/NorthstarDLL/client/clientvideooverrides.cpp b/NorthstarDLL/client/clientvideooverrides.cpp new file mode 100644 index 00000000..1a5924c7 --- /dev/null +++ b/NorthstarDLL/client/clientvideooverrides.cpp @@ -0,0 +1,42 @@ +#include "pch.h" +#include "mods/modmanager.h" + +AUTOHOOK_INIT() + +// clang-format off +AUTOHOOK_PROCADDRESS(BinkOpen, bink2w64.dll, BinkOpen, +void*, __fastcall, (const char* path, uint32_t flags)) +// clang-format on +{ + std::string filename(fs::path(path).filename().string()); + spdlog::info("BinkOpen {}", filename); + + // figure out which mod is handling the bink + Mod* fileOwner = nullptr; + for (Mod& mod : g_pModManager->m_LoadedMods) + { + if (!mod.m_bEnabled) + continue; + + if (std::find(mod.BinkVideos.begin(), mod.BinkVideos.end(), filename) != mod.BinkVideos.end()) + fileOwner = &mod; + } + + if (fileOwner) + { + // create new path + fs::path binkPath(fileOwner->m_ModDirectory / "media" / filename); + return BinkOpen(binkPath.string().c_str(), flags); + } + else + return BinkOpen(path, flags); +} + +ON_DLL_LOAD_CLIENT("engine.dll", BinkVideo, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + // remove engine check for whether the bik we're trying to load exists in r2/media, as this will fail for biks in mods + // note: the check in engine is actually unnecessary, so it's just useless in practice and we lose nothing by removing it + module.Offset(0x459AD).NOP(6); +} diff --git a/NorthstarDLL/client/debugoverlay.cpp b/NorthstarDLL/client/debugoverlay.cpp new file mode 100644 index 00000000..dd273227 --- /dev/null +++ b/NorthstarDLL/client/debugoverlay.cpp @@ -0,0 +1,141 @@ +#include "pch.h" +#include "dedicated/dedicated.h" +#include "core/convar/cvar.h" +#include "core/math/vector.h" + +AUTOHOOK_INIT() + +enum OverlayType_t +{ + OVERLAY_BOX = 0, + OVERLAY_SPHERE, + OVERLAY_LINE, + OVERLAY_TRIANGLE, + OVERLAY_SWEPT_BOX, + OVERLAY_BOX2, + OVERLAY_CAPSULE +}; + +struct OverlayBase_t +{ + OverlayBase_t() + { + m_Type = OVERLAY_BOX; + m_nServerCount = -1; + m_nCreationTick = -1; + m_flEndTime = 0.0f; + m_pNextOverlay = NULL; + } + + OverlayType_t m_Type; // What type of overlay is it? + int m_nCreationTick; // Duration -1 means go away after this frame # + int m_nServerCount; // Latch server count, too + float m_flEndTime; // When does this box go away + OverlayBase_t* m_pNextOverlay; + void* m_pUnk; +}; + +struct OverlayLine_t : public OverlayBase_t +{ + OverlayLine_t() + { + m_Type = OVERLAY_LINE; + } + + Vector3 origin; + Vector3 dest; + int r; + int g; + int b; + int a; + bool noDepthTest; +}; + +struct OverlayBox_t : public OverlayBase_t +{ + OverlayBox_t() + { + m_Type = OVERLAY_BOX; + } + + Vector3 origin; + Vector3 mins; + Vector3 maxs; + QAngle angles; + int r; + int g; + int b; + int a; +}; + +static HMODULE sEngineModule; + +typedef void (*RenderLineType)(Vector3 v1, Vector3 v2, Color c, bool bZBuffer); +static RenderLineType RenderLine; +typedef void (*RenderBoxType)(Vector3 vOrigin, QAngle angles, Vector3 vMins, Vector3 vMaxs, Color c, bool bZBuffer, bool bInsideOut); +static RenderBoxType RenderBox; +static RenderBoxType RenderWireframeBox; + +// clang-format off +AUTOHOOK(DrawOverlay, engine.dll + 0xABCB0, +void, __fastcall, (OverlayBase_t * pOverlay)) +// clang-format on +{ + EnterCriticalSection((LPCRITICAL_SECTION)((char*)sEngineModule + 0x10DB0A38)); // s_OverlayMutex + + void* pMaterialSystem = *(void**)((char*)sEngineModule + 0x14C675B0); + + switch (pOverlay->m_Type) + { + case OVERLAY_LINE: + { + OverlayLine_t* pLine = static_cast(pOverlay); + RenderLine(pLine->origin, pLine->dest, Color(pLine->r, pLine->g, pLine->b, pLine->a), pLine->noDepthTest); + } + break; + case OVERLAY_BOX: + { + OverlayBox_t* pCurrBox = static_cast(pOverlay); + if (pCurrBox->a > 0) + { + RenderBox( + pCurrBox->origin, + pCurrBox->angles, + pCurrBox->mins, + pCurrBox->maxs, + Color(pCurrBox->r, pCurrBox->g, pCurrBox->b, pCurrBox->a), + false, + false); + } + if (pCurrBox->a < 255) + { + RenderWireframeBox( + pCurrBox->origin, + pCurrBox->angles, + pCurrBox->mins, + pCurrBox->maxs, + Color(pCurrBox->r, pCurrBox->g, pCurrBox->b, 255), + false, + false); + } + } + break; + } + LeaveCriticalSection((LPCRITICAL_SECTION)((char*)sEngineModule + 0x10DB0A38)); +} + +ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", DebugOverlay, ConVar, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + RenderLine = module.Offset(0x192A70).As(); + RenderBox = module.Offset(0x192520).As(); + RenderWireframeBox = module.Offset(0x193DA0).As(); + sEngineModule = reinterpret_cast(module.m_nAddress); + + // not in g_pCVar->FindVar by this point for whatever reason, so have to get from memory + ConVar* Cvar_enable_debug_overlays = module.Offset(0x10DB0990).As(); + Cvar_enable_debug_overlays->SetValue(false); + Cvar_enable_debug_overlays->m_pszDefaultValue = (char*)"0"; + Cvar_enable_debug_overlays->AddFlags(FCVAR_CHEAT); +} diff --git a/NorthstarDLL/client/demofixes.cpp b/NorthstarDLL/client/demofixes.cpp new file mode 100644 index 00000000..5fb49918 --- /dev/null +++ b/NorthstarDLL/client/demofixes.cpp @@ -0,0 +1,26 @@ +#include "pch.h" +#include "core/convar/convar.h" + +ON_DLL_LOAD_CLIENT("engine.dll", EngineDemoFixes, (CModule module)) +{ + // allow demo recording on loopback + module.Offset(0x8E1B1).NOP(2); + module.Offset(0x56CC3).NOP(2); +} + +ON_DLL_LOAD_CLIENT_RELIESON("client.dll", ClientDemoFixes, ConVar, (CModule module)) +{ + // change default values of demo cvars to enable them by default, but not autorecord + // this is before Host_Init, the setvalue calls here will get overwritten by custom cfgs/launch options + ConVar* Cvar_demo_enableDemos = R2::g_pCVar->FindVar("demo_enabledemos"); + Cvar_demo_enableDemos->m_pszDefaultValue = "1"; + Cvar_demo_enableDemos->SetValue(true); + + ConVar* Cvar_demo_writeLocalFile = R2::g_pCVar->FindVar("demo_writeLocalFile"); + Cvar_demo_writeLocalFile->m_pszDefaultValue = "1"; + Cvar_demo_writeLocalFile->SetValue(true); + + ConVar* Cvar_demo_autoRecord = R2::g_pCVar->FindVar("demo_autoRecord"); + Cvar_demo_autoRecord->m_pszDefaultValue = "0"; + Cvar_demo_autoRecord->SetValue(false); +} diff --git a/NorthstarDLL/client/diskvmtfixes.cpp b/NorthstarDLL/client/diskvmtfixes.cpp new file mode 100644 index 00000000..cd762c8a --- /dev/null +++ b/NorthstarDLL/client/diskvmtfixes.cpp @@ -0,0 +1,16 @@ +#include "pch.h" + +ON_DLL_LOAD_CLIENT("materialsystem_dx11.dll", DiskVMTFixes, (CModule module)) +{ + // in retail VMTs will never load if cache read is invalid due to a special case for them in KeyValues::LoadFromFile + // this effectively makes it impossible to load them from mods because we invalidate cache for doing this + // so uhh, stop that from happening + + // tbh idk why they even changed any of this what's the point it looks like it works fine who cares my god + + // matsystem KeyValues::LoadFromFile: patch special case on cache read failure for vmts + module.Offset(0x1281B9).Patch("EB"); + + // CMaterialSystem::FindMaterial: don't call function that crashes if previous patch is applied + module.Offset(0x5F55A).NOP(5); +} diff --git a/NorthstarDLL/client/languagehooks.cpp b/NorthstarDLL/client/languagehooks.cpp new file mode 100644 index 00000000..1a46633c --- /dev/null +++ b/NorthstarDLL/client/languagehooks.cpp @@ -0,0 +1,116 @@ +#include "pch.h" +#include "core/tier0.h" + +#include +#include + +AUTOHOOK_INIT() + +typedef LANGID (*Tier0_DetectDefaultLanguageType)(); + +bool CheckLangAudioExists(char* lang) +{ + std::string path {"r2\\sound\\general_"}; + path += lang; + path += ".mstr"; + return fs::exists(path); +} + +std::vector file_list(fs::path dir, std::regex ext_pattern) +{ + std::vector result; + + if (!fs::exists(dir) || !fs::is_directory(dir)) + return result; + + using iterator = fs::directory_iterator; + + const iterator end; + for (iterator iter {dir}; iter != end; ++iter) + { + const std::string filename = iter->path().filename().string(); + std::smatch matches; + if (fs::is_regular_file(*iter) && std::regex_match(filename, matches, ext_pattern)) + { + result.push_back(std::move(matches.str(1))); + } + } + + return result; +} + +std::string GetAnyInstalledAudioLanguage() +{ + for (const auto& lang : file_list("r2\\sound\\", std::regex(".*?general_([a-z]+)_patch_1\\.mstr"))) + if (lang != "general" || lang != "") + return lang; + return "NO LANGUAGE DETECTED"; +} + +// clang-format off +AUTOHOOK(GetGameLanguage, tier0.dll + 0xF560, +char*, __fastcall, ()) +// clang-format on +{ + auto tier0Handle = GetModuleHandleA("tier0.dll"); + auto Tier0_DetectDefaultLanguageType = GetProcAddress(tier0Handle, "Tier0_DetectDefaultLanguage"); + char* ingameLang1 = (char*)tier0Handle + 0xA9B60; // one of the globals we need to override if overriding lang (size: 256) + bool& canOriginDictateLang = *(bool*)((char*)tier0Handle + 0xA9A90); + + const char* forcedLanguage; + if (Tier0::CommandLine()->CheckParm("-language", &forcedLanguage)) + { + if (!CheckLangAudioExists((char*)forcedLanguage)) + { + spdlog::info( + "User tried to force the language (-language) to \"{}\", but audio for this language doesn't exist and the game is bound " + "to error, falling back to next option...", + forcedLanguage); + } + else + { + spdlog::info("User forcing the language (-language) to: {}", forcedLanguage); + strncpy(ingameLang1, forcedLanguage, 256); + return ingameLang1; + } + } + + canOriginDictateLang = true; // let it try + { + auto lang = GetGameLanguage(); + if (!CheckLangAudioExists(lang)) + { + if (strcmp(lang, "russian") != + 0) // don't log for "russian" since it's the default and that means Origin detection just didn't change it most likely + spdlog::info( + "Origin detected language \"{}\", but we do not have audio for it installed, falling back to the next option", lang); + } + else + { + spdlog::info("Origin detected language: {}", lang); + return lang; + } + } + + Tier0_DetectDefaultLanguageType(); // force the global in tier0 to be populated with language inferred from user's system rather than + // defaulting to Russian + canOriginDictateLang = false; // Origin has no say anymore, we will fallback to user's system setup language + auto lang = GetGameLanguage(); + spdlog::info("Detected system language: {}", lang); + if (!CheckLangAudioExists(lang)) + { + spdlog::warn("Caution, audio for this language does NOT exist. You might want to override your game language with -language " + "command line option."); + auto lang = GetAnyInstalledAudioLanguage(); + spdlog::warn("Falling back to the first installed audio language: {}", lang.c_str()); + strncpy(ingameLang1, lang.c_str(), 256); + return ingameLang1; + } + + return lang; +} + +ON_DLL_LOAD_CLIENT("tier0.dll", LanguageHooks, (CModule module)) +{ + AUTOHOOK_DISPATCH() +} diff --git a/NorthstarDLL/client/latencyflex.cpp b/NorthstarDLL/client/latencyflex.cpp new file mode 100644 index 00000000..a1ef72ca --- /dev/null +++ b/NorthstarDLL/client/latencyflex.cpp @@ -0,0 +1,44 @@ +#include "pch.h" +#include "core/convar/convar.h" + +AUTOHOOK_INIT() + +ConVar* Cvar_r_latencyflex; + +void (*m_winelfx_WaitAndBeginFrame)(); + +// clang-format off +AUTOHOOK(OnRenderStart, client.dll + 0x1952C0, +void, __fastcall, ()) +// clang-format on +{ + if (Cvar_r_latencyflex->GetBool() && m_winelfx_WaitAndBeginFrame) + m_winelfx_WaitAndBeginFrame(); + + OnRenderStart(); +} + +ON_DLL_LOAD_CLIENT_RELIESON("client.dll", LatencyFlex, ConVar, (CModule module)) +{ + // Connect to the LatencyFleX service + // LatencyFleX is an open source vendor agnostic replacement for Nvidia Reflex input latency reduction technology. + // https://ishitatsuyuki.github.io/post/latencyflex/ + HMODULE pLfxModule; + + if (pLfxModule = LoadLibraryA("latencyflex_layer.dll")) + m_winelfx_WaitAndBeginFrame = + reinterpret_cast(reinterpret_cast(GetProcAddress(pLfxModule, "lfx_WaitAndBeginFrame"))); + else if (pLfxModule = LoadLibraryA("latencyflex_wine.dll")) + m_winelfx_WaitAndBeginFrame = + reinterpret_cast(reinterpret_cast(GetProcAddress(pLfxModule, "winelfx_WaitAndBeginFrame"))); + else + { + spdlog::info("Unable to load LatencyFleX library, LatencyFleX disabled."); + return; + } + + AUTOHOOK_DISPATCH() + + spdlog::info("LatencyFleX initialized."); + Cvar_r_latencyflex = new ConVar("r_latencyflex", "1", FCVAR_ARCHIVE, "Whether or not to use LatencyFleX input latency reduction."); +} diff --git a/NorthstarDLL/client/localchatwriter.cpp b/NorthstarDLL/client/localchatwriter.cpp new file mode 100644 index 00000000..efa7eeee --- /dev/null +++ b/NorthstarDLL/client/localchatwriter.cpp @@ -0,0 +1,450 @@ +#include "pch.h" +#include "localchatwriter.h" + +class vgui_BaseRichText_vtable; + +class vgui_BaseRichText +{ + public: + vgui_BaseRichText_vtable* vtable; +}; + +class vgui_BaseRichText_vtable +{ + public: + char unknown1[1880]; + + void(__fastcall* InsertChar)(vgui_BaseRichText* self, wchar_t ch); + + // yes these are swapped from the Source 2013 code, who knows why + void(__fastcall* InsertStringWide)(vgui_BaseRichText* self, const wchar_t* wszText); + void(__fastcall* InsertStringAnsi)(vgui_BaseRichText* self, const char* text); + + void(__fastcall* SelectNone)(vgui_BaseRichText* self); + void(__fastcall* SelectAllText)(vgui_BaseRichText* self); + void(__fastcall* SelectNoText)(vgui_BaseRichText* self); + void(__fastcall* CutSelected)(vgui_BaseRichText* self); + void(__fastcall* CopySelected)(vgui_BaseRichText* self); + void(__fastcall* SetPanelInteractive)(vgui_BaseRichText* self, bool bInteractive); + void(__fastcall* SetUnusedScrollbarInvisible)(vgui_BaseRichText* self, bool bInvis); + + void* unknown2; + + void(__fastcall* GotoTextStart)(vgui_BaseRichText* self); + void(__fastcall* GotoTextEnd)(vgui_BaseRichText* self); + + void* unknown3[3]; + + void(__fastcall* SetVerticalScrollbar)(vgui_BaseRichText* self, bool state); + void(__fastcall* SetMaximumCharCount)(vgui_BaseRichText* self, int maxChars); + void(__fastcall* InsertColorChange)(vgui_BaseRichText* self, Color col); + void(__fastcall* InsertIndentChange)(vgui_BaseRichText* self, int pixelsIndent); + void(__fastcall* InsertClickableTextStart)(vgui_BaseRichText* self, const char* pchClickAction); + void(__fastcall* InsertClickableTextEnd)(vgui_BaseRichText* self); + void(__fastcall* InsertPossibleURLString)(vgui_BaseRichText* self, const char* text, Color URLTextColor, Color normalTextColor); + void(__fastcall* InsertFade)(vgui_BaseRichText* self, float flSustain, float flLength); + void(__fastcall* ResetAllFades)(vgui_BaseRichText* self, bool bHold, bool bOnlyExpired, float flNewSustain); + void(__fastcall* SetToFullHeight)(vgui_BaseRichText* self); + int(__fastcall* GetNumLines)(vgui_BaseRichText* self); +}; + +class CGameSettings +{ + public: + char unknown1[92]; + int isChatEnabled; +}; + +// Not sure what this actually refers to but chatFadeLength and chatFadeSustain +// have their value at the same offset +class CGameFloatVar +{ + public: + char unknown1[88]; + float value; +}; + +CGameSettings** gGameSettings; +CGameFloatVar** gChatFadeLength; +CGameFloatVar** gChatFadeSustain; + +CHudChat** CHudChat::allHuds; + +typedef void(__fastcall* ConvertANSIToUnicodeType)(LPCSTR ansi, int ansiCharLength, LPWSTR unicode, int unicodeCharLength); +ConvertANSIToUnicodeType ConvertANSIToUnicode; + +LocalChatWriter::SwatchColor swatchColors[4] = { + LocalChatWriter::MainTextColor, + LocalChatWriter::SameTeamNameColor, + LocalChatWriter::EnemyTeamNameColor, + LocalChatWriter::NetworkNameColor, +}; + +Color darkColors[8] = { + Color {0, 0, 0, 255}, + Color {205, 49, 49, 255}, + Color {13, 188, 121, 255}, + Color {229, 229, 16, 255}, + Color {36, 114, 200, 255}, + Color {188, 63, 188, 255}, + Color {17, 168, 205, 255}, + Color {229, 229, 229, 255}}; + +Color lightColors[8] = { + Color {102, 102, 102, 255}, + Color {241, 76, 76, 255}, + Color {35, 209, 139, 255}, + Color {245, 245, 67, 255}, + Color {59, 142, 234, 255}, + Color {214, 112, 214, 255}, + Color {41, 184, 219, 255}, + Color {255, 255, 255, 255}}; + +class AnsiEscapeParser +{ + public: + explicit AnsiEscapeParser(LocalChatWriter* writer) : m_writer(writer) {} + + void HandleVal(unsigned long val) + { + switch (m_next) + { + case Next::ControlType: + m_next = HandleControlType(val); + break; + case Next::ForegroundType: + m_next = HandleForegroundType(val); + break; + case Next::Foreground8Bit: + m_next = HandleForeground8Bit(val); + break; + case Next::ForegroundR: + m_next = HandleForegroundR(val); + break; + case Next::ForegroundG: + m_next = HandleForegroundG(val); + break; + case Next::ForegroundB: + m_next = HandleForegroundB(val); + break; + } + } + + private: + enum class Next + { + ControlType, + ForegroundType, + Foreground8Bit, + ForegroundR, + ForegroundG, + ForegroundB + }; + + LocalChatWriter* m_writer; + Next m_next = Next::ControlType; + Color m_expandedColor {0, 0, 0, 0}; + + Next HandleControlType(unsigned long val) + { + // Reset + if (val == 0 || val == 39) + { + m_writer->InsertSwatchColorChange(LocalChatWriter::MainTextColor); + return Next::ControlType; + } + + // Dark foreground color + if (val >= 30 && val < 38) + { + m_writer->InsertColorChange(darkColors[val - 30]); + return Next::ControlType; + } + + // Light foreground color + if (val >= 90 && val < 98) + { + m_writer->InsertColorChange(lightColors[val - 90]); + return Next::ControlType; + } + + // Game swatch color + if (val >= 110 && val < 114) + { + m_writer->InsertSwatchColorChange(swatchColors[val - 110]); + return Next::ControlType; + } + + // Expanded foreground color + if (val == 38) + { + return Next::ForegroundType; + } + + return Next::ControlType; + } + + Next HandleForegroundType(unsigned long val) + { + // Next values are r,g,b + if (val == 2) + { + m_expandedColor.SetColor(0, 0, 0, 255); + return Next::ForegroundR; + } + // Next value is 8-bit swatch color + if (val == 5) + { + return Next::Foreground8Bit; + } + + // Invalid + return Next::ControlType; + } + + Next HandleForeground8Bit(unsigned long val) + { + if (val < 8) + { + m_writer->InsertColorChange(darkColors[val]); + } + else if (val < 16) + { + m_writer->InsertColorChange(lightColors[val - 8]); + } + else if (val < 232) + { + unsigned char code = val - 16; + unsigned char blue = code % 6; + unsigned char green = ((code - blue) / 6) % 6; + unsigned char red = (code - blue - (green * 6)) / 36; + m_writer->InsertColorChange(Color {(unsigned char)(red * 51), (unsigned char)(green * 51), (unsigned char)(blue * 51), 255}); + } + else if (val < UCHAR_MAX) + { + unsigned char brightness = (val - 232) * 10 + 8; + m_writer->InsertColorChange(Color {brightness, brightness, brightness, 255}); + } + + return Next::ControlType; + } + + Next HandleForegroundR(unsigned long val) + { + if (val >= UCHAR_MAX) + return Next::ControlType; + + m_expandedColor[0] = (unsigned char)val; + return Next::ForegroundG; + } + + Next HandleForegroundG(unsigned long val) + { + if (val >= UCHAR_MAX) + return Next::ControlType; + + m_expandedColor[1] = (unsigned char)val; + return Next::ForegroundB; + } + + Next HandleForegroundB(unsigned long val) + { + if (val >= UCHAR_MAX) + return Next::ControlType; + + m_expandedColor[2] = (unsigned char)val; + m_writer->InsertColorChange(m_expandedColor); + return Next::ControlType; + } +}; + +LocalChatWriter::LocalChatWriter(Context context) : m_context(context) {} + +void LocalChatWriter::Write(const char* str) +{ + char writeBuffer[256]; + + while (true) + { + const char* startOfEscape = strstr(str, "\033["); + + if (startOfEscape == NULL) + { + // No more escape sequences, write the remaining text and exit + InsertText(str); + break; + } + + if (startOfEscape != str) + { + // There is some text before the escape sequence, just print that + size_t copyChars = startOfEscape - str; + if (copyChars > 255) + copyChars = 255; + + strncpy_s(writeBuffer, copyChars + 1, str, copyChars); + + InsertText(writeBuffer); + } + + const char* escape = startOfEscape + 2; + str = ApplyAnsiEscape(escape); + } +} + +void LocalChatWriter::WriteLine(const char* str) +{ + InsertChar(L'\n'); + InsertSwatchColorChange(MainTextColor); + Write(str); +} + +void LocalChatWriter::InsertChar(wchar_t ch) +{ + for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + + hud->m_richText->vtable->InsertChar(hud->m_richText, ch); + } + + if (ch != L'\n') + { + InsertDefaultFade(); + } +} + +void LocalChatWriter::InsertText(const char* str) +{ + spdlog::info(str); + + WCHAR messageUnicode[288]; + ConvertANSIToUnicode(str, -1, messageUnicode, 274); + + for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + + hud->m_richText->vtable->InsertStringWide(hud->m_richText, messageUnicode); + } + + InsertDefaultFade(); +} + +void LocalChatWriter::InsertText(const wchar_t* str) +{ + for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + + hud->m_richText->vtable->InsertStringWide(hud->m_richText, str); + } + + InsertDefaultFade(); +} + +void LocalChatWriter::InsertColorChange(Color color) +{ + for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + + hud->m_richText->vtable->InsertColorChange(hud->m_richText, color); + } +} + +static Color GetHudSwatchColor(CHudChat* hud, LocalChatWriter::SwatchColor swatchColor) +{ + switch (swatchColor) + { + case LocalChatWriter::MainTextColor: + return hud->m_mainTextColor; + + case LocalChatWriter::SameTeamNameColor: + return hud->m_sameTeamColor; + + case LocalChatWriter::EnemyTeamNameColor: + return hud->m_enemyTeamColor; + + case LocalChatWriter::NetworkNameColor: + return hud->m_networkNameColor; + } + + return Color(0, 0, 0, 0); +} + +void LocalChatWriter::InsertSwatchColorChange(SwatchColor swatchColor) +{ + for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + hud->m_richText->vtable->InsertColorChange(hud->m_richText, GetHudSwatchColor(hud, swatchColor)); + } +} + +const char* LocalChatWriter::ApplyAnsiEscape(const char* escape) +{ + AnsiEscapeParser decoder(this); + while (true) + { + char* afterControlType = NULL; + unsigned long controlType = strtoul(escape, &afterControlType, 10); + + // Malformed cases: + // afterControlType = NULL: strtoul errored + // controlType = 0 and escape doesn't actually start with 0: wasn't a number + if (afterControlType == NULL || (controlType == 0 && escape[0] != '0')) + { + return escape; + } + + decoder.HandleVal(controlType); + + // m indicates the end of the sequence + if (afterControlType[0] == 'm') + { + return afterControlType + 1; + } + + // : or ; indicates more values remain, anything else is malformed + if (afterControlType[0] != ':' && afterControlType[0] != ';') + { + return afterControlType; + } + + escape = afterControlType + 1; + } +} + +void LocalChatWriter::InsertDefaultFade() +{ + float fadeLength = 0.f; + float fadeSustain = 0.f; + if ((*gGameSettings)->isChatEnabled) + { + fadeLength = (*gChatFadeLength)->value; + fadeSustain = (*gChatFadeSustain)->value; + } + + for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) + { + if (hud->m_unknownContext != (int)m_context) + continue; + hud->m_richText->vtable->InsertFade(hud->m_richText, fadeSustain, fadeLength); + } +} + +ON_DLL_LOAD_CLIENT("client.dll", LocalChatWriter, (CModule module)) +{ + gGameSettings = module.Offset(0x11BAA48).As(); + gChatFadeLength = module.Offset(0x11BAB78).As(); + gChatFadeSustain = module.Offset(0x11BAC08).As(); + CHudChat::allHuds = module.Offset(0x11BA9E8).As(); + + ConvertANSIToUnicode = module.Offset(0x7339A0).As(); +} diff --git a/NorthstarDLL/client/localchatwriter.h b/NorthstarDLL/client/localchatwriter.h new file mode 100644 index 00000000..8bb1fb2f --- /dev/null +++ b/NorthstarDLL/client/localchatwriter.h @@ -0,0 +1,65 @@ +#pragma once +#include "pch.h" +#include "core/math/color.h" + +class vgui_BaseRichText; + +class CHudChat +{ + public: + static CHudChat** allHuds; + + char unknown1[720]; + + Color m_sameTeamColor; + Color m_enemyTeamColor; + Color m_mainTextColor; + Color m_networkNameColor; + + char unknown2[12]; + + int m_unknownContext; + + char unknown3[8]; + + vgui_BaseRichText* m_richText; + + CHudChat* next; + CHudChat* previous; +}; + +class LocalChatWriter +{ + public: + enum Context + { + NetworkContext = 0, + GameContext = 1 + }; + enum SwatchColor + { + MainTextColor, + SameTeamNameColor, + EnemyTeamNameColor, + NetworkNameColor + }; + + explicit LocalChatWriter(Context context); + + // Custom chat writing with ANSI escape codes + void Write(const char* str); + void WriteLine(const char* str); + + // Low-level RichText access + void InsertChar(wchar_t ch); + void InsertText(const char* str); + void InsertText(const wchar_t* str); + void InsertColorChange(Color color); + void InsertSwatchColorChange(SwatchColor color); + + private: + Context m_context; + + const char* ApplyAnsiEscape(const char* escape); + void InsertDefaultFade(); +}; diff --git a/NorthstarDLL/client/modlocalisation.cpp b/NorthstarDLL/client/modlocalisation.cpp new file mode 100644 index 00000000..430e3a5f --- /dev/null +++ b/NorthstarDLL/client/modlocalisation.cpp @@ -0,0 +1,35 @@ +#include "pch.h" +#include "mods/modmanager.h" + +AUTOHOOK_INIT() + +// clang-format off +AUTOHOOK(AddLocalisationFile, localize.dll + 0x6D80, +bool, __fastcall, (void* pVguiLocalize, const char* path, const char* pathId, char unknown)) +// clang-format on +{ + static bool bLoadModLocalisationFiles = true; + bool ret = AddLocalisationFile(pVguiLocalize, path, pathId, unknown); + + if (ret) + spdlog::info("Loaded localisation file {} successfully", path); + + if (!bLoadModLocalisationFiles) + return ret; + + bLoadModLocalisationFiles = false; + + for (Mod mod : g_pModManager->m_LoadedMods) + if (mod.m_bEnabled) + for (std::string& localisationFile : mod.LocalisationFiles) + AddLocalisationFile(pVguiLocalize, localisationFile.c_str(), pathId, unknown); + + bLoadModLocalisationFiles = true; + + return ret; +} + +ON_DLL_LOAD_CLIENT("localize.dll", Localize, (CModule module)) +{ + AUTOHOOK_DISPATCH() +} diff --git a/NorthstarDLL/client/r2client.cpp b/NorthstarDLL/client/r2client.cpp new file mode 100644 index 00000000..2e7bd564 --- /dev/null +++ b/NorthstarDLL/client/r2client.cpp @@ -0,0 +1,20 @@ +#include "pch.h" +#include "r2client.h" + +using namespace R2; + +// use the R2 namespace for game funcs +namespace R2 +{ + char* g_pLocalPlayerUserID; + char* g_pLocalPlayerOriginToken; + GetBaseLocalClientType GetBaseLocalClient; +} // namespace R2 + +ON_DLL_LOAD("engine.dll", R2EngineClient, (CModule module)) +{ + g_pLocalPlayerUserID = module.Offset(0x13F8E688).As(); + g_pLocalPlayerOriginToken = module.Offset(0x13979C80).As(); + + GetBaseLocalClient = module.Offset(0x78200).As(); +} diff --git a/NorthstarDLL/client/r2client.h b/NorthstarDLL/client/r2client.h new file mode 100644 index 00000000..64ed6c61 --- /dev/null +++ b/NorthstarDLL/client/r2client.h @@ -0,0 +1,11 @@ +#pragma once + +// use the R2 namespace for game funcs +namespace R2 +{ + extern char* g_pLocalPlayerUserID; + extern char* g_pLocalPlayerOriginToken; + + typedef void* (*GetBaseLocalClientType)(); + extern GetBaseLocalClientType GetBaseLocalClient; +} // namespace R2 diff --git a/NorthstarDLL/clientauthhooks.cpp b/NorthstarDLL/clientauthhooks.cpp deleted file mode 100644 index cd01ad91..00000000 --- a/NorthstarDLL/clientauthhooks.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include "pch.h" -#include "masterserver.h" -#include "convar.h" -#include "r2client.h" - -AUTOHOOK_INIT() - -ConVar* Cvar_ns_has_agreed_to_send_token; - -// mirrored in script -const int NOT_DECIDED_TO_SEND_TOKEN = 0; -const int AGREED_TO_SEND_TOKEN = 1; -const int DISAGREED_TO_SEND_TOKEN = 2; - -// clang-format off -AUTOHOOK(AuthWithStryder, engine.dll + 0x1843A0, -void, __fastcall, (void* a1)) -// clang-format on -{ - // game will call this forever, until it gets a valid auth key - // so, we need to manually invalidate our key until we're authed with northstar, then we'll allow game to auth with stryder - if (!g_pMasterServerManager->m_bOriginAuthWithMasterServerDone && Cvar_ns_has_agreed_to_send_token->GetInt() != DISAGREED_TO_SEND_TOKEN) - { - // if player has agreed to send token and we aren't already authing, try to auth - if (Cvar_ns_has_agreed_to_send_token->GetInt() == AGREED_TO_SEND_TOKEN && - !g_pMasterServerManager->m_bOriginAuthWithMasterServerInProgress) - g_pMasterServerManager->AuthenticateOriginWithMasterServer(R2::g_pLocalPlayerUserID, R2::g_pLocalPlayerOriginToken); - - // invalidate key so auth will fail - *R2::g_pLocalPlayerOriginToken = 0; - } - - AuthWithStryder(a1); -} - -char* p3PToken; - -// clang-format off -AUTOHOOK(Auth3PToken, engine.dll + 0x183760, -char*, __fastcall, ()) -// clang-format on -{ - if (g_pMasterServerManager->m_sOwnClientAuthToken[0]) - { - memset(p3PToken, 0x0, 1024); - strcpy(p3PToken, "Protocol 3: Protect the Pilot"); - } - - return Auth3PToken(); -} - -ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", ClientAuthHooks, ConVar, (CModule module)) -{ - AUTOHOOK_DISPATCH() - - p3PToken = module.Offset(0x13979D80).As(); - - // this cvar will save to cfg once initially agreed with - Cvar_ns_has_agreed_to_send_token = new ConVar( - "ns_has_agreed_to_send_token", - "0", - FCVAR_ARCHIVE_PLAYERPROFILE, - "whether the user has agreed to send their origin token to the northstar masterserver"); -} diff --git a/NorthstarDLL/clientchathooks.cpp b/NorthstarDLL/clientchathooks.cpp deleted file mode 100644 index 847b5eb1..00000000 --- a/NorthstarDLL/clientchathooks.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "pch.h" -#include "squirrel.h" - -#include "serverchathooks.h" -#include "localchatwriter.h" - -#include - -AUTOHOOK_INIT() - -// 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; - - int senderId = inboxId & CUSTOM_MESSAGE_INDEX_MASK; - bool isAnonymous = senderId == 0; - bool isCustom = isAnonymous || (inboxId & CUSTOM_MESSAGE_INDEX_BIT); - - // Type is set to 0 for non-custom messages, custom messages have a type encoded as the first byte - int type = 0; - const char* payload = message; - if (isCustom) - { - type = message[0]; - payload = message + 1; - } - - SQRESULT result = g_pSquirrel->Call( - "CHudChat_ProcessMessageStartThread", static_cast(senderId) - 1, payload, isTeam, isDead, type); - if (result == SQRESULT_ERROR) - for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) - CHudChat__AddGameLine(hud, message, inboxId, isTeam, isDead); -} - -ADD_SQFUNC("void", NSChatWrite, "int context, string text", "", ScriptContext::CLIENT) -{ - int chatContext = g_pSquirrel->getinteger(sqvm, 1); - const char* str = g_pSquirrel->getstring(sqvm, 2); - - LocalChatWriter((LocalChatWriter::Context)chatContext).Write(str); - return SQRESULT_NULL; -} - -ADD_SQFUNC("void", NSChatWriteRaw, "int context, string text", "", ScriptContext::CLIENT) -{ - int chatContext = g_pSquirrel->getinteger(sqvm, 1); - const char* str = g_pSquirrel->getstring(sqvm, 2); - - LocalChatWriter((LocalChatWriter::Context)chatContext).InsertText(str); - return SQRESULT_NULL; -} - -ADD_SQFUNC("void", NSChatWriteLine, "int context, string text", "", ScriptContext::CLIENT) -{ - int chatContext = g_pSquirrel->getinteger(sqvm, 1); - const char* str = g_pSquirrel->getstring(sqvm, 2); - - LocalChatWriter((LocalChatWriter::Context)chatContext).WriteLine(str); - return SQRESULT_NULL; -} - -ON_DLL_LOAD_CLIENT("client.dll", ClientChatHooks, (CModule module)) -{ - AUTOHOOK_DISPATCH() -} diff --git a/NorthstarDLL/clientruihooks.cpp b/NorthstarDLL/clientruihooks.cpp deleted file mode 100644 index 3cb08368..00000000 --- a/NorthstarDLL/clientruihooks.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "pch.h" -#include "convar.h" - -AUTOHOOK_INIT() - -ConVar* Cvar_rui_drawEnable; - -// clang-format off -AUTOHOOK(DrawRUIFunc, engine.dll + 0xFC500, -bool, __fastcall, (void* a1, float* a2)) -// clang-format on -{ - if (!Cvar_rui_drawEnable->GetBool()) - return 0; - - return DrawRUIFunc(a1, a2); -} - -ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", RUI, ConVar, (CModule module)) -{ - AUTOHOOK_DISPATCH() - - Cvar_rui_drawEnable = new ConVar("rui_drawEnable", "1", FCVAR_CLIENTDLL, "Controls whether RUI should be drawn"); -} diff --git a/NorthstarDLL/clientvideooverrides.cpp b/NorthstarDLL/clientvideooverrides.cpp deleted file mode 100644 index e4d96ff5..00000000 --- a/NorthstarDLL/clientvideooverrides.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "pch.h" -#include "modmanager.h" - -AUTOHOOK_INIT() - -// clang-format off -AUTOHOOK_PROCADDRESS(BinkOpen, bink2w64.dll, BinkOpen, -void*, __fastcall, (const char* path, uint32_t flags)) -// clang-format on -{ - std::string filename(fs::path(path).filename().string()); - spdlog::info("BinkOpen {}", filename); - - // figure out which mod is handling the bink - Mod* fileOwner = nullptr; - for (Mod& mod : g_pModManager->m_LoadedMods) - { - if (!mod.m_bEnabled) - continue; - - if (std::find(mod.BinkVideos.begin(), mod.BinkVideos.end(), filename) != mod.BinkVideos.end()) - fileOwner = &mod; - } - - if (fileOwner) - { - // create new path - fs::path binkPath(fileOwner->m_ModDirectory / "media" / filename); - return BinkOpen(binkPath.string().c_str(), flags); - } - else - return BinkOpen(path, flags); -} - -ON_DLL_LOAD_CLIENT("engine.dll", BinkVideo, (CModule module)) -{ - AUTOHOOK_DISPATCH() - - // remove engine check for whether the bik we're trying to load exists in r2/media, as this will fail for biks in mods - // note: the check in engine is actually unnecessary, so it's just useless in practice and we lose nothing by removing it - module.Offset(0x459AD).NOP(6); -} diff --git a/NorthstarDLL/color.cpp b/NorthstarDLL/color.cpp deleted file mode 100644 index 05cf645f..00000000 --- a/NorthstarDLL/color.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "pch.h" - -// clang-format off -namespace NS::Colors -{ - Color SCRIPT_UI (100, 255, 255); - Color SCRIPT_CL (100, 255, 100); - Color SCRIPT_SV (255, 100, 255); - Color NATIVE_UI (50 , 150, 150); - Color NATIVE_CL (50 , 150, 50 ); - Color NATIVE_SV (150, 50 , 150); - Color NATIVE_ENGINE (252, 133, 153); - Color FILESYSTEM (0 , 150, 150); - Color RPAK (255, 190, 0 ); - Color NORTHSTAR (66 , 72 , 128); - Color ECHO (150, 150, 159); - - Color TRACE (0 , 255, 255); - Color DEBUG (0 , 255, 255); - Color INFO (16 , 160, 16 ); - Color WARN (255, 255, 0 ); - Color ERR (255, 50 , 50 ); - Color CRIT (255, 0 , 0 ); - Color OFF (0 , 0 , 0 ); -}; -// clang-format on diff --git a/NorthstarDLL/color.h b/NorthstarDLL/color.h deleted file mode 100644 index 4dc9b36e..00000000 --- a/NorthstarDLL/color.h +++ /dev/null @@ -1,197 +0,0 @@ -#pragma once - -struct color24 -{ - uint8_t r, g, b; -}; - -typedef struct color32_s -{ - bool operator!=(const struct color32_s& other) const - { - return r != other.r || g != other.g || b != other.b || a != other.a; - } - inline unsigned* asInt(void) - { - return reinterpret_cast(this); - } - inline const unsigned* asInt(void) const - { - return reinterpret_cast(this); - } - inline void Copy(const color32_s& rhs) - { - *asInt() = *rhs.asInt(); - } - - uint8_t r, g, b, a; -} color32; - -struct SourceColor -{ - unsigned char R; - unsigned char G; - unsigned char B; - unsigned char A; - - SourceColor(unsigned char r, unsigned char g, unsigned char b, unsigned char a) - { - R = r; - G = g; - B = b; - A = a; - } - - SourceColor() - { - R = 0; - G = 0; - B = 0; - A = 0; - } -}; - -//----------------------------------------------------------------------------- -// Purpose: Basic handler for an rgb set of colors -// This class is fully inline -//----------------------------------------------------------------------------- -class Color -{ - public: - Color(int r, int g, int b, int a = 255) - { - _color[0] = (unsigned char)r; - _color[1] = (unsigned char)g; - _color[2] = (unsigned char)b; - _color[3] = (unsigned char)a; - } - void SetColor(int _r, int _g, int _b, int _a = 0) - { - _color[0] = (unsigned char)_r; - _color[1] = (unsigned char)_g; - _color[2] = (unsigned char)_b; - _color[3] = (unsigned char)_a; - } - void GetColor(int& _r, int& _g, int& _b, int& _a) const - { - _r = _color[0]; - _g = _color[1]; - _b = _color[2]; - _a = _color[3]; - } - int GetValue(int index) const - { - return _color[index]; - } - void SetRawColor(int color32) - { - *((int*)this) = color32; - } - int GetRawColor(void) const - { - return *((int*)this); - } - - inline int r() const - { - return _color[0]; - } - inline int g() const - { - return _color[1]; - } - inline int b() const - { - return _color[2]; - } - inline int a() const - { - return _color[3]; - } - - unsigned char& operator[](int index) - { - return _color[index]; - } - - const unsigned char& operator[](int index) const - { - return _color[index]; - } - - bool operator==(const Color& rhs) const - { - return (*((int*)this) == *((int*)&rhs)); - } - - bool operator!=(const Color& rhs) const - { - return !(operator==(rhs)); - } - - Color& operator=(const Color& rhs) - { - SetRawColor(rhs.GetRawColor()); - return *this; - } - - Color& operator=(const color32& rhs) - { - _color[0] = rhs.r; - _color[1] = rhs.g; - _color[2] = rhs.b; - _color[3] = rhs.a; - return *this; - } - - color32 ToColor32(void) const - { - color32 newColor {}; - newColor.r = _color[0]; - newColor.g = _color[1]; - newColor.b = _color[2]; - newColor.a = _color[3]; - return newColor; - } - - std::string ToANSIColor() - { - std::string out = "\033[38;2;"; - out += std::to_string(_color[0]) + ";"; - out += std::to_string(_color[1]) + ";"; - out += std::to_string(_color[2]) + ";"; - out += "49m"; - return out; - } - - SourceColor ToSourceColor() - { - return SourceColor(_color[0], _color[1], _color[2], _color[3]); - } - - private: - unsigned char _color[4]; -}; - -namespace NS::Colors -{ - extern Color SCRIPT_UI; - extern Color SCRIPT_CL; - extern Color SCRIPT_SV; - extern Color NATIVE_UI; - extern Color NATIVE_CL; - extern Color NATIVE_SV; - extern Color NATIVE_ENGINE; - extern Color FILESYSTEM; - extern Color RPAK; - extern Color NORTHSTAR; - extern Color ECHO; - - extern Color TRACE; - extern Color DEBUG; - extern Color INFO; - extern Color WARN; - extern Color ERR; - extern Color CRIT; - extern Color OFF; -}; // namespace NS::Colors diff --git a/NorthstarDLL/concommand.cpp b/NorthstarDLL/concommand.cpp deleted file mode 100644 index 6d77e137..00000000 --- a/NorthstarDLL/concommand.cpp +++ /dev/null @@ -1,151 +0,0 @@ -#include "pch.h" -#include "concommand.h" -#include "misccommands.h" - -#include - -//----------------------------------------------------------------------------- -// Purpose: Returns true if this is a command -// Output : bool -//----------------------------------------------------------------------------- -bool ConCommand::IsCommand(void) const -{ - return true; -} - -//----------------------------------------------------------------------------- -// Purpose: Returns true if this is a command -// Output : bool -//----------------------------------------------------------------------------- -bool ConCommandBase::IsCommand(void) const -{ - return true; -} - -//----------------------------------------------------------------------------- -// Purpose: Has this cvar been registered -// Output : Returns true on success, false on failure. -//----------------------------------------------------------------------------- -bool ConCommandBase::IsRegistered(void) const -{ - return m_bRegistered; -} - -//----------------------------------------------------------------------------- -// Purpose: Test each ConCommand query before execution. -// Input : *pCommandBase - nFlags -// Output : False if execution is permitted, true if not. -//----------------------------------------------------------------------------- -bool ConCommandBase::IsFlagSet(int nFlags) const -{ - return m_nFlags & nFlags; -} - -//----------------------------------------------------------------------------- -// Purpose: Checks if ConCommand has requested flags. -// Input : nFlags - -// Output : True if ConCommand has nFlags. -//----------------------------------------------------------------------------- -bool ConCommandBase::HasFlags(int nFlags) -{ - return m_nFlags & nFlags; -} - -//----------------------------------------------------------------------------- -// Purpose: Add's flags to ConCommand. -// Input : nFlags - -//----------------------------------------------------------------------------- -void ConCommandBase::AddFlags(int nFlags) -{ - m_nFlags |= nFlags; -} - -//----------------------------------------------------------------------------- -// Purpose: Removes flags from ConCommand. -// Input : nFlags - -//----------------------------------------------------------------------------- -void ConCommandBase::RemoveFlags(int nFlags) -{ - m_nFlags &= ~nFlags; -} - -//----------------------------------------------------------------------------- -// Purpose: Returns current flags. -// Output : int -//----------------------------------------------------------------------------- -int ConCommandBase::GetFlags(void) const -{ - return m_nFlags; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Output : const ConCommandBase -//----------------------------------------------------------------------------- -ConCommandBase* ConCommandBase::GetNext(void) const -{ - return m_pNext; -} - -//----------------------------------------------------------------------------- -// Purpose: Returns the ConCommandBase help text. -// Output : const char* -//----------------------------------------------------------------------------- -const char* ConCommandBase::GetHelpText(void) const -{ - return m_pszHelpString; -} - -//----------------------------------------------------------------------------- -// Purpose: Copies string using local new/delete operators -// Input : *szFrom - -// Output : char -//----------------------------------------------------------------------------- -char* ConCommandBase::CopyString(const char* szFrom) const -{ - size_t nLen; - char* szTo; - - nLen = strlen(szFrom); - if (nLen <= 0) - { - szTo = new char[1]; - szTo[0] = 0; - } - else - { - szTo = new char[nLen + 1]; - memmove(szTo, szFrom, nLen + 1); - } - 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(); - AddMiscConCommands(); -} diff --git a/NorthstarDLL/concommand.h b/NorthstarDLL/concommand.h deleted file mode 100644 index 89363bc7..00000000 --- a/NorthstarDLL/concommand.h +++ /dev/null @@ -1,140 +0,0 @@ -#pragma once - -// From Source SDK -class ConCommandBase; -class IConCommandBaseAccessor -{ - public: - // Flags is a combination of FCVAR flags in cvar.h. - // hOut is filled in with a handle to the variable. - virtual bool RegisterConCommandBase(ConCommandBase* pVar) = 0; -}; - -class CCommand -{ - public: - CCommand() = delete; - - int64_t ArgC() const; - const char** ArgV() const; - const char* ArgS() const; // All args that occur after the 0th arg, in string form - const char* GetCommandString() const; // The entire command in string form, including the 0th arg - const char* operator[](int nIndex) const; // Gets at arguments - const char* Arg(int nIndex) const; // Gets at arguments - - static int MaxCommandLength(); - - private: - enum - { - COMMAND_MAX_ARGC = 64, - COMMAND_MAX_LENGTH = 512, - }; - - int64_t m_nArgc; - int64_t m_nArgv0Size; - char m_pArgSBuffer[COMMAND_MAX_LENGTH]; - char m_pArgvBuffer[COMMAND_MAX_LENGTH]; - const char* m_ppArgv[COMMAND_MAX_ARGC]; -}; - -inline int CCommand::MaxCommandLength() -{ - return COMMAND_MAX_LENGTH - 1; -} -inline int64_t CCommand::ArgC() const -{ - return m_nArgc; -} -inline const char** CCommand::ArgV() const -{ - return m_nArgc ? (const char**)m_ppArgv : NULL; -} -inline const char* CCommand::ArgS() const -{ - return m_nArgv0Size ? &m_pArgSBuffer[m_nArgv0Size] : ""; -} -inline const char* CCommand::GetCommandString() const -{ - return m_nArgc ? m_pArgSBuffer : ""; -} -inline const char* CCommand::Arg(int nIndex) const -{ - // FIXME: Many command handlers appear to not be particularly careful - // about checking for valid argc range. For now, we're going to - // do the extra check and return an empty string if it's out of range - if (nIndex < 0 || nIndex >= m_nArgc) - return ""; - return m_ppArgv[nIndex]; -} -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 -{ - public: - bool HasFlags(int nFlags); - void AddFlags(int nFlags); - void RemoveFlags(int nFlags); - - bool IsCommand(void) const; - bool IsRegistered(void) const; - bool IsFlagSet(int nFlags) const; - static bool IsFlagSet(ConCommandBase* pCommandBase, int nFlags); // For hooking to engine's implementation. - - int GetFlags(void) const; - ConCommandBase* GetNext(void) const; - const char* GetHelpText(void) const; - - char* CopyString(const char* szFrom) const; - - void* m_pConCommandBaseVTable; // 0x0000 - ConCommandBase* m_pNext; // 0x0008 - bool m_bRegistered; // 0x0010 - char pad_0011[7]; // 0x0011 <- 3 bytes padding + unk int32. - const char* m_pszName; // 0x0018 - const char* m_pszHelpString; // 0x0020 - int m_nFlags; // 0x0028 - ConCommandBase* s_pConCommandBases; // 0x002C - IConCommandBaseAccessor* s_pAccessor; // 0x0034 -}; // Size: 0x0040 - -// taken from ttf2sdk -class ConCommand : public ConCommandBase -{ - friend class CCVar; - - public: - ConCommand(void) {}; // !TODO: Rebuild engine constructor in SDK instead. - ConCommand(const char* szName, const char* szHelpString, int nFlags, void* pCallback, void* pCommandCompletionCallback); - void Init(void); - bool IsCommand(void) const; - - 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 - int unk1; // 0x005C -}; // Size: 0x0060 - -void RegisterConCommand(const char* name, void (*callback)(const CCommand&), const char* helpString, int flags); -void RegisterConCommand( - const char* name, void (*callback)(const CCommand&), const char* helpString, int flags, FnCommandCompletionCallback completionCallback); diff --git a/NorthstarDLL/config/profile.cpp b/NorthstarDLL/config/profile.cpp new file mode 100644 index 00000000..7518a2b3 --- /dev/null +++ b/NorthstarDLL/config/profile.cpp @@ -0,0 +1,44 @@ +#include "pch.h" +#include "config/profile.h" +#include "dedicated/dedicated.h" +#include + +std::string GetNorthstarPrefix() +{ + return NORTHSTAR_FOLDER_PREFIX; +} + +void InitialiseNorthstarPrefix() +{ + char* clachar = strstr(GetCommandLineA(), "-profile="); + if (clachar) + { + std::string cla = std::string(clachar); + if (strncmp(cla.substr(9, 1).c_str(), "\"", 1)) + { + int space = cla.find(" "); + std::string dirname = cla.substr(9, space - 9); + spdlog::info("Found profile in command line arguments: " + dirname); + NORTHSTAR_FOLDER_PREFIX = dirname; + } + else + { + std::string quote = "\""; + int quote1 = cla.find(quote); + int quote2 = (cla.substr(quote1 + 1)).find(quote); + std::string dirname = cla.substr(quote1 + 1, quote2); + spdlog::info("Found profile in command line arguments: " + dirname); + NORTHSTAR_FOLDER_PREFIX = dirname; + } + } + else + { + spdlog::info("Profile was not found in command line arguments. Using default: R2Northstar"); + NORTHSTAR_FOLDER_PREFIX = "R2Northstar"; + } + + // set the console title to show the current profile + // dont do this on dedi as title contains useful information on dedi and setting title breaks it as well + if (!IsDedicatedServer()) + SetConsoleTitleA((std::string("NorthstarLauncher | ") + NORTHSTAR_FOLDER_PREFIX).c_str()); +} diff --git a/NorthstarDLL/config/profile.h b/NorthstarDLL/config/profile.h new file mode 100644 index 00000000..9f3087fb --- /dev/null +++ b/NorthstarDLL/config/profile.h @@ -0,0 +1,7 @@ +#pragma once +#include + +static std::string NORTHSTAR_FOLDER_PREFIX; + +void InitialiseNorthstarPrefix(); +std::string GetNorthstarPrefix(); diff --git a/NorthstarDLL/convar.cpp b/NorthstarDLL/convar.cpp deleted file mode 100644 index 80f93c64..00000000 --- a/NorthstarDLL/convar.cpp +++ /dev/null @@ -1,490 +0,0 @@ -#include "pch.h" -#include "bits.h" -#include "cvar.h" -#include "convar.h" -#include "sourceinterface.h" - -#include - -typedef void (*ConVarRegisterType)( - ConVar* pConVar, - const char* pszName, - const char* pszDefaultValue, - int nFlags, - const char* pszHelpString, - bool bMin, - float fMin, - bool bMax, - float fMax, - void* pCallback); -ConVarRegisterType conVarRegister; - -typedef void (*ConVarMallocType)(void* pConVarMaloc, int a2, int a3); -ConVarMallocType conVarMalloc; - -void* g_pConVar_Vtable = nullptr; -void* g_pIConVar_Vtable = nullptr; - -//----------------------------------------------------------------------------- -// Purpose: ConVar interface initialization -//----------------------------------------------------------------------------- -ON_DLL_LOAD("engine.dll", ConVar, (CModule module)) -{ - conVarMalloc = module.Offset(0x415C20).As(); - conVarRegister = module.Offset(0x417230).As(); - - g_pConVar_Vtable = module.Offset(0x67FD28); - g_pIConVar_Vtable = module.Offset(0x67FDC8); - - R2::g_pCVarInterface = new SourceInterface("vstdlib.dll", "VEngineCvar007"); - R2::g_pCVar = *R2::g_pCVarInterface; -} - -//----------------------------------------------------------------------------- -// Purpose: constructor -//----------------------------------------------------------------------------- -ConVar::ConVar(const char* pszName, const char* pszDefaultValue, int nFlags, const char* pszHelpString) -{ - spdlog::info("Registering Convar {}", pszName); - - this->m_ConCommandBase.m_pConCommandBaseVTable = g_pConVar_Vtable; - this->m_ConCommandBase.s_pConCommandBases = (ConCommandBase*)g_pIConVar_Vtable; - - conVarMalloc(&this->m_pMalloc, 0, 0); // Allocate new memory for ConVar. - conVarRegister(this, pszName, pszDefaultValue, nFlags, pszHelpString, 0, 0, 0, 0, 0); -} - -//----------------------------------------------------------------------------- -// Purpose: constructor -//----------------------------------------------------------------------------- -ConVar::ConVar( - const char* pszName, - const char* pszDefaultValue, - int nFlags, - const char* pszHelpString, - bool bMin, - float fMin, - bool bMax, - float fMax, - FnChangeCallback_t pCallback) -{ - spdlog::info("Registering Convar {}", pszName); - - this->m_ConCommandBase.m_pConCommandBaseVTable = g_pConVar_Vtable; - this->m_ConCommandBase.s_pConCommandBases = (ConCommandBase*)g_pIConVar_Vtable; - - conVarMalloc(&this->m_pMalloc, 0, 0); // Allocate new memory for ConVar. - conVarRegister(this, pszName, pszDefaultValue, nFlags, pszHelpString, bMin, fMin, bMax, fMax, pCallback); -} - -//----------------------------------------------------------------------------- -// Purpose: destructor -//----------------------------------------------------------------------------- -ConVar::~ConVar(void) -{ - if (m_Value.m_pszString) - delete[] m_Value.m_pszString; -} - -//----------------------------------------------------------------------------- -// Purpose: Returns the base ConVar name. -// Output : const char* -//----------------------------------------------------------------------------- -const char* ConVar::GetBaseName(void) const -{ - return m_ConCommandBase.m_pszName; -} - -//----------------------------------------------------------------------------- -// Purpose: Returns the ConVar help text. -// Output : const char* -//----------------------------------------------------------------------------- -const char* ConVar::GetHelpText(void) const -{ - return m_ConCommandBase.m_pszHelpString; -} - -//----------------------------------------------------------------------------- -// Purpose: Add's flags to ConVar. -// Input : nFlags - -//----------------------------------------------------------------------------- -void ConVar::AddFlags(int nFlags) -{ - m_ConCommandBase.m_nFlags |= nFlags; -} - -//----------------------------------------------------------------------------- -// Purpose: Removes flags from ConVar. -// Input : nFlags - -//----------------------------------------------------------------------------- -void ConVar::RemoveFlags(int nFlags) -{ - m_ConCommandBase.m_nFlags &= ~nFlags; -} - -//----------------------------------------------------------------------------- -// Purpose: Return ConVar value as a boolean. -// Output : bool -//----------------------------------------------------------------------------- -bool ConVar::GetBool(void) const -{ - return !!GetInt(); -} - -//----------------------------------------------------------------------------- -// Purpose: Return ConVar value as a float. -// Output : float -//----------------------------------------------------------------------------- -float ConVar::GetFloat(void) const -{ - return m_Value.m_fValue; -} - -//----------------------------------------------------------------------------- -// Purpose: Return ConVar value as an integer. -// Output : int -//----------------------------------------------------------------------------- -int ConVar::GetInt(void) const -{ - return m_Value.m_nValue; -} - -//----------------------------------------------------------------------------- -// Purpose: Return ConVar value as a color. -// Output : Color -//----------------------------------------------------------------------------- -Color ConVar::GetColor(void) const -{ - unsigned char* pColorElement = ((unsigned char*)&m_Value.m_nValue); - return Color(pColorElement[0], pColorElement[1], pColorElement[2], pColorElement[3]); -} - -//----------------------------------------------------------------------------- -// Purpose: Return ConVar value as a string. -// Output : const char * -//----------------------------------------------------------------------------- -const char* ConVar::GetString(void) const -{ - if (m_ConCommandBase.m_nFlags & FCVAR_NEVER_AS_STRING) - { - return "FCVAR_NEVER_AS_STRING"; - } - - char const* str = m_Value.m_pszString; - return str ? str : ""; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : flMinVal - -// Output : true if there is a min set. -//----------------------------------------------------------------------------- -bool ConVar::GetMin(float& flMinVal) const -{ - flMinVal = m_fMinVal; - return m_bHasMin; -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : flMaxVal - -// Output : true if there is a max set. -//----------------------------------------------------------------------------- -bool ConVar::GetMax(float& flMaxVal) const -{ - flMaxVal = m_fMaxVal; - return m_bHasMax; -} - -//----------------------------------------------------------------------------- -// Purpose: returns the min value. -// Output : float -//----------------------------------------------------------------------------- -float ConVar::GetMinValue(void) const -{ - return m_fMinVal; -} - -//----------------------------------------------------------------------------- -// Purpose: returns the max value. -// Output : float -//----------------------------------------------------------------------------- -float ConVar::GetMaxValue(void) const -{ - return m_fMaxVal; - ; -} - -//----------------------------------------------------------------------------- -// Purpose: checks if ConVar has min value. -// Output : bool -//----------------------------------------------------------------------------- -bool ConVar::HasMin(void) const -{ - return m_bHasMin; -} - -//----------------------------------------------------------------------------- -// Purpose: checks if ConVar has max value. -// Output : bool -//----------------------------------------------------------------------------- -bool ConVar::HasMax(void) const -{ - return m_bHasMax; -} - -//----------------------------------------------------------------------------- -// Purpose: sets the ConVar int value. -// Input : nValue - -//----------------------------------------------------------------------------- -void ConVar::SetValue(int nValue) -{ - if (nValue == m_Value.m_nValue) - { - return; - } - - float flValue = (float)nValue; - - // Check bounds. - if (ClampValue(flValue)) - { - nValue = (int)(flValue); - } - - // Redetermine value. - float flOldValue = m_Value.m_fValue; - m_Value.m_fValue = flValue; - m_Value.m_nValue = nValue; - - if (!(m_ConCommandBase.m_nFlags & FCVAR_NEVER_AS_STRING)) - { - char szTempValue[32]; - snprintf(szTempValue, sizeof(szTempValue), "%d", m_Value.m_nValue); - ChangeStringValue(szTempValue, flOldValue); - } -} - -//----------------------------------------------------------------------------- -// Purpose: sets the ConVar float value. -// Input : flValue - -//----------------------------------------------------------------------------- -void ConVar::SetValue(float flValue) -{ - if (flValue == m_Value.m_fValue) - { - return; - } - - // Check bounds. - ClampValue(flValue); - - // Redetermine value. - float flOldValue = m_Value.m_fValue; - m_Value.m_fValue = flValue; - m_Value.m_nValue = (int)m_Value.m_fValue; - - if (!(m_ConCommandBase.m_nFlags & FCVAR_NEVER_AS_STRING)) - { - char szTempValue[32]; - snprintf(szTempValue, sizeof(szTempValue), "%f", m_Value.m_fValue); - ChangeStringValue(szTempValue, flOldValue); - } -} - -//----------------------------------------------------------------------------- -// Purpose: sets the ConVar string value. -// Input : *szValue - -//----------------------------------------------------------------------------- -void ConVar::SetValue(const char* pszValue) -{ - if (strcmp(this->m_Value.m_pszString, pszValue) == 0) - return; - - char szTempValue[32] {}; - const char* pszNewValue {}; - - float flOldValue = m_Value.m_fValue; - pszNewValue = (char*)pszValue; - if (!pszNewValue) - { - pszNewValue = ""; - } - - if (!SetColorFromString(pszValue)) - { - // Not a color, do the standard thing - float flNewValue = (float)atof(pszValue); - if (!IsFinite(flNewValue)) - { - spdlog::warn("Warning: ConVar '{}' = '{}' is infinite, clamping value.\n", GetBaseName(), pszValue); - flNewValue = FLT_MAX; - } - - if (ClampValue(flNewValue)) - { - snprintf(szTempValue, sizeof(szTempValue), "%f", flNewValue); - pszNewValue = szTempValue; - } - - // Redetermine value - m_Value.m_fValue = flNewValue; - m_Value.m_nValue = (int)(m_Value.m_fValue); - } - - if (!(m_ConCommandBase.m_nFlags & FCVAR_NEVER_AS_STRING)) - { - ChangeStringValue(pszNewValue, flOldValue); - } -} - -//----------------------------------------------------------------------------- -// Purpose: sets the ConVar color value. -// Input : clValue - -//----------------------------------------------------------------------------- -void ConVar::SetValue(Color clValue) -{ - std::string svResult = ""; - - for (int i = 0; i < 4; i++) - { - if (!(clValue.GetValue(i) == 0 && svResult.size() == 0)) - { - svResult += std::to_string(clValue.GetValue(i)); - svResult.append(" "); - } - } - - this->m_Value.m_pszString = svResult.c_str(); -} - -//----------------------------------------------------------------------------- -// Purpose: changes the ConVar string value. -// Input : *pszTempVal - flOldValue -//----------------------------------------------------------------------------- -void ConVar::ChangeStringValue(const char* pszTempVal, float flOldValue) -{ - assert(!(m_ConCommandBase.m_nFlags & FCVAR_NEVER_AS_STRING)); - - char* pszOldValue = (char*)_malloca(m_Value.m_iStringLength); - if (pszOldValue != NULL) - { - memcpy(pszOldValue, m_Value.m_pszString, m_Value.m_iStringLength); - } - - if (pszTempVal) - { - int len = strlen(pszTempVal) + 1; - - if (len > m_Value.m_iStringLength) - { - if (m_Value.m_pszString) - delete[] m_Value.m_pszString; - - m_Value.m_pszString = new char[len]; - m_Value.m_iStringLength = len; - } - - memcpy((char*)m_Value.m_pszString, pszTempVal, len); - } - else - { - m_Value.m_pszString = NULL; - } - - pszOldValue = 0; -} - -//----------------------------------------------------------------------------- -// Purpose: sets the ConVar color value from string. -// Input : *pszValue - -//----------------------------------------------------------------------------- -bool ConVar::SetColorFromString(const char* pszValue) -{ - bool bColor = false; - - // Try pulling RGBA color values out of the string. - int nRGBA[4] {}; - int nParamsRead = sscanf_s(pszValue, "%i %i %i %i", &(nRGBA[0]), &(nRGBA[1]), &(nRGBA[2]), &(nRGBA[3])); - - if (nParamsRead >= 3) - { - // This is probably a color! - if (nParamsRead == 3) - { - // Assume they wanted full alpha. - nRGBA[3] = 255; - } - - if (nRGBA[0] >= 0 && nRGBA[0] <= 255 && nRGBA[1] >= 0 && nRGBA[1] <= 255 && nRGBA[2] >= 0 && nRGBA[2] <= 255 && nRGBA[3] >= 0 && - nRGBA[3] <= 255) - { - // printf("*** WOW! Found a color!! ***\n"); - - // This is definitely a color! - bColor = true; - - // Stuff all the values into each byte of our int. - unsigned char* pColorElement = ((unsigned char*)&m_Value.m_nValue); - pColorElement[0] = nRGBA[0]; - pColorElement[1] = nRGBA[1]; - pColorElement[2] = nRGBA[2]; - pColorElement[3] = nRGBA[3]; - - // Copy that value into our float. - m_Value.m_fValue = (float)(m_Value.m_nValue); - } - } - - return bColor; -} - -//----------------------------------------------------------------------------- -// Purpose: Checks if ConVar is registered. -// Output : bool -//----------------------------------------------------------------------------- -bool ConVar::IsRegistered(void) const -{ - return m_ConCommandBase.m_bRegistered; -} - -//----------------------------------------------------------------------------- -// Purpose: Returns true if this is a command -// Output : bool -//----------------------------------------------------------------------------- -bool ConVar::IsCommand(void) const -{ - return false; -} - -//----------------------------------------------------------------------------- -// Purpose: Test each ConVar query before setting the value. -// Input : nFlags -// Output : False if change is permitted, true if not. -//----------------------------------------------------------------------------- -bool ConVar::IsFlagSet(int nFlags) const -{ - return m_ConCommandBase.m_nFlags & nFlags; -} - -//----------------------------------------------------------------------------- -// Purpose: Check whether to clamp and then perform clamp. -// Input : flValue - -// Output : Returns true if value changed. -//----------------------------------------------------------------------------- -bool ConVar::ClampValue(float& flValue) -{ - if (m_bHasMin && (flValue < m_fMinVal)) - { - flValue = m_fMinVal; - return true; - } - - if (m_bHasMax && (flValue > m_fMaxVal)) - { - flValue = m_fMaxVal; - return true; - } - - return false; -} diff --git a/NorthstarDLL/convar.h b/NorthstarDLL/convar.h deleted file mode 100644 index 176d0d72..00000000 --- a/NorthstarDLL/convar.h +++ /dev/null @@ -1,192 +0,0 @@ -#pragma once -#include "sourceinterface.h" -#include "color.h" -#include "cvar.h" -#include "concommand.h" - -// taken directly from iconvar.h - -// The default, no flags at all -#define FCVAR_NONE 0 - -// Command to ConVars and ConCommands -// ConVar Systems -#define FCVAR_UNREGISTERED (1 << 0) // If this is set, don't add to linked list, etc. -#define FCVAR_DEVELOPMENTONLY (1 << 1) // Hidden in released products. Flag is removed automatically if ALLOW_DEVELOPMENT_CVARS is defined. -#define FCVAR_GAMEDLL (1 << 2) // defined by the game DLL -#define FCVAR_CLIENTDLL (1 << 3) // defined by the client DLL -#define FCVAR_HIDDEN (1 << 4) // Hidden. Doesn't appear in find or auto complete. Like DEVELOPMENTONLY, but can't be compiled out. - -// ConVar only -#define FCVAR_PROTECTED \ - (1 << 5) // It's a server cvar, but we don't send the data since it's a password, etc. Sends 1 if it's not bland/zero, 0 otherwise as - // value. -#define FCVAR_SPONLY (1 << 6) // This cvar cannot be changed by clients connected to a multiplayer server. -#define FCVAR_ARCHIVE (1 << 7) // set to cause it to be saved to vars.rc -#define FCVAR_NOTIFY (1 << 8) // notifies players when changed -#define FCVAR_USERINFO (1 << 9) // changes the client's info string - -#define FCVAR_PRINTABLEONLY (1 << 10) // This cvar's string cannot contain unprintable characters ( e.g., used for player name etc ). -#define FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS \ - (1 << 10) // When on concommands this allows remote clients to execute this cmd on the server. - // We are changing the default behavior of concommands to disallow execution by remote clients without - // this flag due to the number existing concommands that can lag or crash the server when clients abuse them. - -#define FCVAR_UNLOGGED (1 << 11) // If this is a FCVAR_SERVER, don't log changes to the log file / console if we are creating a log -#define FCVAR_NEVER_AS_STRING (1 << 12) // never try to print that cvar - -// It's a ConVar that's shared between the client and the server. -// At signon, the values of all such ConVars are sent from the server to the client (skipped for local client, of course ) -// If a change is requested it must come from the console (i.e., no remote client changes) -// If a value is changed while a server is active, it's replicated to all connected clients -#define FCVAR_REPLICATED (1 << 13) // server setting enforced on clients, TODO rename to FCAR_SERVER at some time -#define FCVAR_CHEAT (1 << 14) // Only useable in singleplayer / debug / multiplayer & sv_cheats -#define FCVAR_SS (1 << 15) // causes varnameN where N == 2 through max splitscreen slots for mod to be autogenerated -#define FCVAR_DEMO (1 << 16) // record this cvar when starting a demo file -#define FCVAR_DONTRECORD (1 << 17) // don't record these command in demofiles -#define FCVAR_SS_ADDED (1 << 18) // This is one of the "added" FCVAR_SS variables for the splitscreen players -#define FCVAR_RELEASE (1 << 19) // Cvars tagged with this are the only cvars avaliable to customers -#define FCVAR_RELOAD_MATERIALS (1 << 20) // If this cvar changes, it forces a material reload -#define FCVAR_RELOAD_TEXTURES (1 << 21) // If this cvar changes, if forces a texture reload - -#define FCVAR_NOT_CONNECTED (1 << 22) // cvar cannot be changed by a client that is connected to a server -#define FCVAR_MATERIAL_SYSTEM_THREAD (1 << 23) // Indicates this cvar is read from the material system thread -#define FCVAR_ARCHIVE_PLAYERPROFILE (1 << 24) // respawn-defined flag, same as FCVAR_ARCHIVE but writes to profile.cfg - -#define FCVAR_SERVER_CAN_EXECUTE \ - (1 << 28) // the server is allowed to execute this command on clients via - // 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 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 -//----------------------------------------------------------------------------- -class ConCommandBase; -class ConCommand; -class ConVar; - -typedef void (*FnChangeCallback_t)(ConVar* var, const char* pOldValue, float flOldValue); - -//----------------------------------------------------------------------------- -// Purpose: A console variable -//----------------------------------------------------------------------------- -class ConVar -{ - public: - ConVar(void) {}; - ConVar(const char* pszName, const char* pszDefaultValue, int nFlags, const char* pszHelpString); - ConVar( - const char* pszName, - const char* pszDefaultValue, - int nFlags, - const char* pszHelpString, - bool bMin, - float fMin, - bool bMax, - float fMax, - FnChangeCallback_t pCallback); - ~ConVar(void); - - const char* GetBaseName(void) const; - const char* GetHelpText(void) const; - - void AddFlags(int nFlags); - void RemoveFlags(int nFlags); - - bool GetBool(void) const; - float GetFloat(void) const; - int GetInt(void) const; - Color GetColor(void) const; - const char* GetString(void) const; - - bool GetMin(float& flMinValue) const; - bool GetMax(float& flMaxValue) const; - float GetMinValue(void) const; - float GetMaxValue(void) const; - - bool HasMin(void) const; - bool HasMax(void) const; - - void SetValue(int nValue); - void SetValue(float flValue); - void SetValue(const char* pszValue); - void SetValue(Color clValue); - - void ChangeStringValue(const char* pszTempValue, float flOldValue); - bool SetColorFromString(const char* pszValue); - bool ClampValue(float& value); - - bool IsRegistered(void) const; - bool IsCommand(void) const; - bool IsFlagSet(int nFlags) const; - - struct CVValue_t - { - const char* m_pszString; - int64_t m_iStringLength; - float m_fValue; - int m_nValue; - }; - - ConCommandBase m_ConCommandBase {}; // 0x0000 - const char* m_pszDefaultValue {}; // 0x0040 - CVValue_t m_Value {}; // 0x0048 - bool m_bHasMin {}; // 0x005C - float m_fMinVal {}; // 0x0060 - bool m_bHasMax {}; // 0x0064 - float m_fMaxVal {}; // 0x0068 - void* m_pMalloc {}; // 0x0070 - char m_pPad80[10] {}; // 0x0080 -}; // Size: 0x0080 diff --git a/NorthstarDLL/core/convar/concommand.cpp b/NorthstarDLL/core/convar/concommand.cpp new file mode 100644 index 00000000..02c9e50f --- /dev/null +++ b/NorthstarDLL/core/convar/concommand.cpp @@ -0,0 +1,151 @@ +#include "pch.h" +#include "concommand.h" +#include "shared/misccommands.h" + +#include + +//----------------------------------------------------------------------------- +// Purpose: Returns true if this is a command +// Output : bool +//----------------------------------------------------------------------------- +bool ConCommand::IsCommand(void) const +{ + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns true if this is a command +// Output : bool +//----------------------------------------------------------------------------- +bool ConCommandBase::IsCommand(void) const +{ + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Has this cvar been registered +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool ConCommandBase::IsRegistered(void) const +{ + return m_bRegistered; +} + +//----------------------------------------------------------------------------- +// Purpose: Test each ConCommand query before execution. +// Input : *pCommandBase - nFlags +// Output : False if execution is permitted, true if not. +//----------------------------------------------------------------------------- +bool ConCommandBase::IsFlagSet(int nFlags) const +{ + return m_nFlags & nFlags; +} + +//----------------------------------------------------------------------------- +// Purpose: Checks if ConCommand has requested flags. +// Input : nFlags - +// Output : True if ConCommand has nFlags. +//----------------------------------------------------------------------------- +bool ConCommandBase::HasFlags(int nFlags) +{ + return m_nFlags & nFlags; +} + +//----------------------------------------------------------------------------- +// Purpose: Add's flags to ConCommand. +// Input : nFlags - +//----------------------------------------------------------------------------- +void ConCommandBase::AddFlags(int nFlags) +{ + m_nFlags |= nFlags; +} + +//----------------------------------------------------------------------------- +// Purpose: Removes flags from ConCommand. +// Input : nFlags - +//----------------------------------------------------------------------------- +void ConCommandBase::RemoveFlags(int nFlags) +{ + m_nFlags &= ~nFlags; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns current flags. +// Output : int +//----------------------------------------------------------------------------- +int ConCommandBase::GetFlags(void) const +{ + return m_nFlags; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : const ConCommandBase +//----------------------------------------------------------------------------- +ConCommandBase* ConCommandBase::GetNext(void) const +{ + return m_pNext; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the ConCommandBase help text. +// Output : const char* +//----------------------------------------------------------------------------- +const char* ConCommandBase::GetHelpText(void) const +{ + return m_pszHelpString; +} + +//----------------------------------------------------------------------------- +// Purpose: Copies string using local new/delete operators +// Input : *szFrom - +// Output : char +//----------------------------------------------------------------------------- +char* ConCommandBase::CopyString(const char* szFrom) const +{ + size_t nLen; + char* szTo; + + nLen = strlen(szFrom); + if (nLen <= 0) + { + szTo = new char[1]; + szTo[0] = 0; + } + else + { + szTo = new char[nLen + 1]; + memmove(szTo, szFrom, nLen + 1); + } + 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(); + AddMiscConCommands(); +} diff --git a/NorthstarDLL/core/convar/concommand.h b/NorthstarDLL/core/convar/concommand.h new file mode 100644 index 00000000..89363bc7 --- /dev/null +++ b/NorthstarDLL/core/convar/concommand.h @@ -0,0 +1,140 @@ +#pragma once + +// From Source SDK +class ConCommandBase; +class IConCommandBaseAccessor +{ + public: + // Flags is a combination of FCVAR flags in cvar.h. + // hOut is filled in with a handle to the variable. + virtual bool RegisterConCommandBase(ConCommandBase* pVar) = 0; +}; + +class CCommand +{ + public: + CCommand() = delete; + + int64_t ArgC() const; + const char** ArgV() const; + const char* ArgS() const; // All args that occur after the 0th arg, in string form + const char* GetCommandString() const; // The entire command in string form, including the 0th arg + const char* operator[](int nIndex) const; // Gets at arguments + const char* Arg(int nIndex) const; // Gets at arguments + + static int MaxCommandLength(); + + private: + enum + { + COMMAND_MAX_ARGC = 64, + COMMAND_MAX_LENGTH = 512, + }; + + int64_t m_nArgc; + int64_t m_nArgv0Size; + char m_pArgSBuffer[COMMAND_MAX_LENGTH]; + char m_pArgvBuffer[COMMAND_MAX_LENGTH]; + const char* m_ppArgv[COMMAND_MAX_ARGC]; +}; + +inline int CCommand::MaxCommandLength() +{ + return COMMAND_MAX_LENGTH - 1; +} +inline int64_t CCommand::ArgC() const +{ + return m_nArgc; +} +inline const char** CCommand::ArgV() const +{ + return m_nArgc ? (const char**)m_ppArgv : NULL; +} +inline const char* CCommand::ArgS() const +{ + return m_nArgv0Size ? &m_pArgSBuffer[m_nArgv0Size] : ""; +} +inline const char* CCommand::GetCommandString() const +{ + return m_nArgc ? m_pArgSBuffer : ""; +} +inline const char* CCommand::Arg(int nIndex) const +{ + // FIXME: Many command handlers appear to not be particularly careful + // about checking for valid argc range. For now, we're going to + // do the extra check and return an empty string if it's out of range + if (nIndex < 0 || nIndex >= m_nArgc) + return ""; + return m_ppArgv[nIndex]; +} +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 +{ + public: + bool HasFlags(int nFlags); + void AddFlags(int nFlags); + void RemoveFlags(int nFlags); + + bool IsCommand(void) const; + bool IsRegistered(void) const; + bool IsFlagSet(int nFlags) const; + static bool IsFlagSet(ConCommandBase* pCommandBase, int nFlags); // For hooking to engine's implementation. + + int GetFlags(void) const; + ConCommandBase* GetNext(void) const; + const char* GetHelpText(void) const; + + char* CopyString(const char* szFrom) const; + + void* m_pConCommandBaseVTable; // 0x0000 + ConCommandBase* m_pNext; // 0x0008 + bool m_bRegistered; // 0x0010 + char pad_0011[7]; // 0x0011 <- 3 bytes padding + unk int32. + const char* m_pszName; // 0x0018 + const char* m_pszHelpString; // 0x0020 + int m_nFlags; // 0x0028 + ConCommandBase* s_pConCommandBases; // 0x002C + IConCommandBaseAccessor* s_pAccessor; // 0x0034 +}; // Size: 0x0040 + +// taken from ttf2sdk +class ConCommand : public ConCommandBase +{ + friend class CCVar; + + public: + ConCommand(void) {}; // !TODO: Rebuild engine constructor in SDK instead. + ConCommand(const char* szName, const char* szHelpString, int nFlags, void* pCallback, void* pCommandCompletionCallback); + void Init(void); + bool IsCommand(void) const; + + 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 + int unk1; // 0x005C +}; // Size: 0x0060 + +void RegisterConCommand(const char* name, void (*callback)(const CCommand&), const char* helpString, int flags); +void RegisterConCommand( + const char* name, void (*callback)(const CCommand&), const char* helpString, int flags, FnCommandCompletionCallback completionCallback); diff --git a/NorthstarDLL/core/convar/convar.cpp b/NorthstarDLL/core/convar/convar.cpp new file mode 100644 index 00000000..eaa988dc --- /dev/null +++ b/NorthstarDLL/core/convar/convar.cpp @@ -0,0 +1,490 @@ +#include "pch.h" +#include "bits.h" +#include "cvar.h" +#include "convar.h" +#include "shared/sourceinterface.h" + +#include + +typedef void (*ConVarRegisterType)( + ConVar* pConVar, + const char* pszName, + const char* pszDefaultValue, + int nFlags, + const char* pszHelpString, + bool bMin, + float fMin, + bool bMax, + float fMax, + void* pCallback); +ConVarRegisterType conVarRegister; + +typedef void (*ConVarMallocType)(void* pConVarMaloc, int a2, int a3); +ConVarMallocType conVarMalloc; + +void* g_pConVar_Vtable = nullptr; +void* g_pIConVar_Vtable = nullptr; + +//----------------------------------------------------------------------------- +// Purpose: ConVar interface initialization +//----------------------------------------------------------------------------- +ON_DLL_LOAD("engine.dll", ConVar, (CModule module)) +{ + conVarMalloc = module.Offset(0x415C20).As(); + conVarRegister = module.Offset(0x417230).As(); + + g_pConVar_Vtable = module.Offset(0x67FD28); + g_pIConVar_Vtable = module.Offset(0x67FDC8); + + R2::g_pCVarInterface = new SourceInterface("vstdlib.dll", "VEngineCvar007"); + R2::g_pCVar = *R2::g_pCVarInterface; +} + +//----------------------------------------------------------------------------- +// Purpose: constructor +//----------------------------------------------------------------------------- +ConVar::ConVar(const char* pszName, const char* pszDefaultValue, int nFlags, const char* pszHelpString) +{ + spdlog::info("Registering Convar {}", pszName); + + this->m_ConCommandBase.m_pConCommandBaseVTable = g_pConVar_Vtable; + this->m_ConCommandBase.s_pConCommandBases = (ConCommandBase*)g_pIConVar_Vtable; + + conVarMalloc(&this->m_pMalloc, 0, 0); // Allocate new memory for ConVar. + conVarRegister(this, pszName, pszDefaultValue, nFlags, pszHelpString, 0, 0, 0, 0, 0); +} + +//----------------------------------------------------------------------------- +// Purpose: constructor +//----------------------------------------------------------------------------- +ConVar::ConVar( + const char* pszName, + const char* pszDefaultValue, + int nFlags, + const char* pszHelpString, + bool bMin, + float fMin, + bool bMax, + float fMax, + FnChangeCallback_t pCallback) +{ + spdlog::info("Registering Convar {}", pszName); + + this->m_ConCommandBase.m_pConCommandBaseVTable = g_pConVar_Vtable; + this->m_ConCommandBase.s_pConCommandBases = (ConCommandBase*)g_pIConVar_Vtable; + + conVarMalloc(&this->m_pMalloc, 0, 0); // Allocate new memory for ConVar. + conVarRegister(this, pszName, pszDefaultValue, nFlags, pszHelpString, bMin, fMin, bMax, fMax, pCallback); +} + +//----------------------------------------------------------------------------- +// Purpose: destructor +//----------------------------------------------------------------------------- +ConVar::~ConVar(void) +{ + if (m_Value.m_pszString) + delete[] m_Value.m_pszString; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the base ConVar name. +// Output : const char* +//----------------------------------------------------------------------------- +const char* ConVar::GetBaseName(void) const +{ + return m_ConCommandBase.m_pszName; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns the ConVar help text. +// Output : const char* +//----------------------------------------------------------------------------- +const char* ConVar::GetHelpText(void) const +{ + return m_ConCommandBase.m_pszHelpString; +} + +//----------------------------------------------------------------------------- +// Purpose: Add's flags to ConVar. +// Input : nFlags - +//----------------------------------------------------------------------------- +void ConVar::AddFlags(int nFlags) +{ + m_ConCommandBase.m_nFlags |= nFlags; +} + +//----------------------------------------------------------------------------- +// Purpose: Removes flags from ConVar. +// Input : nFlags - +//----------------------------------------------------------------------------- +void ConVar::RemoveFlags(int nFlags) +{ + m_ConCommandBase.m_nFlags &= ~nFlags; +} + +//----------------------------------------------------------------------------- +// Purpose: Return ConVar value as a boolean. +// Output : bool +//----------------------------------------------------------------------------- +bool ConVar::GetBool(void) const +{ + return !!GetInt(); +} + +//----------------------------------------------------------------------------- +// Purpose: Return ConVar value as a float. +// Output : float +//----------------------------------------------------------------------------- +float ConVar::GetFloat(void) const +{ + return m_Value.m_fValue; +} + +//----------------------------------------------------------------------------- +// Purpose: Return ConVar value as an integer. +// Output : int +//----------------------------------------------------------------------------- +int ConVar::GetInt(void) const +{ + return m_Value.m_nValue; +} + +//----------------------------------------------------------------------------- +// Purpose: Return ConVar value as a color. +// Output : Color +//----------------------------------------------------------------------------- +Color ConVar::GetColor(void) const +{ + unsigned char* pColorElement = ((unsigned char*)&m_Value.m_nValue); + return Color(pColorElement[0], pColorElement[1], pColorElement[2], pColorElement[3]); +} + +//----------------------------------------------------------------------------- +// Purpose: Return ConVar value as a string. +// Output : const char * +//----------------------------------------------------------------------------- +const char* ConVar::GetString(void) const +{ + if (m_ConCommandBase.m_nFlags & FCVAR_NEVER_AS_STRING) + { + return "FCVAR_NEVER_AS_STRING"; + } + + char const* str = m_Value.m_pszString; + return str ? str : ""; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : flMinVal - +// Output : true if there is a min set. +//----------------------------------------------------------------------------- +bool ConVar::GetMin(float& flMinVal) const +{ + flMinVal = m_fMinVal; + return m_bHasMin; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : flMaxVal - +// Output : true if there is a max set. +//----------------------------------------------------------------------------- +bool ConVar::GetMax(float& flMaxVal) const +{ + flMaxVal = m_fMaxVal; + return m_bHasMax; +} + +//----------------------------------------------------------------------------- +// Purpose: returns the min value. +// Output : float +//----------------------------------------------------------------------------- +float ConVar::GetMinValue(void) const +{ + return m_fMinVal; +} + +//----------------------------------------------------------------------------- +// Purpose: returns the max value. +// Output : float +//----------------------------------------------------------------------------- +float ConVar::GetMaxValue(void) const +{ + return m_fMaxVal; + ; +} + +//----------------------------------------------------------------------------- +// Purpose: checks if ConVar has min value. +// Output : bool +//----------------------------------------------------------------------------- +bool ConVar::HasMin(void) const +{ + return m_bHasMin; +} + +//----------------------------------------------------------------------------- +// Purpose: checks if ConVar has max value. +// Output : bool +//----------------------------------------------------------------------------- +bool ConVar::HasMax(void) const +{ + return m_bHasMax; +} + +//----------------------------------------------------------------------------- +// Purpose: sets the ConVar int value. +// Input : nValue - +//----------------------------------------------------------------------------- +void ConVar::SetValue(int nValue) +{ + if (nValue == m_Value.m_nValue) + { + return; + } + + float flValue = (float)nValue; + + // Check bounds. + if (ClampValue(flValue)) + { + nValue = (int)(flValue); + } + + // Redetermine value. + float flOldValue = m_Value.m_fValue; + m_Value.m_fValue = flValue; + m_Value.m_nValue = nValue; + + if (!(m_ConCommandBase.m_nFlags & FCVAR_NEVER_AS_STRING)) + { + char szTempValue[32]; + snprintf(szTempValue, sizeof(szTempValue), "%d", m_Value.m_nValue); + ChangeStringValue(szTempValue, flOldValue); + } +} + +//----------------------------------------------------------------------------- +// Purpose: sets the ConVar float value. +// Input : flValue - +//----------------------------------------------------------------------------- +void ConVar::SetValue(float flValue) +{ + if (flValue == m_Value.m_fValue) + { + return; + } + + // Check bounds. + ClampValue(flValue); + + // Redetermine value. + float flOldValue = m_Value.m_fValue; + m_Value.m_fValue = flValue; + m_Value.m_nValue = (int)m_Value.m_fValue; + + if (!(m_ConCommandBase.m_nFlags & FCVAR_NEVER_AS_STRING)) + { + char szTempValue[32]; + snprintf(szTempValue, sizeof(szTempValue), "%f", m_Value.m_fValue); + ChangeStringValue(szTempValue, flOldValue); + } +} + +//----------------------------------------------------------------------------- +// Purpose: sets the ConVar string value. +// Input : *szValue - +//----------------------------------------------------------------------------- +void ConVar::SetValue(const char* pszValue) +{ + if (strcmp(this->m_Value.m_pszString, pszValue) == 0) + return; + + char szTempValue[32] {}; + const char* pszNewValue {}; + + float flOldValue = m_Value.m_fValue; + pszNewValue = (char*)pszValue; + if (!pszNewValue) + { + pszNewValue = ""; + } + + if (!SetColorFromString(pszValue)) + { + // Not a color, do the standard thing + float flNewValue = (float)atof(pszValue); + if (!isfinite(flNewValue)) + { + spdlog::warn("Warning: ConVar '{}' = '{}' is infinite, clamping value.\n", GetBaseName(), pszValue); + flNewValue = FLT_MAX; + } + + if (ClampValue(flNewValue)) + { + snprintf(szTempValue, sizeof(szTempValue), "%f", flNewValue); + pszNewValue = szTempValue; + } + + // Redetermine value + m_Value.m_fValue = flNewValue; + m_Value.m_nValue = (int)(m_Value.m_fValue); + } + + if (!(m_ConCommandBase.m_nFlags & FCVAR_NEVER_AS_STRING)) + { + ChangeStringValue(pszNewValue, flOldValue); + } +} + +//----------------------------------------------------------------------------- +// Purpose: sets the ConVar color value. +// Input : clValue - +//----------------------------------------------------------------------------- +void ConVar::SetValue(Color clValue) +{ + std::string svResult = ""; + + for (int i = 0; i < 4; i++) + { + if (!(clValue.GetValue(i) == 0 && svResult.size() == 0)) + { + svResult += std::to_string(clValue.GetValue(i)); + svResult.append(" "); + } + } + + this->m_Value.m_pszString = svResult.c_str(); +} + +//----------------------------------------------------------------------------- +// Purpose: changes the ConVar string value. +// Input : *pszTempVal - flOldValue +//----------------------------------------------------------------------------- +void ConVar::ChangeStringValue(const char* pszTempVal, float flOldValue) +{ + assert(!(m_ConCommandBase.m_nFlags & FCVAR_NEVER_AS_STRING)); + + char* pszOldValue = (char*)_malloca(m_Value.m_iStringLength); + if (pszOldValue != NULL) + { + memcpy(pszOldValue, m_Value.m_pszString, m_Value.m_iStringLength); + } + + if (pszTempVal) + { + int len = strlen(pszTempVal) + 1; + + if (len > m_Value.m_iStringLength) + { + if (m_Value.m_pszString) + delete[] m_Value.m_pszString; + + m_Value.m_pszString = new char[len]; + m_Value.m_iStringLength = len; + } + + memcpy((char*)m_Value.m_pszString, pszTempVal, len); + } + else + { + m_Value.m_pszString = NULL; + } + + pszOldValue = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: sets the ConVar color value from string. +// Input : *pszValue - +//----------------------------------------------------------------------------- +bool ConVar::SetColorFromString(const char* pszValue) +{ + bool bColor = false; + + // Try pulling RGBA color values out of the string. + int nRGBA[4] {}; + int nParamsRead = sscanf_s(pszValue, "%i %i %i %i", &(nRGBA[0]), &(nRGBA[1]), &(nRGBA[2]), &(nRGBA[3])); + + if (nParamsRead >= 3) + { + // This is probably a color! + if (nParamsRead == 3) + { + // Assume they wanted full alpha. + nRGBA[3] = 255; + } + + if (nRGBA[0] >= 0 && nRGBA[0] <= 255 && nRGBA[1] >= 0 && nRGBA[1] <= 255 && nRGBA[2] >= 0 && nRGBA[2] <= 255 && nRGBA[3] >= 0 && + nRGBA[3] <= 255) + { + // printf("*** WOW! Found a color!! ***\n"); + + // This is definitely a color! + bColor = true; + + // Stuff all the values into each byte of our int. + unsigned char* pColorElement = ((unsigned char*)&m_Value.m_nValue); + pColorElement[0] = nRGBA[0]; + pColorElement[1] = nRGBA[1]; + pColorElement[2] = nRGBA[2]; + pColorElement[3] = nRGBA[3]; + + // Copy that value into our float. + m_Value.m_fValue = (float)(m_Value.m_nValue); + } + } + + return bColor; +} + +//----------------------------------------------------------------------------- +// Purpose: Checks if ConVar is registered. +// Output : bool +//----------------------------------------------------------------------------- +bool ConVar::IsRegistered(void) const +{ + return m_ConCommandBase.m_bRegistered; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns true if this is a command +// Output : bool +//----------------------------------------------------------------------------- +bool ConVar::IsCommand(void) const +{ + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Test each ConVar query before setting the value. +// Input : nFlags +// Output : False if change is permitted, true if not. +//----------------------------------------------------------------------------- +bool ConVar::IsFlagSet(int nFlags) const +{ + return m_ConCommandBase.m_nFlags & nFlags; +} + +//----------------------------------------------------------------------------- +// Purpose: Check whether to clamp and then perform clamp. +// Input : flValue - +// Output : Returns true if value changed. +//----------------------------------------------------------------------------- +bool ConVar::ClampValue(float& flValue) +{ + if (m_bHasMin && (flValue < m_fMinVal)) + { + flValue = m_fMinVal; + return true; + } + + if (m_bHasMax && (flValue > m_fMaxVal)) + { + flValue = m_fMaxVal; + return true; + } + + return false; +} diff --git a/NorthstarDLL/core/convar/convar.h b/NorthstarDLL/core/convar/convar.h new file mode 100644 index 00000000..86ec64e6 --- /dev/null +++ b/NorthstarDLL/core/convar/convar.h @@ -0,0 +1,192 @@ +#pragma once +#include "shared/sourceinterface.h" +#include "core/math/color.h" +#include "cvar.h" +#include "concommand.h" + +// taken directly from iconvar.h + +// The default, no flags at all +#define FCVAR_NONE 0 + +// Command to ConVars and ConCommands +// ConVar Systems +#define FCVAR_UNREGISTERED (1 << 0) // If this is set, don't add to linked list, etc. +#define FCVAR_DEVELOPMENTONLY (1 << 1) // Hidden in released products. Flag is removed automatically if ALLOW_DEVELOPMENT_CVARS is defined. +#define FCVAR_GAMEDLL (1 << 2) // defined by the game DLL +#define FCVAR_CLIENTDLL (1 << 3) // defined by the client DLL +#define FCVAR_HIDDEN (1 << 4) // Hidden. Doesn't appear in find or auto complete. Like DEVELOPMENTONLY, but can't be compiled out. + +// ConVar only +#define FCVAR_PROTECTED \ + (1 << 5) // It's a server cvar, but we don't send the data since it's a password, etc. Sends 1 if it's not bland/zero, 0 otherwise as + // value. +#define FCVAR_SPONLY (1 << 6) // This cvar cannot be changed by clients connected to a multiplayer server. +#define FCVAR_ARCHIVE (1 << 7) // set to cause it to be saved to vars.rc +#define FCVAR_NOTIFY (1 << 8) // notifies players when changed +#define FCVAR_USERINFO (1 << 9) // changes the client's info string + +#define FCVAR_PRINTABLEONLY (1 << 10) // This cvar's string cannot contain unprintable characters ( e.g., used for player name etc ). +#define FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS \ + (1 << 10) // When on concommands this allows remote clients to execute this cmd on the server. + // We are changing the default behavior of concommands to disallow execution by remote clients without + // this flag due to the number existing concommands that can lag or crash the server when clients abuse them. + +#define FCVAR_UNLOGGED (1 << 11) // If this is a FCVAR_SERVER, don't log changes to the log file / console if we are creating a log +#define FCVAR_NEVER_AS_STRING (1 << 12) // never try to print that cvar + +// It's a ConVar that's shared between the client and the server. +// At signon, the values of all such ConVars are sent from the server to the client (skipped for local client, of course ) +// If a change is requested it must come from the console (i.e., no remote client changes) +// If a value is changed while a server is active, it's replicated to all connected clients +#define FCVAR_REPLICATED (1 << 13) // server setting enforced on clients, TODO rename to FCAR_SERVER at some time +#define FCVAR_CHEAT (1 << 14) // Only useable in singleplayer / debug / multiplayer & sv_cheats +#define FCVAR_SS (1 << 15) // causes varnameN where N == 2 through max splitscreen slots for mod to be autogenerated +#define FCVAR_DEMO (1 << 16) // record this cvar when starting a demo file +#define FCVAR_DONTRECORD (1 << 17) // don't record these command in demofiles +#define FCVAR_SS_ADDED (1 << 18) // This is one of the "added" FCVAR_SS variables for the splitscreen players +#define FCVAR_RELEASE (1 << 19) // Cvars tagged with this are the only cvars avaliable to customers +#define FCVAR_RELOAD_MATERIALS (1 << 20) // If this cvar changes, it forces a material reload +#define FCVAR_RELOAD_TEXTURES (1 << 21) // If this cvar changes, if forces a texture reload + +#define FCVAR_NOT_CONNECTED (1 << 22) // cvar cannot be changed by a client that is connected to a server +#define FCVAR_MATERIAL_SYSTEM_THREAD (1 << 23) // Indicates this cvar is read from the material system thread +#define FCVAR_ARCHIVE_PLAYERPROFILE (1 << 24) // respawn-defined flag, same as FCVAR_ARCHIVE but writes to profile.cfg + +#define FCVAR_SERVER_CAN_EXECUTE \ + (1 << 28) // the server is allowed to execute this command on clients via + // 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 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 +//----------------------------------------------------------------------------- +class ConCommandBase; +class ConCommand; +class ConVar; + +typedef void (*FnChangeCallback_t)(ConVar* var, const char* pOldValue, float flOldValue); + +//----------------------------------------------------------------------------- +// Purpose: A console variable +//----------------------------------------------------------------------------- +class ConVar +{ + public: + ConVar(void) {}; + ConVar(const char* pszName, const char* pszDefaultValue, int nFlags, const char* pszHelpString); + ConVar( + const char* pszName, + const char* pszDefaultValue, + int nFlags, + const char* pszHelpString, + bool bMin, + float fMin, + bool bMax, + float fMax, + FnChangeCallback_t pCallback); + ~ConVar(void); + + const char* GetBaseName(void) const; + const char* GetHelpText(void) const; + + void AddFlags(int nFlags); + void RemoveFlags(int nFlags); + + bool GetBool(void) const; + float GetFloat(void) const; + int GetInt(void) const; + Color GetColor(void) const; + const char* GetString(void) const; + + bool GetMin(float& flMinValue) const; + bool GetMax(float& flMaxValue) const; + float GetMinValue(void) const; + float GetMaxValue(void) const; + + bool HasMin(void) const; + bool HasMax(void) const; + + void SetValue(int nValue); + void SetValue(float flValue); + void SetValue(const char* pszValue); + void SetValue(Color clValue); + + void ChangeStringValue(const char* pszTempValue, float flOldValue); + bool SetColorFromString(const char* pszValue); + bool ClampValue(float& value); + + bool IsRegistered(void) const; + bool IsCommand(void) const; + bool IsFlagSet(int nFlags) const; + + struct CVValue_t + { + const char* m_pszString; + int64_t m_iStringLength; + float m_fValue; + int m_nValue; + }; + + ConCommandBase m_ConCommandBase {}; // 0x0000 + const char* m_pszDefaultValue {}; // 0x0040 + CVValue_t m_Value {}; // 0x0048 + bool m_bHasMin {}; // 0x005C + float m_fMinVal {}; // 0x0060 + bool m_bHasMax {}; // 0x0064 + float m_fMaxVal {}; // 0x0068 + void* m_pMalloc {}; // 0x0070 + char m_pPad80[10] {}; // 0x0080 +}; // Size: 0x0080 diff --git a/NorthstarDLL/core/convar/cvar.cpp b/NorthstarDLL/core/convar/cvar.cpp new file mode 100644 index 00000000..787790be --- /dev/null +++ b/NorthstarDLL/core/convar/cvar.cpp @@ -0,0 +1,31 @@ +#include "pch.h" +#include "cvar.h" +#include "convar.h" +#include "concommand.h" + +//----------------------------------------------------------------------------- +// Purpose: returns all ConVars +//----------------------------------------------------------------------------- +std::unordered_map CCvar::DumpToMap() +{ + std::stringstream ss; + CCVarIteratorInternal* itint = FactoryInternalIterator(); // Allocate new InternalIterator. + + std::unordered_map allConVars; + + for (itint->SetFirst(); itint->IsValid(); itint->Next()) // Loop through all instances. + { + ConCommandBase* pCommand = itint->Get(); + const char* pszCommandName = pCommand->m_pszName; + allConVars[pszCommandName] = pCommand; + } + + return allConVars; +} + +// use the R2 namespace for game funcs +namespace R2 +{ + SourceInterface* g_pCVarInterface; + CCvar* g_pCVar; +} // namespace R2 diff --git a/NorthstarDLL/core/convar/cvar.h b/NorthstarDLL/core/convar/cvar.h new file mode 100644 index 00000000..e65e5145 --- /dev/null +++ b/NorthstarDLL/core/convar/cvar.h @@ -0,0 +1,43 @@ +#pragma once +#include "convar.h" +#include "pch.h" + +//----------------------------------------------------------------------------- +// Forward declarations +//----------------------------------------------------------------------------- +class ConCommandBase; +class ConCommand; +class ConVar; + +//----------------------------------------------------------------------------- +// Internals for ICVarIterator +//----------------------------------------------------------------------------- +class CCVarIteratorInternal // Fully reversed table, just look at the virtual function table and rename the function. +{ + public: + virtual void SetFirst(void) = 0; // 0 + virtual void Next(void) = 0; // 1 + virtual bool IsValid(void) = 0; // 2 + virtual ConCommandBase* Get(void) = 0; // 3 +}; + +//----------------------------------------------------------------------------- +// Default implementation +//----------------------------------------------------------------------------- +class CCvar +{ + public: + M_VMETHOD(ConCommandBase*, FindCommandBase, 14, (const char* pszCommandName), (this, pszCommandName)); + M_VMETHOD(ConVar*, FindVar, 16, (const char* pszVarName), (this, pszVarName)); + M_VMETHOD(ConCommand*, FindCommand, 18, (const char* pszCommandName), (this, pszCommandName)); + M_VMETHOD(CCVarIteratorInternal*, FactoryInternalIterator, 41, (), (this)); + + std::unordered_map DumpToMap(); +}; + +// use the R2 namespace for game funcs +namespace R2 +{ + extern SourceInterface* g_pCVarInterface; + extern CCvar* g_pCVar; +} // namespace R2 diff --git a/NorthstarDLL/core/filesystem/filesystem.cpp b/NorthstarDLL/core/filesystem/filesystem.cpp new file mode 100644 index 00000000..230c9161 --- /dev/null +++ b/NorthstarDLL/core/filesystem/filesystem.cpp @@ -0,0 +1,186 @@ +#include "pch.h" +#include "filesystem.h" +#include "shared/sourceinterface.h" +#include "mods/modmanager.h" + +#include +#include + +AUTOHOOK_INIT() + +using namespace R2; + +bool bReadingOriginalFile = false; +std::string sCurrentModPath; + +ConVar* Cvar_ns_fs_log_reads; + +// use the R2 namespace for game funcs +namespace R2 +{ + SourceInterface* g_pFilesystem; + + std::string ReadVPKFile(const char* path) + { + // 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); + + 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)); + + (*g_pFilesystem)->m_vtable2->Close(*g_pFilesystem, fileHandle); + + 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 +{ + AddSearchPath(fileSystem, pPath, pathID, addType); + + // 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) +{ + // put our new path to the head if we need to read from a different mod path + // 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->m_ModDirectory) / MOD_OVERRIDE_DIR).string().compare(sCurrentModPath)) + { + NS::log::fs->info("Changing mod search path from {} to {}", sCurrentModPath, mod->m_ModDirectory.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 + AddSearchPath(&*(*g_pFilesystem), fs::absolute(GetCompiledAssetsPath()).string().c_str(), "GAME", PATH_ADD_TO_HEAD); +} + +bool TryReplaceFile(const char* pPath, bool shouldCompile) +{ + if (bReadingOriginalFile) + return false; + + if (shouldCompile) + 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_pModManager->m_ModFiles.find(g_pModManager->NormaliseModFilePath(fs::path(pPath))); + if (file != g_pModManager->m_ModFiles.end()) + { + SetNewModSearchPaths(file->second.m_pOwningMod); + return true; + } + + return false; +} + +// 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 +{ + if (TryReplaceFile(pPath, true)) + return false; + + 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); +} + +// 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 +{ + TryReplaceFile(pPath, true); + return CBaseFileSystem__OpenEx(filesystem, pPath, pOptions, flags, pPathID, ppszResolvedFilename); +} + +HOOK(MountVPKHook, MountVPK, VPKData*, , (IFileSystem * fileSystem, const char* pVpkPath)) +{ + NS::log::fs->info("MountVPK {}", pVpkPath); + VPKData* ret = MountVPK(fileSystem, pVpkPath); + + for (Mod mod : g_pModManager->m_LoadedMods) + { + if (!mod.m_bEnabled) + continue; + + for (ModVPKEntry& vpkEntry : mod.Vpks) + { + // if we're autoloading, just load no matter what + if (!vpkEntry.m_bAutoLoad) + { + // 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(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()); + if (!ret) // this is primarily for map vpks and stuff, so the map's vpk is what gets returned from here + ret = loaded; + } + } + + return ret; +} + +ON_DLL_LOAD("filesystem_stdio.dll", Filesystem, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + R2::g_pFilesystem = new SourceInterface("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); +} diff --git a/NorthstarDLL/core/filesystem/filesystem.h b/NorthstarDLL/core/filesystem/filesystem.h new file mode 100644 index 00000000..8739d342 --- /dev/null +++ b/NorthstarDLL/core/filesystem/filesystem.h @@ -0,0 +1,85 @@ +#pragma once +#include "shared/sourceinterface.h" + +// taken from ttf2sdk +typedef void* FileHandle_t; + +#pragma pack(push, 1) + +// clang-format off +OFFSET_STRUCT(VPKFileEntry) +{ + STRUCT_SIZE(0x44); + FIELDS(0x0, + char* directory; + char* filename; + char* extension; + ) +}; +// clang-format on +#pragma pack(pop) + +#pragma pack(push, 1) +// clang-format off +struct VPKData +{ + STRUCT_SIZE(0x50); + FIELDS(0x0, + char* directory; + char* filename; + char* extension; + ) +}; +// clang-format on +#pragma pack(pop) + +enum SearchPathAdd_t +{ + PATH_ADD_TO_HEAD, // First path searched + PATH_ADD_TO_TAIL, // Last path searched +}; + +class CSearchPath +{ + public: + unsigned char unknown[0x18]; + const char* debugPath; +}; + +class IFileSystem +{ + public: + struct VTable + { + void* unknown[10]; + void (*AddSearchPath)(IFileSystem* fileSystem, const char* pPath, const char* pathID, SearchPathAdd_t addType); + void* unknown2[84]; + bool (*ReadFromCache)(IFileSystem* fileSystem, const char* path, void* result); + void* unknown3[15]; + VPKData* (*MountVPK)(IFileSystem* fileSystem, const char* vpkPath); + }; + + struct VTable2 + { + int (*Read)(IFileSystem::VTable2** fileSystem, void* pOutput, int size, FileHandle_t file); + void* unknown[1]; + 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); + 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); + }; + + VTable* m_vtable; + VTable2* m_vtable2; +}; + +// use the R2 namespace for game funcs +namespace R2 +{ + extern SourceInterface* g_pFilesystem; + + std::string ReadVPKFile(const char* path); + std::string ReadVPKOriginalFile(const char* path); +} // namespace R2 diff --git a/NorthstarDLL/core/filesystem/rpakfilesystem.cpp b/NorthstarDLL/core/filesystem/rpakfilesystem.cpp new file mode 100644 index 00000000..b46218e0 --- /dev/null +++ b/NorthstarDLL/core/filesystem/rpakfilesystem.cpp @@ -0,0 +1,342 @@ +#include "pch.h" +#include "rpakfilesystem.h" +#include "mods/modmanager.h" +#include "dedicated/dedicated.h" +#include "core/tier0.h" + +AUTOHOOK_INIT() + +// there are more i'm just too lazy to add +struct PakLoadFuncs +{ + void* unk0[3]; + int (*LoadPakAsync)(const char* pPath, void* unknownSingleton, int flags, void* callback0, void* callback1); + void* unk1[2]; + void* (*UnloadPak)(int iPakHandle, void* callback); + void* unk2[6]; + void* (*LoadFile)(const char* path); // unsure + void* unk3[10]; + void* (*ReadFileAsync)(const char* pPath, void* a2); +}; + +PakLoadFuncs* g_pakLoadApi; + +PakLoadManager* g_pPakLoadManager; +void** pUnknownPakLoadSingleton; + +int PakLoadManager::LoadPakAsync(const char* pPath, const ePakLoadSource nLoadSource) +{ + int nHandle = g_pakLoadApi->LoadPakAsync(pPath, *pUnknownPakLoadSingleton, 2, nullptr, nullptr); + + // set the load source of the pak we just loaded + if (nHandle != -1) + GetPakInfo(nHandle)->m_nLoadSource = nLoadSource; + + return nHandle; +} + +void PakLoadManager::UnloadPak(const int nPakHandle) +{ + g_pakLoadApi->UnloadPak(nPakHandle, nullptr); +} + +void PakLoadManager::UnloadMapPaks() +{ + for (auto& pair : m_vLoadedPaks) + if (pair.second.m_nLoadSource == ePakLoadSource::MAP) + UnloadPak(pair.first); +} + +LoadedPak* PakLoadManager::TrackLoadedPak(ePakLoadSource nLoadSource, int nPakHandle, size_t nPakNameHash) +{ + LoadedPak pak; + pak.m_nLoadSource = nLoadSource; + pak.m_nPakHandle = nPakHandle; + pak.m_nPakNameHash = nPakNameHash; + + m_vLoadedPaks.insert(std::make_pair(nPakHandle, pak)); + return &m_vLoadedPaks.at(nPakHandle); +} + +void PakLoadManager::RemoveLoadedPak(int nPakHandle) +{ + m_vLoadedPaks.erase(nPakHandle); +} + +LoadedPak* PakLoadManager::GetPakInfo(const int nPakHandle) +{ + return &m_vLoadedPaks.at(nPakHandle); +} + +int PakLoadManager::GetPakHandle(const size_t nPakNameHash) +{ + for (auto& pair : m_vLoadedPaks) + if (pair.second.m_nPakNameHash == nPakNameHash) + return pair.first; + + return -1; +} + +int PakLoadManager::GetPakHandle(const char* pPath) +{ + return GetPakHandle(STR_HASH(pPath)); +} + +void* PakLoadManager::LoadFile(const char* path) +{ + return g_pakLoadApi->LoadFile(path); +} + +void HandlePakAliases(char** map) +{ + // convert the pak being loaded to it's aliased one, e.g. aliasing mp_hub_timeshift => sp_hub_timeshift + for (int64_t i = g_pModManager->m_LoadedMods.size() - 1; i > -1; i--) + { + Mod* mod = &g_pModManager->m_LoadedMods[i]; + if (!mod->m_bEnabled) + continue; + + if (mod->RpakAliases.find(*map) != mod->RpakAliases.end()) + { + *map = &mod->RpakAliases[*map][0]; + return; + } + } +} + +void LoadPreloadPaks() +{ + // note, loading from ./ is necessary otherwise paks will load from gamedir/r2/paks + for (Mod& mod : g_pModManager->m_LoadedMods) + { + if (!mod.m_bEnabled) + continue; + + // need to get a relative path of mod to mod folder + fs::path modPakPath("./" / mod.m_ModDirectory / "paks"); + + for (ModRpakEntry& pak : mod.Rpaks) + if (pak.m_bAutoLoad) + g_pPakLoadManager->LoadPakAsync((modPakPath / pak.m_sPakName).string().c_str(), ePakLoadSource::CONSTANT); + } +} + +void LoadPostloadPaks(const char* pPath) +{ + // note, loading from ./ is necessary otherwise paks will load from gamedir/r2/paks + for (Mod& mod : g_pModManager->m_LoadedMods) + { + if (!mod.m_bEnabled) + continue; + + // need to get a relative path of mod to mod folder + fs::path modPakPath("./" / mod.m_ModDirectory / "paks"); + + for (ModRpakEntry& pak : mod.Rpaks) + if (pak.m_sLoadAfterPak == pPath) + g_pPakLoadManager->LoadPakAsync((modPakPath / pak.m_sPakName).string().c_str(), ePakLoadSource::CONSTANT); + } +} + +void LoadCustomMapPaks(char** pakName, bool* bNeedToFreePakName) +{ + // whether the vanilla game has this rpak + bool bHasOriginalPak = fs::exists(fs::path("r2/paks/Win64/") / *pakName); + + // note, loading from ./ is necessary otherwise paks will load from gamedir/r2/paks + for (Mod& mod : g_pModManager->m_LoadedMods) + { + if (!mod.m_bEnabled) + continue; + + // need to get a relative path of mod to mod folder + fs::path modPakPath("./" / mod.m_ModDirectory / "paks"); + + for (ModRpakEntry& pak : mod.Rpaks) + { + if (!pak.m_bAutoLoad && !pak.m_sPakName.compare(*pakName)) + { + // if the game doesn't have the original pak, let it handle loading this one as if it was the one it was loading originally + if (!bHasOriginalPak) + { + std::string path = (modPakPath / pak.m_sPakName).string(); + *pakName = new char[path.size() + 1]; + strcpy(*pakName, &path[0]); + (*pakName)[path.size()] = '\0'; + + bHasOriginalPak = true; + *bNeedToFreePakName = + true; // we can't free this memory until we're done with the pak, so let whatever's calling this deal with it + } + else + g_pPakLoadManager->LoadPakAsync((modPakPath / pak.m_sPakName).string().c_str(), ePakLoadSource::MAP); + } + } + } +} + +// clang-format off +HOOK(LoadPakAsyncHook, LoadPakAsync, +int, __fastcall, (char* pPath, void* unknownSingleton, int flags, void* pCallback0, void* pCallback1)) +// clang-format on +{ + HandlePakAliases(&pPath); + + // dont load the pak if it's currently loaded already + size_t nPathHash = STR_HASH(pPath); + if (g_pPakLoadManager->GetPakHandle(nPathHash) != -1) + return -1; + + bool bNeedToFreePakName = false; + + static bool bShouldLoadPaks = true; + if (bShouldLoadPaks) + { + // make a copy of the path for comparing to determine whether we should load this pak on dedi, before it could get overwritten by + // LoadCustomMapPaks + std::string originalPath(pPath); + + // disable preloading while we're doing this + bShouldLoadPaks = false; + + LoadPreloadPaks(); + LoadCustomMapPaks(&pPath, &bNeedToFreePakName); + + bShouldLoadPaks = true; + + // do this after custom paks load and in bShouldLoadPaks so we only ever call this on the root pakload call + // todo: could probably add some way to flag custom paks to not be loaded on dedicated servers in rpak.json + + // dedicated only needs common, common_mp, common_sp, and sp_ rpaks + // sp_ rpaks contain tutorial ghost data + // sucks to have to load the entire rpak for that but sp was never meant to be done on dedi + if (IsDedicatedServer() && (Tier0::CommandLine()->CheckParm("-nopakdedi") || + strncmp(&originalPath[0], "common", 6) && strncmp(&originalPath[0], "sp_", 3))) + { + if (bNeedToFreePakName) + delete[] pPath; + + NS::log::rpak->info("Not loading pak {} for dedicated server", originalPath); + return -1; + } + } + + int iPakHandle = LoadPakAsync(pPath, unknownSingleton, flags, pCallback0, pCallback1); + NS::log::rpak->info("LoadPakAsync {} {}", pPath, iPakHandle); + + // trak the pak + g_pPakLoadManager->TrackLoadedPak(ePakLoadSource::UNTRACKED, iPakHandle, nPathHash); + LoadPostloadPaks(pPath); + + if (bNeedToFreePakName) + delete[] pPath; + + return iPakHandle; +} + +// clang-format off +HOOK(UnloadPakHook, UnloadPak, +void*, __fastcall, (int nPakHandle, void* pCallback)) +// clang-format on +{ + // stop tracking the pak + g_pPakLoadManager->RemoveLoadedPak(nPakHandle); + + static bool bShouldUnloadPaks = true; + if (bShouldUnloadPaks) + { + bShouldUnloadPaks = false; + g_pPakLoadManager->UnloadMapPaks(); + bShouldUnloadPaks = true; + } + + NS::log::rpak->info("UnloadPak {}", nPakHandle); + return UnloadPak(nPakHandle, pCallback); +} + +// we hook this exclusively for resolving stbsp paths, but seemingly it's also used for other stuff like vpk, rpak, mprj and starpak loads +// tbh this actually might be for memory mapped files or something, would make sense i think +// clang-format off +HOOK(ReadFileAsyncHook, ReadFileAsync, +void*, __fastcall, (const char* pPath, void* pCallback)) +// clang-format on +{ + fs::path path(pPath); + std::string newPath = ""; + fs::path filename = path.filename(); + + if (path.extension() == ".stbsp") + { + NS::log::rpak->info("LoadStreamBsp: {}", filename.string()); + + // resolve modded stbsp path so we can load mod stbsps + auto modFile = g_pModManager->m_ModFiles.find(g_pModManager->NormaliseModFilePath(fs::path("maps" / filename))); + if (modFile != g_pModManager->m_ModFiles.end()) + { + newPath = (modFile->second.m_pOwningMod->m_ModDirectory / "mod" / modFile->second.m_Path).string(); + pPath = newPath.c_str(); + } + } + else if (path.extension() == ".starpak") + { + // code for this is mostly stolen from above + + // unfortunately I can't find a way to get the rpak that is causing this function call, so I have to + // store them on mod init and then compare the current path with the stored paths + + // game adds r2\ to every path, so assume that a starpak path that begins with r2\paks\ is a vanilla one + // modded starpaks will be in the mod's paks folder (but can be in folders within the paks folder) + + // this might look a bit confusing, but its just an iterator over the various directories in a path. + // path.begin() being the first directory, r2 in this case, which is guaranteed anyway, + // so increment the iterator with ++ to get the first actual directory, * just gets the actual value + // then we compare to "paks" to determine if it's a vanilla rpak or not + if (*++path.begin() != "paks") + { + // remove the r2\ from the start used for path lookups + std::string starpakPath = path.string().substr(3); + // hash the starpakPath to compare with stored entries + size_t hashed = STR_HASH(starpakPath); + + // loop through all loaded mods + for (Mod& mod : g_pModManager->m_LoadedMods) + { + // ignore non-loaded mods + if (!mod.m_bEnabled) + continue; + + // loop through the stored starpak paths + for (size_t hash : mod.StarpakPaths) + { + if (hash == hashed) + { + // construct new path + newPath = (mod.m_ModDirectory / "paks" / starpakPath).string(); + // set path to the new path + pPath = newPath.c_str(); + goto LOG_STARPAK; + } + } + } + } + + LOG_STARPAK: + NS::log::rpak->info("LoadStreamPak: {}", filename.string()); + } + + return ReadFileAsync(pPath, pCallback); +} + +ON_DLL_LOAD("engine.dll", RpakFilesystem, (CModule module)) +{ + AUTOHOOK_DISPATCH(); + + g_pPakLoadManager = new PakLoadManager; + + g_pakLoadApi = module.Offset(0x5BED78).Deref().As(); + pUnknownPakLoadSingleton = module.Offset(0x7C5E20).As(); + + LoadPakAsyncHook.Dispatch(g_pakLoadApi->LoadPakAsync); + UnloadPakHook.Dispatch(g_pakLoadApi->UnloadPak); + ReadFileAsyncHook.Dispatch(g_pakLoadApi->ReadFileAsync); +} diff --git a/NorthstarDLL/core/filesystem/rpakfilesystem.h b/NorthstarDLL/core/filesystem/rpakfilesystem.h new file mode 100644 index 00000000..3f608dba --- /dev/null +++ b/NorthstarDLL/core/filesystem/rpakfilesystem.h @@ -0,0 +1,39 @@ +#pragma once + +enum class ePakLoadSource +{ + UNTRACKED = -1, // not a pak we loaded, we shouldn't touch this one + + CONSTANT, // should be loaded at all times + MAP // loaded from a map, should be unloaded when the map is unloaded +}; + +struct LoadedPak +{ + ePakLoadSource m_nLoadSource; + int m_nPakHandle; + size_t m_nPakNameHash; +}; + +class PakLoadManager +{ + private: + std::map m_vLoadedPaks {}; + std::unordered_map m_HashToPakHandle {}; + + public: + int LoadPakAsync(const char* pPath, const ePakLoadSource nLoadSource); + void UnloadPak(const int nPakHandle); + void UnloadMapPaks(); + void* LoadFile(const char* path); // this is a guess + + LoadedPak* TrackLoadedPak(ePakLoadSource nLoadSource, int nPakHandle, size_t nPakNameHash); + void RemoveLoadedPak(int nPakHandle); + + LoadedPak* GetPakInfo(const int nPakHandle); + + int GetPakHandle(const size_t nPakNameHash); + int GetPakHandle(const char* pPath); +}; + +extern PakLoadManager* g_pPakLoadManager; diff --git a/NorthstarDLL/core/hooks.cpp b/NorthstarDLL/core/hooks.cpp new file mode 100644 index 00000000..79bcbb92 --- /dev/null +++ b/NorthstarDLL/core/hooks.cpp @@ -0,0 +1,426 @@ +#include "pch.h" +#include "dedicated/dedicated.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +AUTOHOOK_INIT() + +// called from the ON_DLL_LOAD macros +__dllLoadCallback::__dllLoadCallback( + eDllLoadCallbackSide side, const std::string dllName, DllLoadCallbackFuncType callback, std::string uniqueStr, std::string reliesOn) +{ + // parse reliesOn array from string + std::vector reliesOnArray; + + 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; + } + } +} + +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 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& GetDllLoadCallbacks() +{ + static std::vector vec = std::vector(); + return vec; +} + +void AddDllLoadCallback(std::string dll, DllLoadCallbackFuncType callback, std::string tag, std::vector 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 reliesOn) +{ + if (!IsDedicatedServer()) + return; + + AddDllLoadCallback(dll, callback, tag, reliesOn); +} + +void AddDllLoadCallbackForClient(std::string dll, DllLoadCallbackFuncType callback, std::string tag, std::vector 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 = _GetCommandLineA(); + bool isDedi = strstr(cmdlineOrg, "-dedicated"); // well, this one has to be a real argument + bool ignoreStartupArgs = strstr(cmdlineOrg, "-nostartupargs"); + + std::string args; + std::ifstream cmdlineArgFile; + + // it looks like CommandLine() prioritizes parameters apprearing first, so we want the real commandline to take priority + // not to mention that cmdlineOrg starts with the EXE path + args.append(cmdlineOrg); + args.append(" "); + + // append those from the file + + if (!ignoreStartupArgs) + { + + cmdlineArgFile = std::ifstream(!isDedi ? "ns_startup_args.txt" : "ns_startup_args_dedi.txt"); + + if (cmdlineArgFile) + { + std::stringstream argBuffer; + argBuffer << cmdlineArgFile.rdbuf(); + cmdlineArgFile.close(); + + // if some other command line option includes "-northstar" in the future then you have to refactor this check to check with + // both either space after or ending with + if (!isDedi && argBuffer.str().find("-northstar") != std::string::npos) + MessageBoxA( + NULL, + "The \"-northstar\" command line option is NOT supposed to go into ns_startup_args.txt file!\n\nThis option is " + "supposed to go into Origin/Steam game launch options, and then you are supposed to launch the original " + "Titanfall2.exe " + "rather than NorthstarLauncher.exe to make use of it.", + "Northstar Warning", + MB_ICONWARNING); + + args.append(argBuffer.str()); + } + } + + auto len = args.length(); + cmdlineModified = new char[len + 1]; + if (!cmdlineModified) + { + spdlog::error("malloc failed for command line"); + return cmdlineOrg; + } + memcpy(cmdlineModified, args.c_str(), len + 1); + + spdlog::info("Command line: {}", cmdlineModified); + } + + return cmdlineModified; +} + +std::vector calledTags; +void CallLoadLibraryACallbacks(LPCSTR lpLibFileName, HMODULE moduleAddress) +{ + CModule cModule(moduleAddress); + + while (true) + { + bool bDoneCalling = true; + + for (auto& callbackStruct : GetDllLoadCallbacks()) + { + 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) +{ + CModule cModule(moduleAddress); + + while (true) + { + bool bDoneCalling = true; + + for (auto& callbackStruct : GetDllLoadCallbacks()) + { + 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 CallAllPendingDLLLoadCallbacks() +{ + HMODULE hMods[1024]; + HANDLE hProcess = GetCurrentProcess(); + DWORD cbNeeded; + unsigned int i; + + // Get a list of all the modules in this process. + if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) + { + for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) + { + wchar_t szModName[MAX_PATH]; + + // Get the full path to the module's file. + if (GetModuleFileNameExW(hProcess, hMods[i], szModName, sizeof(szModName) / sizeof(TCHAR))) + { + CallLoadLibraryWCallbacks(szModName, hMods[i]); + } + } + } +} + +// 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)) + { + moduleAddress = _LoadLibraryExA("XInput9_1_0.dll", hFile, dwFlags); + + if (!moduleAddress) + { + MessageBoxA(0, "Could not find XInput9_1_0.dll", "Northstar", MB_ICONERROR); + exit(EXIT_FAILURE); + + return nullptr; + } + } + else + moduleAddress = _LoadLibraryExA(lpLibFileName, hFile, dwFlags); + + if (moduleAddress) + CallLoadLibraryACallbacks(lpLibFileName, moduleAddress); + + return moduleAddress; +} + +// clang-format off +AUTOHOOK_ABSOLUTEADDR(_LoadLibraryA, LoadLibraryA, +HMODULE, WINAPI, (LPCSTR lpLibFileName)) +// clang-format on +{ + HMODULE moduleAddress = _LoadLibraryA(lpLibFileName); + + if (moduleAddress) + CallLoadLibraryACallbacks(lpLibFileName, moduleAddress); + + return moduleAddress; +} + +// clang-format off +AUTOHOOK_ABSOLUTEADDR(_LoadLibraryExW, LoadLibraryExW, +HMODULE, WINAPI, (LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)) +// clang-format on +{ + 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() +} diff --git a/NorthstarDLL/core/hooks.h b/NorthstarDLL/core/hooks.h new file mode 100644 index 00000000..f47791fb --- /dev/null +++ b/NorthstarDLL/core/hooks.h @@ -0,0 +1,311 @@ +#pragma once +#include "memory.h" + +#include +#include + +void InstallInitialHooks(); + +typedef void (*DllLoadCallbackFuncType)(CModule moduleAddress); +void AddDllLoadCallback(std::string dll, DllLoadCallbackFuncType callback, std::string tag = "", std::vector reliesOn = {}); +void AddDllLoadCallbackForDedicatedServer( + std::string dll, DllLoadCallbackFuncType callback, std::string tag = "", std::vector reliesOn = {}); +void AddDllLoadCallbackForClient( + std::string dll, DllLoadCallbackFuncType callback, std::string tag = "", std::vector 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)) diff --git a/NorthstarDLL/core/math/bitbuf.h b/NorthstarDLL/core/math/bitbuf.h new file mode 100644 index 00000000..8e8e216f --- /dev/null +++ b/NorthstarDLL/core/math/bitbuf.h @@ -0,0 +1,1148 @@ +#pragma once + +#define INLINE inline + +#define BITS_PER_INT 32 + +INLINE int GetBitForBitnum(int bitNum) +{ + static int bitsForBitnum[] = { + (1 << 0), (1 << 1), (1 << 2), (1 << 3), (1 << 4), (1 << 5), (1 << 6), (1 << 7), (1 << 8), (1 << 9), (1 << 10), + (1 << 11), (1 << 12), (1 << 13), (1 << 14), (1 << 15), (1 << 16), (1 << 17), (1 << 18), (1 << 19), (1 << 20), (1 << 21), + (1 << 22), (1 << 23), (1 << 24), (1 << 25), (1 << 26), (1 << 27), (1 << 28), (1 << 29), (1 << 30), (1 << 31), + }; + + return bitsForBitnum[(bitNum) & (BITS_PER_INT - 1)]; +} + +#undef BITS_PER_INT + +using u8 = uint8_t; +using u16 = uint16_t; +using u32 = uint32_t; +using u64 = uint64_t; +using uptr = uintptr_t; + +using i8 = int8_t; +using i16 = int16_t; +using i32 = int32_t; +using i64 = int64_t; +using iptr = intptr_t; + +// Endianess, don't use on PPC64 nor ARM64BE +#define LittleDWord(val) (val) + +static INLINE void StoreLittleDWord(u32* base, size_t dwordIndex, u32 dword) +{ + base[dwordIndex] = LittleDWord(dword); +} + +static INLINE u32 LoadLittleDWord(u32* base, size_t dwordIndex) +{ + return LittleDWord(base[dwordIndex]); +} + +#include + +static inline const u32 s_nMaskTable[33] = { + 0, + (1 << 1) - 1, + (1 << 2) - 1, + (1 << 3) - 1, + (1 << 4) - 1, + (1 << 5) - 1, + (1 << 6) - 1, + (1 << 7) - 1, + (1 << 8) - 1, + (1 << 9) - 1, + (1 << 10) - 1, + (1 << 11) - 1, + (1 << 12) - 1, + (1 << 13) - 1, + (1 << 14) - 1, + (1 << 15) - 1, + (1 << 16) - 1, + (1 << 17) - 1, + (1 << 18) - 1, + (1 << 19) - 1, + (1 << 20) - 1, + (1 << 21) - 1, + (1 << 22) - 1, + (1 << 23) - 1, + (1 << 24) - 1, + (1 << 25) - 1, + (1 << 26) - 1, + (1 << 27) - 1, + (1 << 28) - 1, + (1 << 29) - 1, + (1 << 30) - 1, + 0x7fffffff, + 0xffffffff, +}; + +enum EBitCoordType +{ + kCW_None, + kCW_LowPrecision, + kCW_Integral +}; + +class BitBufferBase +{ + protected: + INLINE void SetName(const char* name) + { + m_BufferName = name; + } + + public: + INLINE bool IsOverflowed() + { + return m_Overflow; + } + INLINE void SetOverflowed() + { + m_Overflow = true; + } + + INLINE const char* GetName() + { + return m_BufferName; + } + + private: + const char* m_BufferName = ""; + + protected: + u8 m_Overflow = false; +}; + +class BFRead : public BitBufferBase +{ + public: + BFRead() = default; + + INLINE BFRead(uptr data, size_t byteLength, size_t startPos = 0, const char* bufferName = 0) + { + StartReading(data, byteLength, startPos); + + if (bufferName) + SetName(bufferName); + } + + public: + INLINE void StartReading(uptr data, size_t byteLength, size_t startPos = 0) + { + m_Data = reinterpret_cast(data); + m_DataIn = m_Data; + + m_DataBytes = byteLength; + m_DataBits = byteLength << 3; + + m_DataEnd = reinterpret_cast(reinterpret_cast(m_Data) + m_DataBytes); + + Seek(startPos); + } + + INLINE void GrabNextDWord(bool overflow = false) + { + if (m_Data == m_DataEnd) + { + m_CachedBitsLeft = 1; + m_CachedBufWord = 0; + + m_DataIn++; + + if (overflow) + SetOverflowed(); + } + else + { + if (m_DataIn > m_DataEnd) + { + SetOverflowed(); + m_CachedBufWord = 0; + } + else + { + m_CachedBufWord = LittleDWord(*(m_DataIn++)); + } + } + } + + INLINE void FetchNext() + { + m_CachedBitsLeft = 32; + GrabNextDWord(false); + } + + INLINE i32 ReadOneBit() + { + i32 ret = m_CachedBufWord & 1; + + if (--m_CachedBitsLeft == 0) + FetchNext(); + else + m_CachedBufWord >>= 1; + + return ret; + } + + INLINE u32 ReadUBitLong(i32 numBits) + { + if (m_CachedBitsLeft >= numBits) + { + u32 ret = m_CachedBufWord & s_nMaskTable[numBits]; + + m_CachedBitsLeft -= numBits; + + if (m_CachedBitsLeft) + m_CachedBufWord >>= numBits; + else + FetchNext(); + + return ret; + } + else + { + // need to merge words + u32 ret = m_CachedBufWord; + numBits -= m_CachedBitsLeft; + + GrabNextDWord(true); + + if (IsOverflowed()) + return 0; + + ret |= ((m_CachedBufWord & s_nMaskTable[numBits]) << m_CachedBitsLeft); + + m_CachedBitsLeft = 32 - numBits; + m_CachedBufWord >>= numBits; + + return ret; + } + } + + INLINE i32 ReadSBitLong(int numBits) + { + i32 ret = ReadUBitLong(numBits); + return (ret << (32 - numBits)) >> (32 - numBits); + } + + INLINE u32 ReadUBitVar() + { + u32 ret = ReadUBitLong(6); + + switch (ret & (16 | 32)) + { + case 16: + ret = (ret & 15) | (ReadUBitLong(4) << 4); + // Assert(ret >= 16); + break; + case 32: + ret = (ret & 15) | (ReadUBitLong(8) << 4); + // Assert(ret >= 256); + break; + case 48: + ret = (ret & 15) | (ReadUBitLong(32 - 4) << 4); + // Assert(ret >= 4096); + break; + } + + return ret; + } + + INLINE u32 PeekUBitLong(i32 numBits) + { + i32 nSaveBA = m_CachedBitsLeft; + i32 nSaveW = m_CachedBufWord; + u32 const* pSaveP = m_DataIn; + u32 nRet = ReadUBitLong(numBits); + + m_CachedBitsLeft = nSaveBA; + m_CachedBufWord = nSaveW; + m_DataIn = pSaveP; + + return nRet; + } + + INLINE float ReadBitFloat() + { + u32 value = ReadUBitLong(32); + return *reinterpret_cast(&value); + } + + /*INLINE float ReadBitCoord() { + i32 intval = 0, fractval = 0, signbit = 0; + float value = 0.0; + + // Read the required integer and fraction flags + intval = ReadOneBit(); + fractval = ReadOneBit(); + + // If we got either parse them, otherwise it's a zero. + if (intval || fractval) { + // Read the sign bit + signbit = ReadOneBit(); + + // If there's an integer, read it in + if (intval) { + // Adjust the integers from [0..MAX_COORD_VALUE-1] to [1..MAX_COORD_VALUE] + intval = ReadUBitLong(COORD_INTEGER_BITS) + 1; + } + + // If there's a fraction, read it in + if (fractval) { + fractval = ReadUBitLong(COORD_FRACTIONAL_BITS); + } + + // Calculate the correct floating point value + value = intval + ((float)fractval * COORD_RESOLUTION); + + // Fixup the sign if negative. + if (signbit) + value = -value; + } + + return value; + } + + INLINE float ReadBitCoordMP() { + i32 intval = 0, fractval = 0, signbit = 0; + float value = 0.0; + + bool inBounds = ReadOneBit() ? true : false; + + // Read the required integer and fraction flags + intval = ReadOneBit(); + + // If we got either parse them, otherwise it's a zero. + if (intval) { + // Read the sign bit + signbit = ReadOneBit(); + + // If there's an integer, read it in + // Adjust the integers from [0..MAX_COORD_VALUE-1] to [1..MAX_COORD_VALUE] + if (inBounds) + value = ReadUBitLong(COORD_INTEGER_BITS_MP) + 1; + else + value = ReadUBitLong(COORD_INTEGER_BITS) + 1; + } + + // Fixup the sign if negative. + if (signbit) + value = -value; + + return value; + } + + INLINE float ReadBitCellCoord(int bits, EBitCoordType coordType) { + bool bIntegral = (coordType == kCW_Integral); + bool bLowPrecision = (coordType == kCW_LowPrecision); + + int intval = 0, fractval = 0; + float value = 0.0; + + if (bIntegral) + value = ReadUBitLong(bits); + else { + intval = ReadUBitLong(bits); + + // If there's a fraction, read it in + fractval = ReadUBitLong(bLowPrecision ? COORD_FRACTIONAL_BITS_MP_LOWPRECISION : COORD_FRACTIONAL_BITS); + + // Calculate the correct floating point value + value = intval + ((float)fractval * (bLowPrecision ? COORD_RESOLUTION_LOWPRECISION : COORD_RESOLUTION)); + } + + return value; + } + + INLINE float ReadBitNormal() { + // Read the sign bit + i32 signbit = ReadOneBit(); + + // Read the fractional part + u32 fractval = ReadUBitLong(NORMAL_FRACTIONAL_BITS); + + // Calculate the correct floating point value + float value = (float)fractval * NORMAL_RESOLUTION; + + // Fixup the sign if negative. + if (signbit) + value = -value; + + return value; + } + + INLINE void ReadBitVec3Coord(Vector& fa) { + i32 xflag, yflag, zflag; + + // This vector must be initialized! Otherwise, If any of the flags aren't set, + // the corresponding component will not be read and will be stack garbage. + fa.Init(0, 0, 0); + + xflag = ReadOneBit(); + yflag = ReadOneBit(); + zflag = ReadOneBit(); + + if (xflag) + fa[0] = ReadBitCoord(); + if (yflag) + fa[1] = ReadBitCoord(); + if (zflag) + fa[2] = ReadBitCoord(); + } + + INLINE void ReadBitVec3Normal(Vector& fa) { + i32 xflag = ReadOneBit(); + i32 yflag = ReadOneBit(); + + if (xflag) + fa[0] = ReadBitNormal(); + else + fa[0] = 0.0f; + + if (yflag) + fa[1] = ReadBitNormal(); + else + fa[1] = 0.0f; + + // The first two imply the third (but not its sign) + i32 znegative = ReadOneBit(); + + float fafafbfb = fa[0] * fa[0] + fa[1] * fa[1]; + if (fafafbfb < 1.0f) + fa[2] = sqrt(1.0f - fafafbfb); + else + fa[2] = 0.0f; + + if (znegative) + fa[2] = -fa[2]; + } + + INLINE void ReadBitAngles(QAngle& fa) { + Vector tmp; + ReadBitVec3Coord(tmp); + fa.Init(tmp.x, tmp.y, tmp.z); + }*/ + + INLINE float ReadBitAngle(int numBits) + { + float shift = (float)(GetBitForBitnum(numBits)); + + i32 i = ReadUBitLong(numBits); + float fReturn = (float)i * (360.0 / shift); + + return fReturn; + } + + INLINE i32 ReadChar() + { + return ReadSBitLong(sizeof(char) << 3); + } + INLINE u32 ReadByte() + { + return ReadUBitLong(sizeof(unsigned char) << 3); + } + + INLINE i32 ReadShort() + { + return ReadSBitLong(sizeof(short) << 3); + } + INLINE u32 ReadWord() + { + return ReadUBitLong(sizeof(unsigned short) << 3); + } + + INLINE i32 ReadLong() + { + return (i32)(ReadUBitLong(sizeof(i32) << 3)); + } + INLINE float ReadFloat() + { + u32 temp = ReadUBitLong(sizeof(float) << 3); + return *reinterpret_cast(&temp); + } + + INLINE u32 ReadVarInt32() + { + constexpr int kMaxVarint32Bytes = 5; + + u32 result = 0; + int count = 0; + u32 b; + + do + { + if (count == kMaxVarint32Bytes) + return result; + + b = ReadUBitLong(8); + result |= (b & 0x7F) << (7 * count); + ++count; + } while (b & 0x80); + + return result; + } + + INLINE u64 ReadVarInt64() + { + constexpr int kMaxVarintBytes = 10; + + u64 result = 0; + int count = 0; + u64 b; + + do + { + if (count == kMaxVarintBytes) + return result; + + b = ReadUBitLong(8); + result |= static_cast(b & 0x7F) << (7 * count); + ++count; + } while (b & 0x80); + + return result; + } + + INLINE void ReadBits(uptr outData, u32 bitLength) + { + u8* out = reinterpret_cast(outData); + int bitsLeft = bitLength; + + // align output to dword boundary + while (((uptr)out & 3) != 0 && bitsLeft >= 8) + { + *out = (unsigned char)ReadUBitLong(8); + ++out; + bitsLeft -= 8; + } + + // read dwords + while (bitsLeft >= 32) + { + *((u32*)out) = ReadUBitLong(32); + out += sizeof(u32); + bitsLeft -= 32; + } + + // read remaining bytes + while (bitsLeft >= 8) + { + *out = ReadUBitLong(8); + ++out; + bitsLeft -= 8; + } + + // read remaining bits + if (bitsLeft) + *out = ReadUBitLong(bitsLeft); + } + + INLINE bool ReadBytes(uptr outData, u32 byteLength) + { + ReadBits(outData, byteLength << 3); + return !IsOverflowed(); + } + + INLINE bool ReadString(char* str, i32 maxLength, bool stopAtLineTermination = false, i32* outNumChars = 0) + { + bool tooSmall = false; + int iChar = 0; + + while (1) + { + char val = ReadChar(); + + if (val == 0) + break; + else if (stopAtLineTermination && val == '\n') + break; + + if (iChar < (maxLength - 1)) + { + str[iChar] = val; + ++iChar; + } + else + { + tooSmall = true; + } + } + + // Make sure it's null-terminated. + // Assert(iChar < maxLength); + str[iChar] = 0; + + if (outNumChars) + *outNumChars = iChar; + + return !IsOverflowed() && !tooSmall; + } + + INLINE char* ReadAndAllocateString(bool* hasOverflowed = 0) + { + char str[2048]; + + int chars = 0; + bool overflowed = !ReadString(str, sizeof(str), false, &chars); + + if (hasOverflowed) + *hasOverflowed = overflowed; + + // Now copy into the output and return it; + char* ret = new char[chars + 1]; + for (u32 i = 0; i <= chars; i++) + ret[i] = str[i]; + + return ret; + } + + INLINE i64 ReadLongLong() + { + i64 retval; + u32* longs = (u32*)&retval; + + // Read the two DWORDs according to network endian + const short endianIndex = 0x0100; + u8* idx = (u8*)&endianIndex; + + longs[*idx++] = ReadUBitLong(sizeof(i32) << 3); + longs[*idx] = ReadUBitLong(sizeof(i32) << 3); + + return retval; + } + + INLINE bool Seek(size_t startPos) + { + bool bSucc = true; + + if (startPos < 0 || startPos > m_DataBits) + { + SetOverflowed(); + bSucc = false; + startPos = m_DataBits; + } + + // non-multiple-of-4 bytes at head of buffer. We put the "round off" + // at the head to make reading and detecting the end efficient. + int nHead = m_DataBytes & 3; + + int posBytes = startPos / 8; + if ((m_DataBytes < 4) || (nHead && (posBytes < nHead))) + { + // partial first dword + u8 const* partial = (u8 const*)m_Data; + + if (m_Data) + { + m_CachedBufWord = *(partial++); + if (nHead > 1) + m_CachedBufWord |= (*partial++) << 8; + if (nHead > 2) + m_CachedBufWord |= (*partial++) << 16; + } + + m_DataIn = (u32 const*)partial; + + m_CachedBufWord >>= (startPos & 31); + m_CachedBitsLeft = (nHead << 3) - (startPos & 31); + } + else + { + int adjustedPos = startPos - (nHead << 3); + + m_DataIn = reinterpret_cast(reinterpret_cast(m_Data) + ((adjustedPos / 32) << 2) + nHead); + + if (m_Data) + { + m_CachedBitsLeft = 32; + GrabNextDWord(); + } + else + { + m_CachedBufWord = 0; + m_CachedBitsLeft = 1; + } + + m_CachedBufWord >>= (adjustedPos & 31); + m_CachedBitsLeft = std::min(m_CachedBitsLeft, u32(32 - (adjustedPos & 31))); // in case grabnextdword overflowed + } + + return bSucc; + } + + INLINE size_t GetNumBitsRead() + { + if (!m_Data) + return 0; + + size_t nCurOfs = size_t(((iptr(m_DataIn) - iptr(m_Data)) / 4) - 1); + nCurOfs *= 32; + nCurOfs += (32 - m_CachedBitsLeft); + + size_t nAdjust = 8 * (m_DataBytes & 3); + return std::min(nCurOfs + nAdjust, m_DataBits); + } + + INLINE bool SeekRelative(size_t offset) + { + return Seek(GetNumBitsRead() + offset); + } + + INLINE size_t TotalBytesAvailable() + { + return m_DataBytes; + } + + INLINE size_t GetNumBitsLeft() + { + return m_DataBits - GetNumBitsRead(); + } + INLINE size_t GetNumBytesLeft() + { + return GetNumBitsLeft() >> 3; + } + + private: + size_t m_DataBits; // 0x0010 + size_t m_DataBytes; // 0x0018 + + u32 m_CachedBufWord; // 0x0020 + u32 m_CachedBitsLeft; // 0x0024 + + const u32* m_DataIn; // 0x0028 + const u32* m_DataEnd; // 0x0030 + const u32* m_Data; // 0x0038 +}; + +class BFWrite : public BitBufferBase +{ + public: + BFWrite() = default; + + INLINE BFWrite(uptr data, size_t byteLength, const char* bufferName = 0) + { + StartWriting(data, byteLength); + + if (bufferName) + SetName(bufferName); + } + + public: + INLINE void StartWriting(uptr data, size_t byteLength) + { + m_Data = reinterpret_cast(data); + m_DataOut = m_Data; + + m_DataBytes = byteLength; + m_DataBits = byteLength << 3; + + m_DataEnd = reinterpret_cast(reinterpret_cast(m_Data) + m_DataBytes); + } + + INLINE int GetNumBitsLeft() + { + return m_OutBitsLeft + (32 * (m_DataEnd - m_DataOut - 1)); + } + + INLINE void Reset() + { + m_Overflow = false; + m_OutBufWord = 0; + m_OutBitsLeft = 32; + m_DataOut = m_Data; + } + + INLINE void TempFlush() + { + if (m_OutBitsLeft != 32) + { + if (m_DataOut == m_DataEnd) + SetOverflowed(); + else + StoreLittleDWord(m_DataOut, 0, LoadLittleDWord(m_DataOut, 0) & ~s_nMaskTable[32 - m_OutBitsLeft] | m_OutBufWord); + } + + m_Flushed = true; + } + + INLINE u8* GetBasePointer() + { + TempFlush(); + return reinterpret_cast(m_Data); + } + + INLINE u8* GetData() + { + return GetBasePointer(); + } + + INLINE void Finish() + { + if (m_OutBitsLeft != 32) + { + if (m_DataOut == m_DataEnd) + SetOverflowed(); + + StoreLittleDWord(m_DataOut, 0, m_OutBufWord); + } + } + + INLINE void FlushNoCheck() + { + StoreLittleDWord(m_DataOut++, 0, m_OutBufWord); + + m_OutBitsLeft = 32; + m_OutBufWord = 0; + } + + INLINE void Flush() + { + if (m_DataOut == m_DataEnd) + SetOverflowed(); + else + StoreLittleDWord(m_DataOut++, 0, m_OutBufWord); + + m_OutBitsLeft = 32; + m_OutBufWord = 0; + } + + INLINE void WriteOneBitNoCheck(i32 value) + { + m_OutBufWord |= (value & 1) << (32 - m_OutBitsLeft); + + if (--m_OutBitsLeft == 0) + FlushNoCheck(); + } + + INLINE void WriteOneBit(i32 value) + { + m_OutBufWord |= (value & 1) << (32 - m_OutBitsLeft); + + if (--m_OutBitsLeft == 0) + Flush(); + } + + INLINE void WriteUBitLong(u32 data, i32 numBits, bool checkRange = true) + { + if (numBits <= m_OutBitsLeft) + { + if (checkRange) + m_OutBufWord |= (data) << (32 - m_OutBitsLeft); + else + m_OutBufWord |= (data & s_nMaskTable[numBits]) << (32 - m_OutBitsLeft); + + m_OutBitsLeft -= numBits; + + if (m_OutBitsLeft == 0) + Flush(); + } + else + { + // split dwords case + i32 overflowBits = (numBits - m_OutBitsLeft); + m_OutBufWord |= (data & s_nMaskTable[m_OutBitsLeft]) << (32 - m_OutBitsLeft); + Flush(); + m_OutBufWord = (data >> (numBits - overflowBits)); + m_OutBitsLeft = 32 - overflowBits; + } + } + + INLINE void WriteSBitLong(i32 data, i32 numBits) + { + WriteUBitLong((u32)data, numBits, false); + } + + INLINE void WriteUBitVar(u32 n) + { + if (n < 16) + WriteUBitLong(n, 6); + else if (n < 256) + WriteUBitLong((n & 15) | 16 | ((n & (128 | 64 | 32 | 16)) << 2), 10); + else if (n < 4096) + WriteUBitLong((n & 15) | 32 | ((n & (2048 | 1024 | 512 | 256 | 128 | 64 | 32 | 16)) << 2), 14); + else + { + WriteUBitLong((n & 15) | 48, 6); + WriteUBitLong((n >> 4), 32 - 4); + } + } + + INLINE void WriteBitFloat(float value) + { + auto temp = &value; + WriteUBitLong(*reinterpret_cast(temp), 32); + } + + INLINE void WriteFloat(float value) + { + auto temp = &value; + WriteUBitLong(*reinterpret_cast(temp), 32); + } + + INLINE bool WriteBits(const uptr data, i32 numBits) + { + u8* out = (u8*)data; + i32 numBitsLeft = numBits; + + // Bounds checking.. + if ((GetNumBitsWritten() + numBits) > m_DataBits) + { + SetOverflowed(); + return false; + } + + // !! speed!! need fast paths + // write remaining bytes + while (numBitsLeft >= 8) + { + WriteUBitLong(*out, 8, false); + ++out; + numBitsLeft -= 8; + } + + // write remaining bits + if (numBitsLeft) + WriteUBitLong(*out, numBitsLeft, false); + + return !IsOverflowed(); + } + + INLINE bool WriteBytes(const uptr data, i32 numBytes) + { + return WriteBits(data, numBytes << 3); + } + + INLINE i32 GetNumBitsWritten() + { + return (32 - m_OutBitsLeft) + (32 * (m_DataOut - m_Data)); + } + + INLINE i32 GetNumBytesWritten() + { + return (GetNumBitsWritten() + 7) >> 3; + } + + INLINE void WriteChar(i32 val) + { + WriteSBitLong(val, sizeof(char) << 3); + } + + INLINE void WriteByte(i32 val) + { + WriteUBitLong(val, sizeof(unsigned char) << 3, false); + } + + INLINE void WriteShort(i32 val) + { + WriteSBitLong(val, sizeof(short) << 3); + } + + INLINE void WriteWord(i32 val) + { + WriteUBitLong(val, sizeof(unsigned short) << 3); + } + + INLINE bool WriteString(const char* str) + { + if (str) + while (*str) + WriteChar(*(str++)); + + WriteChar(0); + + return !IsOverflowed(); + } + + INLINE void WriteLongLong(i64 val) + { + u32* pLongs = (u32*)&val; + + // Insert the two DWORDS according to network endian + const short endianIndex = 0x0100; + u8* idx = (u8*)&endianIndex; + + WriteUBitLong(pLongs[*idx++], sizeof(i32) << 3); + WriteUBitLong(pLongs[*idx], sizeof(i32) << 3); + } + + /*INLINE void WriteBitCoord(const float f) { + i32 signbit = (f <= -COORD_RESOLUTION); + i32 intval = (i32)abs(f); + i32 fractval = abs((i32)(f * COORD_DENOMINATOR)) & (COORD_DENOMINATOR - 1); + + // Send the bit flags that indicate whether we have an integer part and/or a fraction part. + WriteOneBit(intval); + WriteOneBit(fractval); + + if (intval || fractval) { + // Send the sign bit + WriteOneBit(signbit); + + // Send the integer if we have one. + if (intval) { + // Adjust the integers from [1..MAX_COORD_VALUE] to [0..MAX_COORD_VALUE-1] + intval--; + WriteUBitLong((u32)intval, COORD_INTEGER_BITS); + } + + // Send the fraction if we have one + if (fractval) { + WriteUBitLong((u32)fractval, COORD_FRACTIONAL_BITS); + } + } + } + + INLINE void WriteBitCoordMP(const float f) { + i32 signbit = (f <= -COORD_RESOLUTION); + i32 intval = (i32)abs(f); + i32 fractval = (abs((i32)(f * COORD_DENOMINATOR)) & (COORD_DENOMINATOR - 1)); + + bool bInBounds = intval < (1 << COORD_INTEGER_BITS_MP); + + WriteOneBit(bInBounds); + + // Send the sign bit + WriteOneBit(intval); + + if (intval) { + WriteOneBit(signbit); + + // Send the integer if we have one. + // Adjust the integers from [1..MAX_COORD_VALUE] to [0..MAX_COORD_VALUE-1] + intval--; + + if (bInBounds) + WriteUBitLong((u32)intval, COORD_INTEGER_BITS_MP); + else + WriteUBitLong((u32)intval, COORD_INTEGER_BITS); + } + } + + INLINE void WriteBitCellCoord(const float f, int bits, EBitCoordType coordType) { + bool bIntegral = (coordType == kCW_Integral); + bool bLowPrecision = (coordType == kCW_LowPrecision); + + i32 intval = (i32)abs(f); + i32 fractval = bLowPrecision ? (abs((i32)(f * COORD_DENOMINATOR_LOWPRECISION)) & (COORD_DENOMINATOR_LOWPRECISION - 1)) : + (abs((i32)(f * COORD_DENOMINATOR)) & (COORD_DENOMINATOR - 1)); + + if (bIntegral) + WriteUBitLong((u32)intval, bits); + else { + WriteUBitLong((u32)intval, bits); + WriteUBitLong((u32)fractval, bLowPrecision ? COORD_FRACTIONAL_BITS_MP_LOWPRECISION : COORD_FRACTIONAL_BITS); + } + }*/ + + INLINE void SeekToBit(int bit) + { + TempFlush(); + + m_DataOut = m_Data + (bit / 32); + m_OutBufWord = LoadLittleDWord(m_DataOut, 0); + m_OutBitsLeft = 32 - (bit & 31); + } + + /*INLINE void WriteBitVec3Coord(const Vector& fa) { + i32 xflag, yflag, zflag; + + xflag = (fa[0] >= COORD_RESOLUTION) || (fa[0] <= -COORD_RESOLUTION); + yflag = (fa[1] >= COORD_RESOLUTION) || (fa[1] <= -COORD_RESOLUTION); + zflag = (fa[2] >= COORD_RESOLUTION) || (fa[2] <= -COORD_RESOLUTION); + + WriteOneBit(xflag); + WriteOneBit(yflag); + WriteOneBit(zflag); + + if (xflag) + WriteBitCoord(fa[0]); + if (yflag) + WriteBitCoord(fa[1]); + if (zflag) + WriteBitCoord(fa[2]); + } + + INLINE void WriteBitNormal(float f) { + i32 signbit = (f <= -NORMAL_RESOLUTION); + + // NOTE: Since +/-1 are valid values for a normal, I'm going to encode that as all ones + u32 fractval = abs((i32)(f * NORMAL_DENOMINATOR)); + + // clamp.. + if (fractval > NORMAL_DENOMINATOR) + fractval = NORMAL_DENOMINATOR; + + // Send the sign bit + WriteOneBit(signbit); + + // Send the fractional component + WriteUBitLong(fractval, NORMAL_FRACTIONAL_BITS); + } + + INLINE void WriteBitVec3Normal(const Vector& fa) { + i32 xflag, yflag; + + xflag = (fa[0] >= NORMAL_RESOLUTION) || (fa[0] <= -NORMAL_RESOLUTION); + yflag = (fa[1] >= NORMAL_RESOLUTION) || (fa[1] <= -NORMAL_RESOLUTION); + + WriteOneBit(xflag); + WriteOneBit(yflag); + + if (xflag) + WriteBitNormal(fa[0]); + if (yflag) + WriteBitNormal(fa[1]); + + // Write z sign bit + i32 signbit = (fa[2] <= -NORMAL_RESOLUTION); + WriteOneBit(signbit); + }*/ + + INLINE void WriteBitAngle(float angle, int numBits) + { + u32 shift = GetBitForBitnum(numBits); + u32 mask = shift - 1; + + i32 d = (i32)((angle / 360.0) * shift); + d &= mask; + + WriteUBitLong((u32)d, numBits); + } + + INLINE bool WriteBitsFromBuffer(BFRead* in, int numBits) + { + while (numBits > 32) + { + WriteUBitLong(in->ReadUBitLong(32), 32); + numBits -= 32; + } + + WriteUBitLong(in->ReadUBitLong(numBits), numBits); + return !IsOverflowed() && !in->IsOverflowed(); + } + + /*INLINE void WriteBitAngles(const QAngle& fa) { + // FIXME: + Vector tmp(fa.x, fa.y, fa.z); + WriteBitVec3Coord(tmp); + }*/ + + private: + size_t m_DataBits = 0; + size_t m_DataBytes = 0; + + u32 m_OutBufWord = 0; + u32 m_OutBitsLeft = 32; + + u32* m_DataOut = nullptr; + u32* m_DataEnd = nullptr; + u32* m_Data = nullptr; + + bool m_Flushed = false; // :flushed: +}; + +#undef INLINE diff --git a/NorthstarDLL/core/math/bits.cpp b/NorthstarDLL/core/math/bits.cpp new file mode 100644 index 00000000..014899f2 --- /dev/null +++ b/NorthstarDLL/core/math/bits.cpp @@ -0,0 +1,46 @@ +//=============================================================================// +// +// Purpose: look for NANs, infinities, and underflows. +// +//=============================================================================// + +#include "pch.h" +#include "bits.h" + +//----------------------------------------------------------------------------- +// This follows the ANSI/IEEE 754-1985 standard +//----------------------------------------------------------------------------- +unsigned long& FloatBits(float& f) +{ + return *reinterpret_cast(&f); +} + +unsigned long const& FloatBits(float const& f) +{ + return *reinterpret_cast(&f); +} + +float BitsToFloat(unsigned long i) +{ + return *reinterpret_cast(&i); +} + +bool IsFinite(float f) +{ + return ((FloatBits(f) & 0x7F800000) != 0x7F800000); +} + +unsigned long FloatAbsBits(float f) +{ + return FloatBits(f) & 0x7FFFFFFF; +} + +float FloatMakePositive(float f) +{ + return fabsf(f); +} + +float FloatNegate(float f) +{ + return -f; // BitsToFloat( FloatBits(f) ^ 0x80000000 ); +} diff --git a/NorthstarDLL/core/math/bits.h b/NorthstarDLL/core/math/bits.h new file mode 100644 index 00000000..0532a9bd --- /dev/null +++ b/NorthstarDLL/core/math/bits.h @@ -0,0 +1,10 @@ +#pragma once + +unsigned long& FloatBits(float& f); +unsigned long const& FloatBits(float const& f); +float BitsToFloat(unsigned long i); +bool IsFinite(float f); +unsigned long FloatAbsBits(float f); + +#define FLOAT32_NAN_BITS (std::uint32_t)0x7FC00000 // NaN! +#define FLOAT32_NAN BitsToFloat(FLOAT32_NAN_BITS) diff --git a/NorthstarDLL/core/math/color.cpp b/NorthstarDLL/core/math/color.cpp new file mode 100644 index 00000000..05cf645f --- /dev/null +++ b/NorthstarDLL/core/math/color.cpp @@ -0,0 +1,26 @@ +#include "pch.h" + +// clang-format off +namespace NS::Colors +{ + Color SCRIPT_UI (100, 255, 255); + Color SCRIPT_CL (100, 255, 100); + Color SCRIPT_SV (255, 100, 255); + Color NATIVE_UI (50 , 150, 150); + Color NATIVE_CL (50 , 150, 50 ); + Color NATIVE_SV (150, 50 , 150); + Color NATIVE_ENGINE (252, 133, 153); + Color FILESYSTEM (0 , 150, 150); + Color RPAK (255, 190, 0 ); + Color NORTHSTAR (66 , 72 , 128); + Color ECHO (150, 150, 159); + + Color TRACE (0 , 255, 255); + Color DEBUG (0 , 255, 255); + Color INFO (16 , 160, 16 ); + Color WARN (255, 255, 0 ); + Color ERR (255, 50 , 50 ); + Color CRIT (255, 0 , 0 ); + Color OFF (0 , 0 , 0 ); +}; +// clang-format on diff --git a/NorthstarDLL/core/math/color.h b/NorthstarDLL/core/math/color.h new file mode 100644 index 00000000..4dc9b36e --- /dev/null +++ b/NorthstarDLL/core/math/color.h @@ -0,0 +1,197 @@ +#pragma once + +struct color24 +{ + uint8_t r, g, b; +}; + +typedef struct color32_s +{ + bool operator!=(const struct color32_s& other) const + { + return r != other.r || g != other.g || b != other.b || a != other.a; + } + inline unsigned* asInt(void) + { + return reinterpret_cast(this); + } + inline const unsigned* asInt(void) const + { + return reinterpret_cast(this); + } + inline void Copy(const color32_s& rhs) + { + *asInt() = *rhs.asInt(); + } + + uint8_t r, g, b, a; +} color32; + +struct SourceColor +{ + unsigned char R; + unsigned char G; + unsigned char B; + unsigned char A; + + SourceColor(unsigned char r, unsigned char g, unsigned char b, unsigned char a) + { + R = r; + G = g; + B = b; + A = a; + } + + SourceColor() + { + R = 0; + G = 0; + B = 0; + A = 0; + } +}; + +//----------------------------------------------------------------------------- +// Purpose: Basic handler for an rgb set of colors +// This class is fully inline +//----------------------------------------------------------------------------- +class Color +{ + public: + Color(int r, int g, int b, int a = 255) + { + _color[0] = (unsigned char)r; + _color[1] = (unsigned char)g; + _color[2] = (unsigned char)b; + _color[3] = (unsigned char)a; + } + void SetColor(int _r, int _g, int _b, int _a = 0) + { + _color[0] = (unsigned char)_r; + _color[1] = (unsigned char)_g; + _color[2] = (unsigned char)_b; + _color[3] = (unsigned char)_a; + } + void GetColor(int& _r, int& _g, int& _b, int& _a) const + { + _r = _color[0]; + _g = _color[1]; + _b = _color[2]; + _a = _color[3]; + } + int GetValue(int index) const + { + return _color[index]; + } + void SetRawColor(int color32) + { + *((int*)this) = color32; + } + int GetRawColor(void) const + { + return *((int*)this); + } + + inline int r() const + { + return _color[0]; + } + inline int g() const + { + return _color[1]; + } + inline int b() const + { + return _color[2]; + } + inline int a() const + { + return _color[3]; + } + + unsigned char& operator[](int index) + { + return _color[index]; + } + + const unsigned char& operator[](int index) const + { + return _color[index]; + } + + bool operator==(const Color& rhs) const + { + return (*((int*)this) == *((int*)&rhs)); + } + + bool operator!=(const Color& rhs) const + { + return !(operator==(rhs)); + } + + Color& operator=(const Color& rhs) + { + SetRawColor(rhs.GetRawColor()); + return *this; + } + + Color& operator=(const color32& rhs) + { + _color[0] = rhs.r; + _color[1] = rhs.g; + _color[2] = rhs.b; + _color[3] = rhs.a; + return *this; + } + + color32 ToColor32(void) const + { + color32 newColor {}; + newColor.r = _color[0]; + newColor.g = _color[1]; + newColor.b = _color[2]; + newColor.a = _color[3]; + return newColor; + } + + std::string ToANSIColor() + { + std::string out = "\033[38;2;"; + out += std::to_string(_color[0]) + ";"; + out += std::to_string(_color[1]) + ";"; + out += std::to_string(_color[2]) + ";"; + out += "49m"; + return out; + } + + SourceColor ToSourceColor() + { + return SourceColor(_color[0], _color[1], _color[2], _color[3]); + } + + private: + unsigned char _color[4]; +}; + +namespace NS::Colors +{ + extern Color SCRIPT_UI; + extern Color SCRIPT_CL; + extern Color SCRIPT_SV; + extern Color NATIVE_UI; + extern Color NATIVE_CL; + extern Color NATIVE_SV; + extern Color NATIVE_ENGINE; + extern Color FILESYSTEM; + extern Color RPAK; + extern Color NORTHSTAR; + extern Color ECHO; + + extern Color TRACE; + extern Color DEBUG; + extern Color INFO; + extern Color WARN; + extern Color ERR; + extern Color CRIT; + extern Color OFF; +}; // namespace NS::Colors diff --git a/NorthstarDLL/core/math/vector.h b/NorthstarDLL/core/math/vector.h new file mode 100644 index 00000000..95eae7ca --- /dev/null +++ b/NorthstarDLL/core/math/vector.h @@ -0,0 +1,52 @@ +#pragma once + +union Vector3 +{ + struct + { + float x; + float y; + float z; + }; + + float raw[3]; + + Vector3() : x(0), y(0), z(0) {} + Vector3(float x, float y, float z) : x(x), y(y), z(z) {} + Vector3(float* pRawFloats) // assumes float[3] => vector + { + memcpy(raw, pRawFloats, sizeof(this)); + } + + void MakeValid() + { + for (auto& fl : raw) + if (isnan(fl)) + fl = 0; + } + + // todo: more operators maybe + bool operator==(const Vector3& other) + { + return x == other.x && y == other.y && z == other.z; + } +}; + +union QAngle +{ + struct + { + float x; + float y; + float z; + float w; + }; + + float raw[4]; + + // todo: more operators maybe + bool operator==(const QAngle& other) + { + return x == other.x && y == other.y && z == other.z && w == other.w; + } +}; diff --git a/NorthstarDLL/core/memalloc.cpp b/NorthstarDLL/core/memalloc.cpp new file mode 100644 index 00000000..122ab444 --- /dev/null +++ b/NorthstarDLL/core/memalloc.cpp @@ -0,0 +1,74 @@ +#include "pch.h" +#include "core/memalloc.h" +#include "core/tier0.h" + +using namespace Tier0; + +// TODO: rename to malloc and free after removing statically compiled .libs + +extern "C" void* _malloc_base(size_t n) +{ + // allocate into static buffer if g_pMemAllocSingleton isn't initialised + if (!g_pMemAllocSingleton) + TryCreateGlobalMemAlloc(); + + return g_pMemAllocSingleton->m_vtable->Alloc(g_pMemAllocSingleton, n); +} + +/*extern "C" void* malloc(size_t n) +{ + return _malloc_base(n); +}*/ + +extern "C" void _free_base(void* p) +{ + if (!g_pMemAllocSingleton) + TryCreateGlobalMemAlloc(); + + g_pMemAllocSingleton->m_vtable->Free(g_pMemAllocSingleton, p); +} + +extern "C" void* _realloc_base(void* oldPtr, size_t size) +{ + if (!g_pMemAllocSingleton) + TryCreateGlobalMemAlloc(); + + return g_pMemAllocSingleton->m_vtable->Realloc(g_pMemAllocSingleton, oldPtr, size); +} + +extern "C" void* _calloc_base(size_t n, size_t size) +{ + size_t bytes = n * size; + void* memory = _malloc_base(bytes); + if (memory) + { + memset(memory, 0, bytes); + } + return memory; +} + +extern "C" char* _strdup_base(const char* src) +{ + char* str; + char* p; + int len = 0; + + while (src[len]) + len++; + str = reinterpret_cast(_malloc_base(len + 1)); + p = str; + while (*src) + *p++ = *src++; + *p = '\0'; + return str; +} + +void* operator new(size_t n) +{ + return _malloc_base(n); +} + +void operator delete(void* p) noexcept +{ + _free_base(p); +} // /FORCE:MULTIPLE diff --git a/NorthstarDLL/core/memalloc.h b/NorthstarDLL/core/memalloc.h new file mode 100644 index 00000000..7df68429 --- /dev/null +++ b/NorthstarDLL/core/memalloc.h @@ -0,0 +1,49 @@ +#pragma once + +#include "rapidjson/document.h" +//#include "include/rapidjson/allocators.h" + +extern "C" void* _malloc_base(size_t size); +extern "C" void* _calloc_base(size_t const count, size_t const size); +extern "C" void* _realloc_base(void* block, size_t size); +extern "C" void* _recalloc_base(void* const block, size_t const count, size_t const size); +extern "C" void _free_base(void* const block); +extern "C" char* _strdup_base(const char* src); + +void* operator new(size_t n); +void operator delete(void* p) noexcept; + +// void* malloc(size_t n); + +class SourceAllocator +{ + public: + static const bool kNeedFree = true; + void* Malloc(size_t size) + { + if (size) // behavior of malloc(0) is implementation defined. + return _malloc_base(size); + else + return NULL; // standardize to returning NULL. + } + void* Realloc(void* originalPtr, size_t originalSize, size_t newSize) + { + (void)originalSize; + if (newSize == 0) + { + _free_base(originalPtr); + return NULL; + } + return _realloc_base(originalPtr, newSize); + } + static void Free(void* ptr) + { + _free_base(ptr); + } +}; + +typedef rapidjson::GenericDocument, rapidjson::MemoryPoolAllocator, SourceAllocator> rapidjson_document; +// typedef rapidjson::GenericDocument, SourceAllocator, SourceAllocator> rapidjson_document; +// typedef rapidjson::Document rapidjson_document; +// using MyDocument = rapidjson::GenericDocument, MemoryAllocator>; +// using rapidjson_document = rapidjson::GenericDocument, SourceAllocator, SourceAllocator>; diff --git a/NorthstarDLL/core/memory.cpp b/NorthstarDLL/core/memory.cpp new file mode 100644 index 00000000..5e7c3e78 --- /dev/null +++ b/NorthstarDLL/core/memory.cpp @@ -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(pAddress)) {} + +// operators +MemoryAddress::operator uintptr_t() const +{ + return m_nAddress; +} + +MemoryAddress::operator void*() const +{ + return reinterpret_cast(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(ret); + + return MemoryAddress(ret); +} + +// patching +void MemoryAddress::Patch(const uint8_t* pBytes, const size_t nSize) +{ + if (nSize) + WriteProcessMemory(GetCurrentProcess(), reinterpret_cast(m_nAddress), pBytes, nSize, NULL); +} + +void MemoryAddress::Patch(const std::initializer_list 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 HexBytesToString(const char* pHexString) +{ + std::vector 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_msg(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 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(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(mInfo.SizeOfImage); + m_pModuleBase = reinterpret_cast(mInfo.lpBaseOfDll); + m_nAddress = m_pModuleBase; + + if (!m_nModuleSize || !m_pModuleBase) + return; + + m_pDOSHeader = reinterpret_cast(m_pModuleBase); + m_pNTHeaders = reinterpret_cast(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(hCurrentSection.Name)), + static_cast(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(GetProcAddress(reinterpret_cast(m_nAddress), pExportName))); +} + +MemoryAddress CModule::FindPattern(const uint8_t* pPattern, const char* pMask) +{ + if (!m_ExecutableCode.IsSectionValid()) + return MemoryAddress(); + + uint64_t nBase = static_cast(m_ExecutableCode.m_pSectionBase); + uint64_t nSize = static_cast(m_ExecutableCode.m_nSectionSize); + + const uint8_t* pData = reinterpret_cast(nBase); + const uint8_t* pEnd = pData + static_cast(nSize) - strlen(pMask); + + int nMasks[64]; // 64*16 = enough masks for 1024 bytes. + int iNumMasks = static_cast(ceil(static_cast(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(&nMasks[i]), j); + } + } + } + __m128i xmm1 = _mm_loadu_si128(reinterpret_cast(pPattern)); + __m128i xmm2, xmm3, msks; + for (; pData != pEnd; _mm_prefetch(reinterpret_cast(++pData + 64), _MM_HINT_NTA)) + { + if (pPattern[0] == pData[0]) + { + xmm2 = _mm_loadu_si128(reinterpret_cast(pData)); + msks = _mm_cmpeq_epi8(xmm1, xmm2); + if ((_mm_movemask_epi8(msks) & nMasks[0]) == nMasks[0]) + { + for (uintptr_t i = 1; i < static_cast(iNumMasks); ++i) + { + xmm2 = _mm_loadu_si128(reinterpret_cast((pData + i * 16))); + xmm3 = _mm_loadu_si128(reinterpret_cast((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(pData)); + } + } + else + goto CONTINUE; + } + + return MemoryAddress((&*(const_cast(pData)))); + } + } + + CONTINUE:; + } + + return MemoryAddress(); +} + +inline std::pair, std::string> MaskedBytesFromPattern(const char* pPatternString) +{ + std::vector 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_msg(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()); +} diff --git a/NorthstarDLL/core/memory.h b/NorthstarDLL/core/memory.h new file mode 100644 index 00000000..38c76cb3 --- /dev/null +++ b/NorthstarDLL/core/memory.h @@ -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 T As() + { + return reinterpret_cast(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 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 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); +}; diff --git a/NorthstarDLL/core/structs.h b/NorthstarDLL/core/structs.h new file mode 100644 index 00000000..ac29f711 --- /dev/null +++ b/NorthstarDLL/core/structs.h @@ -0,0 +1,62 @@ +#pragma once +//clang-format off +// About this file: +// This file contains several macros used to define reversed structs +// The reason we use these macros is to make it easier to update existing structs +// when new fields are reversed +// This means we dont have to manually add padding, and recalculate when updating + +// Technical note: +// While functionally, these structs act like a regular struct, they are actually +// defined as unions with anonymous structs in them. +// This means that each field is essentially an offset into a union. +// We acknowledge that this goes against C++'s active-member guideline for unions +// However, this is not such a big deal here as these structs will not be constructed + +// Usage: +// To use these macros, define a struct like so: +/* +OFFSET_STRUCT(Name) +{ + STRUCT_SIZE(0x100) // Total in-memory struct size + FIELD(0x0, int field) // offset, signature +} +*/ + +#define OFFSET_STRUCT(name) union name +#define STRUCT_SIZE(size) char __size[size]; +#define STRUCT_FIELD_OFFSET(offset, signature) \ + struct \ + { \ + char CONCAT2(pad, __LINE__)[offset]; \ + signature; \ + }; + +// Special case for a 0-offset field +#define STRUCT_FIELD_NOOFFSET(offset, signature) signature; + +// Based on: https://stackoverflow.com/questions/11632219/c-preprocessor-macro-specialisation-based-on-an-argument +// Yes, this is hacky, but it works quite well actually +// This basically makes sure that when the offset is 0x0, no padding field gets generated +#define OFFSET_0x0 () + +#define IIF(c) CONCAT2(IIF_, c) +#define IIF_0(t, ...) __VA_ARGS__ +#define IIF_1(t, ...) t + +#define PROBE(x) x, 1 +#define MSVC_VA_ARGS_WORKAROUND(define, args) define args +#define CHECK(...) MSVC_VA_ARGS_WORKAROUND(CHECK_N, (__VA_ARGS__, 0)) +#define DO_PROBE(offset) PROBE_PROXY(OFFSET_##offset) // concatenate prefix with offset +#define PROBE_PROXY(...) PROBE_PRIMITIVE(__VA_ARGS__) // expand arguments +#define PROBE_PRIMITIVE(x) PROBE_COMBINE_##x // merge +#define PROBE_COMBINE_(...) PROBE(~) // if merge successful, expand to probe + +#define CHECK_N(x, n, ...) n + +#define IS_0(offset) CHECK(DO_PROBE(offset)) + +#define FIELD(offset, signature) IIF(IS_0(offset))(STRUCT_FIELD_NOOFFSET, STRUCT_FIELD_OFFSET)(offset, signature) +#define FIELDS FIELD + +//clang-format on diff --git a/NorthstarDLL/core/tier0.cpp b/NorthstarDLL/core/tier0.cpp new file mode 100644 index 00000000..61ad7783 --- /dev/null +++ b/NorthstarDLL/core/tier0.cpp @@ -0,0 +1,37 @@ +#include "pch.h" +#include "tier0.h" + +// use the Tier0 namespace for tier0 funcs +namespace Tier0 +{ + IMemAlloc* g_pMemAllocSingleton; + + ErrorType Error; + CommandLineType CommandLine; + Plat_FloatTimeType Plat_FloatTime; + ThreadInServerFrameThreadType ThreadInServerFrameThread; +} // namespace Tier0 + +typedef Tier0::IMemAlloc* (*CreateGlobalMemAllocType)(); +CreateGlobalMemAllocType CreateGlobalMemAlloc; + +// needs to be a seperate function, since memalloc.cpp calls it +void TryCreateGlobalMemAlloc() +{ + // init memalloc stuff + CreateGlobalMemAlloc = + reinterpret_cast(GetProcAddress(GetModuleHandleA("tier0.dll"), "CreateGlobalMemAlloc")); + Tier0::g_pMemAllocSingleton = CreateGlobalMemAlloc(); // if it already exists, this returns the preexisting IMemAlloc instance +} + +ON_DLL_LOAD("tier0.dll", Tier0GameFuncs, (CModule module)) +{ + // shouldn't be necessary, but do this just in case + TryCreateGlobalMemAlloc(); + + // setup tier0 funcs + Tier0::Error = module.GetExport("Error").As(); + Tier0::CommandLine = module.GetExport("CommandLine").As(); + Tier0::Plat_FloatTime = module.GetExport("Plat_FloatTime").As(); + Tier0::ThreadInServerFrameThread = module.GetExport("ThreadInServerFrameThread").As(); +} diff --git a/NorthstarDLL/core/tier0.h b/NorthstarDLL/core/tier0.h new file mode 100644 index 00000000..92a63027 --- /dev/null +++ b/NorthstarDLL/core/tier0.h @@ -0,0 +1,68 @@ +#pragma once +namespace Tier0 +{ + 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; + }; + + 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 {} + }; + + extern IMemAlloc* g_pMemAllocSingleton; + + 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; +} // namespace Tier0 + +void TryCreateGlobalMemAlloc(); diff --git a/NorthstarDLL/crashhandler.cpp b/NorthstarDLL/crashhandler.cpp deleted file mode 100644 index be09f870..00000000 --- a/NorthstarDLL/crashhandler.cpp +++ /dev/null @@ -1,376 +0,0 @@ -#include "pch.h" -#include "crashhandler.h" -#include "dedicated.h" -#include "nsprefix.h" -#include "version.h" -#include "modmanager.h" - -#include - -HANDLE hExceptionFilter; - -std::shared_ptr storedException {}; - -#define RUNTIME_EXCEPTION 3765269347 -// clang format did this :/ -std::map ExceptionNames = { - {EXCEPTION_ACCESS_VIOLATION, "Access Violation"}, {EXCEPTION_IN_PAGE_ERROR, "Access Violation"}, - {EXCEPTION_ARRAY_BOUNDS_EXCEEDED, "Array bounds exceeded"}, {EXCEPTION_DATATYPE_MISALIGNMENT, "Datatype misalignment"}, - {EXCEPTION_FLT_DENORMAL_OPERAND, "Denormal operand"}, {EXCEPTION_FLT_DIVIDE_BY_ZERO, "Divide by zero (float)"}, - {EXCEPTION_FLT_INEXACT_RESULT, "Inexact float result"}, {EXCEPTION_FLT_INVALID_OPERATION, "Invalid operation"}, - {EXCEPTION_FLT_OVERFLOW, "Numeric overflow (float)"}, {EXCEPTION_FLT_STACK_CHECK, "Stack check"}, - {EXCEPTION_FLT_UNDERFLOW, "Numeric underflow (float)"}, {EXCEPTION_ILLEGAL_INSTRUCTION, "Illegal instruction"}, - {EXCEPTION_INT_DIVIDE_BY_ZERO, "Divide by zero (int)"}, {EXCEPTION_INT_OVERFLOW, "Numeric overfloat (int)"}, - {EXCEPTION_INVALID_DISPOSITION, "Invalid disposition"}, {EXCEPTION_NONCONTINUABLE_EXCEPTION, "Non-continuable exception"}, - {EXCEPTION_PRIV_INSTRUCTION, "Priviledged instruction"}, {EXCEPTION_STACK_OVERFLOW, "Stack overflow"}, - {RUNTIME_EXCEPTION, "Uncaught runtime exception:"}, -}; - -void PrintExceptionLog(ExceptionLog& exc) -{ - // General crash message - spdlog::error("Northstar version: {}", version); - spdlog::error("Northstar has crashed! a minidump has been written and exception info is available below:"); - if (g_pModManager) - { - spdlog::error("Loaded mods: "); - for (const auto& mod : g_pModManager->m_LoadedMods) - { - if (mod.m_bEnabled) - { - spdlog::error("{} {}", mod.Name, mod.Version); - } - } - } - spdlog::error(exc.cause); - // If this was a runtime error, print the message - if (exc.runtimeInfo.length() != 0) - spdlog::error("\"{}\"", exc.runtimeInfo); - spdlog::error("At: {} + {}", exc.trace[0].name, exc.trace[0].relativeAddress); - spdlog::error(""); - spdlog::error("Stack trace:"); - - // Generate format string for stack trace - std::stringstream formatString; - formatString << " {:<" << exc.longestModuleNameLength + 2 << "} {:<" << exc.longestRelativeAddressLength << "} {}"; - std::string guide = fmt::format(formatString.str(), "Module Name", "Offset", "Full Address"); - std::string line(guide.length() + 2, '-'); - spdlog::error(guide); - spdlog::error(line); - - for (const auto& module : exc.trace) - spdlog::error(formatString.str(), module.name, module.relativeAddress, module.address); - - // Print dump of most CPU registers - spdlog::error(""); - for (const auto& reg : exc.registerDump) - spdlog::error(reg); - - if (!IsDedicatedServer()) - MessageBoxA( - 0, - "Northstar has crashed! Crash info can be found in R2Northstar/logs", - "Northstar has crashed!", - MB_ICONERROR | MB_OK | MB_SYSTEMMODAL); -} - -std::string GetExceptionName(ExceptionLog& exc) -{ - const DWORD exceptionCode = exc.exceptionRecord.ExceptionCode; - auto name = ExceptionNames[exceptionCode]; - if (exceptionCode == EXCEPTION_ACCESS_VIOLATION || exceptionCode == EXCEPTION_IN_PAGE_ERROR) - { - std::stringstream returnString; - returnString << name << ": "; - - auto exceptionInfo0 = exc.exceptionRecord.ExceptionInformation[0]; - auto exceptionInfo1 = exc.exceptionRecord.ExceptionInformation[1]; - - if (!exceptionInfo0) - returnString << "Attempted to read from: 0x" << (void*)exceptionInfo1; - else if (exceptionInfo0 == 1) - returnString << "Attempted to write to: 0x" << (void*)exceptionInfo1; - else if (exceptionInfo0 == 8) - returnString << "Data Execution Prevention (DEP) at: 0x" << (void*)std::hex << exceptionInfo1; - else - returnString << "Unknown access violation at: 0x" << (void*)exceptionInfo1; - return returnString.str(); - } - return name; -} - -// Custom formatter for the Xmm registers -template <> struct fmt::formatter : fmt::formatter -{ - template auto format(const M128A& obj, FormatContext& ctx) - { - // Masking the top and bottom half of the long long - int v1 = obj.Low & INT_MAX; - int v2 = obj.Low >> 32; - int v3 = obj.High & INT_MAX; - int v4 = obj.High >> 32; - return fmt::format_to( - ctx.out(), - "[ {:G}, {:G}, {:G}, {:G}], [ 0x{:x}, 0x{:x}, 0x{:x}, 0x{:x} ]", - *reinterpret_cast(&v1), - *reinterpret_cast(&v2), - *reinterpret_cast(&v3), - *reinterpret_cast(&v4), - v1, - v2, - v3, - v4); - } -}; - -void GenerateTrace(ExceptionLog& exc, bool skipErrorHandlingFrames = true, int numSkipFrames = 0) -{ - - MODULEINFO crashedModuleInfo; - GetModuleInformation(GetCurrentProcess(), exc.crashedModule, &crashedModuleInfo, sizeof(crashedModuleInfo)); - - char crashedModuleFullName[MAX_PATH]; - GetModuleFileNameExA(GetCurrentProcess(), exc.crashedModule, crashedModuleFullName, MAX_PATH); - char* crashedModuleName = strrchr(crashedModuleFullName, '\\') + 1; - - DWORD64 crashedModuleOffset = ((DWORD64)exc.exceptionRecord.ExceptionAddress) - ((DWORD64)crashedModuleInfo.lpBaseOfDll); - - PVOID framesToCapture[62]; - int frames = RtlCaptureStackBackTrace(0, 62, framesToCapture, NULL); - bool haveSkippedErrorHandlingFrames = false; - - for (int i = 0; i < frames; i++) - { - - HMODULE backtraceModuleHandle; - GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, static_cast(framesToCapture[i]), &backtraceModuleHandle); - - char backtraceModuleFullName[MAX_PATH]; - GetModuleFileNameExA(GetCurrentProcess(), backtraceModuleHandle, backtraceModuleFullName, MAX_PATH); - char* backtraceModuleName = strrchr(backtraceModuleFullName, '\\') + 1; - - if (!haveSkippedErrorHandlingFrames) - { - if (!strncmp(backtraceModuleFullName, crashedModuleFullName, MAX_PATH) && - !strncmp(backtraceModuleName, crashedModuleName, MAX_PATH)) - { - haveSkippedErrorHandlingFrames = true; - } - else - { - continue; - } - } - - if (numSkipFrames > 0) - { - numSkipFrames--; - continue; - } - - void* actualAddress = (void*)framesToCapture[i]; - void* relativeAddress = (void*)(uintptr_t(actualAddress) - uintptr_t(backtraceModuleHandle)); - std::string s_moduleName {backtraceModuleName}; - std::string s_relativeAddress {fmt::format("{}", relativeAddress)}; - // These are used for formatting later on - if (s_moduleName.length() > exc.longestModuleNameLength) - { - exc.longestModuleNameLength = s_moduleName.length(); - } - if (s_relativeAddress.length() > exc.longestRelativeAddressLength) - { - exc.longestRelativeAddressLength = s_relativeAddress.length(); - } - - exc.trace.push_back(BacktraceModule {s_moduleName, s_relativeAddress, fmt::format("{}", actualAddress)}); - } - - CONTEXT* exceptionContext = &exc.contextRecord; - - exc.registerDump.push_back(fmt::format("Flags: 0b{0:b}", exceptionContext->ContextFlags)); - exc.registerDump.push_back(fmt::format("RIP: 0x{0:x}", exceptionContext->Rip)); - exc.registerDump.push_back(fmt::format("CS : 0x{0:x}", exceptionContext->SegCs)); - exc.registerDump.push_back(fmt::format("DS : 0x{0:x}", exceptionContext->SegDs)); - exc.registerDump.push_back(fmt::format("ES : 0x{0:x}", exceptionContext->SegEs)); - exc.registerDump.push_back(fmt::format("SS : 0x{0:x}", exceptionContext->SegSs)); - exc.registerDump.push_back(fmt::format("FS : 0x{0:x}", exceptionContext->SegFs)); - exc.registerDump.push_back(fmt::format("GS : 0x{0:x}", exceptionContext->SegGs)); - - exc.registerDump.push_back(fmt::format("RAX: 0x{0:x}", exceptionContext->Rax)); - exc.registerDump.push_back(fmt::format("RBX: 0x{0:x}", exceptionContext->Rbx)); - exc.registerDump.push_back(fmt::format("RCX: 0x{0:x}", exceptionContext->Rcx)); - exc.registerDump.push_back(fmt::format("RDX: 0x{0:x}", exceptionContext->Rdx)); - exc.registerDump.push_back(fmt::format("RSI: 0x{0:x}", exceptionContext->Rsi)); - exc.registerDump.push_back(fmt::format("RDI: 0x{0:x}", exceptionContext->Rdi)); - exc.registerDump.push_back(fmt::format("RBP: 0x{0:x}", exceptionContext->Rbp)); - exc.registerDump.push_back(fmt::format("RSP: 0x{0:x}", exceptionContext->Rsp)); - exc.registerDump.push_back(fmt::format("R8 : 0x{0:x}", exceptionContext->R8)); - exc.registerDump.push_back(fmt::format("R9 : 0x{0:x}", exceptionContext->R9)); - exc.registerDump.push_back(fmt::format("R10: 0x{0:x}", exceptionContext->R10)); - exc.registerDump.push_back(fmt::format("R11: 0x{0:x}", exceptionContext->R11)); - exc.registerDump.push_back(fmt::format("R12: 0x{0:x}", exceptionContext->R12)); - exc.registerDump.push_back(fmt::format("R13: 0x{0:x}", exceptionContext->R13)); - exc.registerDump.push_back(fmt::format("R14: 0x{0:x}", exceptionContext->R14)); - exc.registerDump.push_back(fmt::format("R15: 0x{0:x}", exceptionContext->R15)); - - exc.registerDump.push_back(fmt::format("Xmm0 : {}", exceptionContext->Xmm0)); - exc.registerDump.push_back(fmt::format("Xmm1 : {}", exceptionContext->Xmm1)); - exc.registerDump.push_back(fmt::format("Xmm2 : {}", exceptionContext->Xmm2)); - exc.registerDump.push_back(fmt::format("Xmm3 : {}", exceptionContext->Xmm3)); - exc.registerDump.push_back(fmt::format("Xmm4 : {}", exceptionContext->Xmm4)); - exc.registerDump.push_back(fmt::format("Xmm5 : {}", exceptionContext->Xmm5)); - exc.registerDump.push_back(fmt::format("Xmm6 : {}", exceptionContext->Xmm6)); - exc.registerDump.push_back(fmt::format("Xmm7 : {}", exceptionContext->Xmm7)); - exc.registerDump.push_back(fmt::format("Xmm8 : {}", exceptionContext->Xmm8)); - exc.registerDump.push_back(fmt::format("Xmm9 : {}", exceptionContext->Xmm9)); - exc.registerDump.push_back(fmt::format("Xmm10: {}", exceptionContext->Xmm10)); - exc.registerDump.push_back(fmt::format("Xmm11: {}", exceptionContext->Xmm11)); - exc.registerDump.push_back(fmt::format("Xmm12: {}", exceptionContext->Xmm12)); - exc.registerDump.push_back(fmt::format("Xmm13: {}", exceptionContext->Xmm13)); - exc.registerDump.push_back(fmt::format("Xmm14: {}", exceptionContext->Xmm14)); - exc.registerDump.push_back(fmt::format("Xmm15: {}", exceptionContext->Xmm15)); -} - -void CreateMiniDump(EXCEPTION_POINTERS* exceptionInfo) -{ - time_t time = std::time(nullptr); - tm currentTime = *std::localtime(&time); - std::stringstream stream; - stream << std::put_time(¤tTime, (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()); -} - -long GenerateExceptionLog(EXCEPTION_POINTERS* exceptionInfo) -{ - storedException->exceptionRecord = *exceptionInfo->ExceptionRecord; - storedException->contextRecord = *exceptionInfo->ContextRecord; - const DWORD exceptionCode = exceptionInfo->ExceptionRecord->ExceptionCode; - - void* exceptionAddress = exceptionInfo->ExceptionRecord->ExceptionAddress; - - storedException->cause = GetExceptionName(*storedException); - - HMODULE crashedModuleHandle; - GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, static_cast(exceptionAddress), &crashedModuleHandle); - - storedException->crashedModule = crashedModuleHandle; - - // When encountering a runtime exception, we store the exception to be displayed later - // We then have to return EXCEPTION_CONTINUE_SEARCH so that our runtime handler may be called - // This might possibly cause some issues if client and server are crashing at the same time, but honestly i don't care - if (exceptionCode == RUNTIME_EXCEPTION) - { - GenerateTrace(*storedException, false, 2); - storedException = storedException; - return EXCEPTION_CONTINUE_SEARCH; - } - - GenerateTrace(*storedException, true, 0); - CreateMiniDump(exceptionInfo); - PrintExceptionLog(*storedException); - return EXCEPTION_EXECUTE_HANDLER; -} - -long __stdcall ExceptionFilter(EXCEPTION_POINTERS* exceptionInfo) -{ - if (!IsDebuggerPresent()) - { - // Check if we are capable of handling this type of exception - if (ExceptionNames.find(exceptionInfo->ExceptionRecord->ExceptionCode) == ExceptionNames.end()) - return EXCEPTION_CONTINUE_SEARCH; - if (exceptionInfo->ExceptionRecord->ExceptionCode == RUNTIME_EXCEPTION) - { - storedException = std::make_shared(); - storedException->exceptionRecord = *exceptionInfo->ExceptionRecord; - storedException->contextRecord = *exceptionInfo->ContextRecord; - } - else - { - CreateMiniDump(exceptionInfo); - return GenerateExceptionLog(exceptionInfo); - } - } - - return EXCEPTION_EXECUTE_HANDLER; -} - -void RuntimeExceptionHandler() -{ - auto ptr = std::current_exception(); - if (ptr) - { - try - { - // This is to generate an actual std::exception object that we can then inspect - std::rethrow_exception(ptr); - } - catch (std::exception& e) - { - storedException->runtimeInfo = e.what(); - } - catch (...) - { - storedException->runtimeInfo = "Unknown runtime exception type"; - } - EXCEPTION_POINTERS test {}; - test.ContextRecord = &storedException->contextRecord; - test.ExceptionRecord = &storedException->exceptionRecord; - CreateMiniDump(&test); - GenerateExceptionLog(&test); - PrintExceptionLog(*storedException); - exit(-1); - } - else - { - spdlog::error( - "std::current_exception() returned nullptr while being handled by RuntimeExceptionHandler. This should never happen!"); - std::abort(); - } -} - -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); - std::set_terminate(RuntimeExceptionHandler); -} - -void RemoveCrashHandler() -{ - RemoveVectoredExceptionHandler(hExceptionFilter); -} diff --git a/NorthstarDLL/crashhandler.h b/NorthstarDLL/crashhandler.h deleted file mode 100644 index 4d8a59ce..00000000 --- a/NorthstarDLL/crashhandler.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -void InitialiseCrashHandler(); -void RemoveCrashHandler(); - -struct BacktraceModule -{ - std::string name; - std::string relativeAddress; - std::string address; -}; - -struct ExceptionLog -{ - std::string cause; - HMODULE crashedModule; - EXCEPTION_RECORD exceptionRecord; - CONTEXT contextRecord; - std::vector trace; - std::vector registerDump; - - std::string runtimeInfo; - - int longestModuleNameLength; - int longestRelativeAddressLength; -}; diff --git a/NorthstarDLL/cvar.cpp b/NorthstarDLL/cvar.cpp deleted file mode 100644 index 787790be..00000000 --- a/NorthstarDLL/cvar.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "pch.h" -#include "cvar.h" -#include "convar.h" -#include "concommand.h" - -//----------------------------------------------------------------------------- -// Purpose: returns all ConVars -//----------------------------------------------------------------------------- -std::unordered_map CCvar::DumpToMap() -{ - std::stringstream ss; - CCVarIteratorInternal* itint = FactoryInternalIterator(); // Allocate new InternalIterator. - - std::unordered_map allConVars; - - for (itint->SetFirst(); itint->IsValid(); itint->Next()) // Loop through all instances. - { - ConCommandBase* pCommand = itint->Get(); - const char* pszCommandName = pCommand->m_pszName; - allConVars[pszCommandName] = pCommand; - } - - return allConVars; -} - -// use the R2 namespace for game funcs -namespace R2 -{ - SourceInterface* g_pCVarInterface; - CCvar* g_pCVar; -} // namespace R2 diff --git a/NorthstarDLL/cvar.h b/NorthstarDLL/cvar.h deleted file mode 100644 index e65e5145..00000000 --- a/NorthstarDLL/cvar.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once -#include "convar.h" -#include "pch.h" - -//----------------------------------------------------------------------------- -// Forward declarations -//----------------------------------------------------------------------------- -class ConCommandBase; -class ConCommand; -class ConVar; - -//----------------------------------------------------------------------------- -// Internals for ICVarIterator -//----------------------------------------------------------------------------- -class CCVarIteratorInternal // Fully reversed table, just look at the virtual function table and rename the function. -{ - public: - virtual void SetFirst(void) = 0; // 0 - virtual void Next(void) = 0; // 1 - virtual bool IsValid(void) = 0; // 2 - virtual ConCommandBase* Get(void) = 0; // 3 -}; - -//----------------------------------------------------------------------------- -// Default implementation -//----------------------------------------------------------------------------- -class CCvar -{ - public: - M_VMETHOD(ConCommandBase*, FindCommandBase, 14, (const char* pszCommandName), (this, pszCommandName)); - M_VMETHOD(ConVar*, FindVar, 16, (const char* pszVarName), (this, pszVarName)); - M_VMETHOD(ConCommand*, FindCommand, 18, (const char* pszCommandName), (this, pszCommandName)); - M_VMETHOD(CCVarIteratorInternal*, FactoryInternalIterator, 41, (), (this)); - - std::unordered_map DumpToMap(); -}; - -// use the R2 namespace for game funcs -namespace R2 -{ - extern SourceInterface* g_pCVarInterface; - extern CCvar* g_pCVar; -} // namespace R2 diff --git a/NorthstarDLL/debugoverlay.cpp b/NorthstarDLL/debugoverlay.cpp deleted file mode 100644 index ef456867..00000000 --- a/NorthstarDLL/debugoverlay.cpp +++ /dev/null @@ -1,141 +0,0 @@ -#include "pch.h" -#include "dedicated.h" -#include "cvar.h" -#include "vector.h" - -AUTOHOOK_INIT() - -enum OverlayType_t -{ - OVERLAY_BOX = 0, - OVERLAY_SPHERE, - OVERLAY_LINE, - OVERLAY_TRIANGLE, - OVERLAY_SWEPT_BOX, - OVERLAY_BOX2, - OVERLAY_CAPSULE -}; - -struct OverlayBase_t -{ - OverlayBase_t() - { - m_Type = OVERLAY_BOX; - m_nServerCount = -1; - m_nCreationTick = -1; - m_flEndTime = 0.0f; - m_pNextOverlay = NULL; - } - - OverlayType_t m_Type; // What type of overlay is it? - int m_nCreationTick; // Duration -1 means go away after this frame # - int m_nServerCount; // Latch server count, too - float m_flEndTime; // When does this box go away - OverlayBase_t* m_pNextOverlay; - void* m_pUnk; -}; - -struct OverlayLine_t : public OverlayBase_t -{ - OverlayLine_t() - { - m_Type = OVERLAY_LINE; - } - - Vector3 origin; - Vector3 dest; - int r; - int g; - int b; - int a; - bool noDepthTest; -}; - -struct OverlayBox_t : public OverlayBase_t -{ - OverlayBox_t() - { - m_Type = OVERLAY_BOX; - } - - Vector3 origin; - Vector3 mins; - Vector3 maxs; - QAngle angles; - int r; - int g; - int b; - int a; -}; - -static HMODULE sEngineModule; - -typedef void (*RenderLineType)(Vector3 v1, Vector3 v2, Color c, bool bZBuffer); -static RenderLineType RenderLine; -typedef void (*RenderBoxType)(Vector3 vOrigin, QAngle angles, Vector3 vMins, Vector3 vMaxs, Color c, bool bZBuffer, bool bInsideOut); -static RenderBoxType RenderBox; -static RenderBoxType RenderWireframeBox; - -// clang-format off -AUTOHOOK(DrawOverlay, engine.dll + 0xABCB0, -void, __fastcall, (OverlayBase_t * pOverlay)) -// clang-format on -{ - EnterCriticalSection((LPCRITICAL_SECTION)((char*)sEngineModule + 0x10DB0A38)); // s_OverlayMutex - - void* pMaterialSystem = *(void**)((char*)sEngineModule + 0x14C675B0); - - switch (pOverlay->m_Type) - { - case OVERLAY_LINE: - { - OverlayLine_t* pLine = static_cast(pOverlay); - RenderLine(pLine->origin, pLine->dest, Color(pLine->r, pLine->g, pLine->b, pLine->a), pLine->noDepthTest); - } - break; - case OVERLAY_BOX: - { - OverlayBox_t* pCurrBox = static_cast(pOverlay); - if (pCurrBox->a > 0) - { - RenderBox( - pCurrBox->origin, - pCurrBox->angles, - pCurrBox->mins, - pCurrBox->maxs, - Color(pCurrBox->r, pCurrBox->g, pCurrBox->b, pCurrBox->a), - false, - false); - } - if (pCurrBox->a < 255) - { - RenderWireframeBox( - pCurrBox->origin, - pCurrBox->angles, - pCurrBox->mins, - pCurrBox->maxs, - Color(pCurrBox->r, pCurrBox->g, pCurrBox->b, 255), - false, - false); - } - } - break; - } - LeaveCriticalSection((LPCRITICAL_SECTION)((char*)sEngineModule + 0x10DB0A38)); -} - -ON_DLL_LOAD_CLIENT_RELIESON("engine.dll", DebugOverlay, ConVar, (CModule module)) -{ - AUTOHOOK_DISPATCH() - - RenderLine = module.Offset(0x192A70).As(); - RenderBox = module.Offset(0x192520).As(); - RenderWireframeBox = module.Offset(0x193DA0).As(); - sEngineModule = reinterpret_cast(module.m_nAddress); - - // not in g_pCVar->FindVar by this point for whatever reason, so have to get from memory - ConVar* Cvar_enable_debug_overlays = module.Offset(0x10DB0990).As(); - Cvar_enable_debug_overlays->SetValue(false); - Cvar_enable_debug_overlays->m_pszDefaultValue = (char*)"0"; - Cvar_enable_debug_overlays->AddFlags(FCVAR_CHEAT); -} diff --git a/NorthstarDLL/dedicated.cpp b/NorthstarDLL/dedicated.cpp deleted file mode 100644 index 7ba62d24..00000000 --- a/NorthstarDLL/dedicated.cpp +++ /dev/null @@ -1,295 +0,0 @@ -#include "pch.h" -#include "dedicated.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() -{ - static bool result = strstr(GetCommandLineA(), "-dedicated"); - return result; -} - -// CDedidcatedExports defs -struct CDedicatedExports; // forward declare - -typedef void (*DedicatedSys_PrintfType)(CDedicatedExports* dedicated, const char* msg); -typedef void (*DedicatedRunServerType)(CDedicatedExports* dedicated); - -// would've liked to just do this as a class but have not been able to get it to work -struct CDedicatedExports -{ - void* vtable; // because it's easier, we just set this to &this, since CDedicatedExports has no props we care about other than funcs - - char unused[56]; - - DedicatedSys_PrintfType Sys_Printf; - DedicatedRunServerType RunServer; -}; - -void Sys_Printf(CDedicatedExports* dedicated, const char* msg) -{ - spdlog::info("[DEDICATED SERVER] {}", msg); -} - -void RunServer(CDedicatedExports* dedicated) -{ - spdlog::info("CDedicatedExports::RunServer(): starting"); - 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 (!Tier0::CommandLine()->CheckParm("+map")) - Tier0::CommandLine()->AppendParm("+map", g_pCVar->FindVar("match_defaultMap")->GetString()); - - // re-run commandline - Cbuf_AddText(Cbuf_GetCurrentPlayer(), "stuffcmds", cmd_source_t::kCommandSrcCode); - Cbuf_Execute(); - - // get tickinterval - ConVar* Cvar_base_tickinterval_mp = g_pCVar->FindVar("base_tickinterval_mp"); - - // main loop - double frameTitle = 0; - while (g_pEngine->m_nQuitting == EngineQuitState::QUIT_NOTQUITTING) - { - double frameStart = Tier0::Plat_FloatTime(); - g_pEngine->Frame(); - - std::this_thread::sleep_for(std::chrono::duration>( - Cvar_base_tickinterval_mp->GetFloat() - fmin(Tier0::Plat_FloatTime() - frameStart, 0.25))); - } -} - -// use server presence to update window title -class DedicatedConsoleServerPresence : public ServerPresenceReporter -{ - 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) - Sleep(1000); - - // Bind stdin to receive console input. - FILE* fp = nullptr; - freopen_s(&fp, "CONIN$", "r", stdin); - - spdlog::info("Ready to receive console commands."); - - { - // Process console input - std::string input; - while (g_pEngine && g_pEngine->m_nQuitting == EngineQuitState::QUIT_NOTQUITTING && std::getline(std::cin, input)) - { - 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; -} - -// 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"); - - AUTOHOOK_DISPATCH_MODULE(engine.dll) - - // 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 - MemoryAddress base = module.Offset(0x1C4EBD); - - // cmp => mov - base.Offset(1).Patch("C6 87"); - - // 00 => 01 - base.Offset(7).Patch("01"); - } - - // 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 - module.Offset(0x159819).NOP(17); - - 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); - module.Offset(0x156B4C).Offset(15).NOP(9); - - // HostState_State_NewGame - // nop an access violation - module.Offset(0xB934C).NOP(9); - - // CEngineAPI::Connect - // remove call to Shader_Connect - module.Offset(0x1C4D7D).NOP(5); - - // Host_Init - // remove call to ui loading stuff - module.Offset(0x156595).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 - module.Offset(0x15A0BB).NOP(5); - - // RunFrameServer - // nop a function that access violations - module.Offset(0x159BF3).NOP(5); - - // func that checks if origin is inited - // always return 1 - module.Offset(0x183B70).Patch("B0 01 C3"); // mov al,01 ret - - // HostState_State_ChangeLevel - // nop clientinterface call - module.Offset(0x1552ED).NOP(16); - - // HostState_State_ChangeLevel - // nop clientinterface call - module.Offset(0x155363).NOP(16); - - // 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; - - *module.Offset(0x13F0B668).As() = dedicatedExports; - - // extra potential patches: - // nop engine.dll+1c67d1 and +1c67d8 to skip videomode creategamewindow - // also look into launcher.dll+d381, seems to cause renderthread to get made - // this crashes HARD if no window which makes sense tbh - // also look into materialsystem + 5B344 since it seems to be the base of all the renderthread stuff - - // big note: datatable gets registered in window creation - // make sure it still gets registered - - // add cmdline args that are good for dedi - 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"); - Tier0::CommandLine()->AppendParm("+community_frame_run", "0"); - - // 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 (!Tier0::CommandLine()->CheckParm("-bringbackquickedit")) - { - HANDLE stdIn = GetStdHandle(STD_INPUT_HANDLE); - DWORD mode = 0; - - if (GetConsoleMode(stdIn, &mode)) - { - if (mode & ENABLE_QUICK_EDIT_MODE) - { - mode &= ~ENABLE_QUICK_EDIT_MODE; - mode &= ~ENABLE_MOUSE_INPUT; - - mode |= ENABLE_PROCESSED_INPUT; - - SetConsoleMode(stdIn, mode); - } - } - } - else - spdlog::info("Quick Edit enabled by user request"); - - // create console input thread - if (!Tier0::CommandLine()->CheckParm("-noconsoleinput")) - consoleInputThreadHandle = CreateThread(0, 0, ConsoleInputThread, 0, 0, NULL); - else - spdlog::info("Console input disabled by user request"); -} - -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 - module.GetExport("Tier0_InitOrigin").Patch("C3"); -} - -// clang-format off -AUTOHOOK(PrintSquirrelError, server.dll + 0x794D0, -void, __fastcall, (void* sqvm)) -// clang-format on -{ - PrintSquirrelError(sqvm); - - // close dedicated server if a fatal error is hit - // atm, this will crash if not aborted, so this just closes more gracefully - static ConVar* Cvar_fatal_script_errors = g_pCVar->FindVar("fatal_script_errors"); - if (Cvar_fatal_script_errors->GetBool()) - abort(); -} - -ON_DLL_LOAD_DEDI("server.dll", DedicatedServerGameDLL, (CModule module)) -{ - 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 - } -} diff --git a/NorthstarDLL/dedicated.h b/NorthstarDLL/dedicated.h deleted file mode 100644 index 82806763..00000000 --- a/NorthstarDLL/dedicated.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once - -bool IsDedicatedServer(); diff --git a/NorthstarDLL/dedicated/dedicated.cpp b/NorthstarDLL/dedicated/dedicated.cpp new file mode 100644 index 00000000..33a3f034 --- /dev/null +++ b/NorthstarDLL/dedicated/dedicated.cpp @@ -0,0 +1,295 @@ +#include "pch.h" +#include "dedicated.h" +#include "core/tier0.h" +#include "playlist.h" +#include "engine/r2engine.h" +#include "engine/hoststate.h" +#include "server/auth/serverauthentication.h" +#include "masterserver/masterserver.h" +#include "util/printcommands.h" + +AUTOHOOK_INIT() + +using namespace R2; + +bool IsDedicatedServer() +{ + static bool result = strstr(GetCommandLineA(), "-dedicated"); + return result; +} + +// CDedidcatedExports defs +struct CDedicatedExports; // forward declare + +typedef void (*DedicatedSys_PrintfType)(CDedicatedExports* dedicated, const char* msg); +typedef void (*DedicatedRunServerType)(CDedicatedExports* dedicated); + +// would've liked to just do this as a class but have not been able to get it to work +struct CDedicatedExports +{ + void* vtable; // because it's easier, we just set this to &this, since CDedicatedExports has no props we care about other than funcs + + char unused[56]; + + DedicatedSys_PrintfType Sys_Printf; + DedicatedRunServerType RunServer; +}; + +void Sys_Printf(CDedicatedExports* dedicated, const char* msg) +{ + spdlog::info("[DEDICATED SERVER] {}", msg); +} + +void RunServer(CDedicatedExports* dedicated) +{ + spdlog::info("CDedicatedExports::RunServer(): starting"); + 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 (!Tier0::CommandLine()->CheckParm("+map")) + Tier0::CommandLine()->AppendParm("+map", g_pCVar->FindVar("match_defaultMap")->GetString()); + + // re-run commandline + Cbuf_AddText(Cbuf_GetCurrentPlayer(), "stuffcmds", cmd_source_t::kCommandSrcCode); + Cbuf_Execute(); + + // get tickinterval + ConVar* Cvar_base_tickinterval_mp = g_pCVar->FindVar("base_tickinterval_mp"); + + // main loop + double frameTitle = 0; + while (g_pEngine->m_nQuitting == EngineQuitState::QUIT_NOTQUITTING) + { + double frameStart = Tier0::Plat_FloatTime(); + g_pEngine->Frame(); + + std::this_thread::sleep_for(std::chrono::duration>( + Cvar_base_tickinterval_mp->GetFloat() - fmin(Tier0::Plat_FloatTime() - frameStart, 0.25))); + } +} + +// use server presence to update window title +class DedicatedConsoleServerPresence : public ServerPresenceReporter +{ + 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) + Sleep(1000); + + // Bind stdin to receive console input. + FILE* fp = nullptr; + freopen_s(&fp, "CONIN$", "r", stdin); + + spdlog::info("Ready to receive console commands."); + + { + // Process console input + std::string input; + while (g_pEngine && g_pEngine->m_nQuitting == EngineQuitState::QUIT_NOTQUITTING && std::getline(std::cin, input)) + { + 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; +} + +// 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"); + + AUTOHOOK_DISPATCH_MODULE(engine.dll) + + // 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 + MemoryAddress base = module.Offset(0x1C4EBD); + + // cmp => mov + base.Offset(1).Patch("C6 87"); + + // 00 => 01 + base.Offset(7).Patch("01"); + } + + // 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 + module.Offset(0x159819).NOP(17); + + 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); + module.Offset(0x156B4C).Offset(15).NOP(9); + + // HostState_State_NewGame + // nop an access violation + module.Offset(0xB934C).NOP(9); + + // CEngineAPI::Connect + // remove call to Shader_Connect + module.Offset(0x1C4D7D).NOP(5); + + // Host_Init + // remove call to ui loading stuff + module.Offset(0x156595).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 + module.Offset(0x15A0BB).NOP(5); + + // RunFrameServer + // nop a function that access violations + module.Offset(0x159BF3).NOP(5); + + // func that checks if origin is inited + // always return 1 + module.Offset(0x183B70).Patch("B0 01 C3"); // mov al,01 ret + + // HostState_State_ChangeLevel + // nop clientinterface call + module.Offset(0x1552ED).NOP(16); + + // HostState_State_ChangeLevel + // nop clientinterface call + module.Offset(0x155363).NOP(16); + + // 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; + + *module.Offset(0x13F0B668).As() = dedicatedExports; + + // extra potential patches: + // nop engine.dll+1c67d1 and +1c67d8 to skip videomode creategamewindow + // also look into launcher.dll+d381, seems to cause renderthread to get made + // this crashes HARD if no window which makes sense tbh + // also look into materialsystem + 5B344 since it seems to be the base of all the renderthread stuff + + // big note: datatable gets registered in window creation + // make sure it still gets registered + + // add cmdline args that are good for dedi + 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"); + Tier0::CommandLine()->AppendParm("+community_frame_run", "0"); + + // 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 (!Tier0::CommandLine()->CheckParm("-bringbackquickedit")) + { + HANDLE stdIn = GetStdHandle(STD_INPUT_HANDLE); + DWORD mode = 0; + + if (GetConsoleMode(stdIn, &mode)) + { + if (mode & ENABLE_QUICK_EDIT_MODE) + { + mode &= ~ENABLE_QUICK_EDIT_MODE; + mode &= ~ENABLE_MOUSE_INPUT; + + mode |= ENABLE_PROCESSED_INPUT; + + SetConsoleMode(stdIn, mode); + } + } + } + else + spdlog::info("Quick Edit enabled by user request"); + + // create console input thread + if (!Tier0::CommandLine()->CheckParm("-noconsoleinput")) + consoleInputThreadHandle = CreateThread(0, 0, ConsoleInputThread, 0, 0, NULL); + else + spdlog::info("Console input disabled by user request"); +} + +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 + module.GetExport("Tier0_InitOrigin").Patch("C3"); +} + +// clang-format off +AUTOHOOK(PrintSquirrelError, server.dll + 0x794D0, +void, __fastcall, (void* sqvm)) +// clang-format on +{ + PrintSquirrelError(sqvm); + + // close dedicated server if a fatal error is hit + // atm, this will crash if not aborted, so this just closes more gracefully + static ConVar* Cvar_fatal_script_errors = g_pCVar->FindVar("fatal_script_errors"); + if (Cvar_fatal_script_errors->GetBool()) + abort(); +} + +ON_DLL_LOAD_DEDI("server.dll", DedicatedServerGameDLL, (CModule module)) +{ + 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 + } +} diff --git a/NorthstarDLL/dedicated/dedicated.h b/NorthstarDLL/dedicated/dedicated.h new file mode 100644 index 00000000..82806763 --- /dev/null +++ b/NorthstarDLL/dedicated/dedicated.h @@ -0,0 +1,3 @@ +#pragma once + +bool IsDedicatedServer(); diff --git a/NorthstarDLL/dedicated/dedicatedmaterialsystem.cpp b/NorthstarDLL/dedicated/dedicatedmaterialsystem.cpp new file mode 100644 index 00000000..37b74576 --- /dev/null +++ b/NorthstarDLL/dedicated/dedicatedmaterialsystem.cpp @@ -0,0 +1,41 @@ +#include "pch.h" +#include "dedicated.h" +#include "core/tier0.h" + +AUTOHOOK_INIT() + +// clang-format off +AUTOHOOK(D3D11CreateDevice, materialsystem_dx11.dll + 0xD9A0E, +HRESULT, __stdcall, ( + void* pAdapter, + int DriverType, + HMODULE Software, + UINT Flags, + int* pFeatureLevels, + UINT FeatureLevels, + UINT SDKVersion, + void** ppDevice, + int* pFeatureLevel, + 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 + + // 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 (Tier0::CommandLine()->CheckParm("-softwared3d11")) + DriverType = 5; // D3D_DRIVER_TYPE_WARP + + return D3D11CreateDevice( + pAdapter, DriverType, Software, Flags, pFeatureLevels, FeatureLevels, SDKVersion, ppDevice, pFeatureLevel, ppImmediateContext); +} + +ON_DLL_LOAD_DEDI("materialsystem_dx11.dll", DedicatedServerMaterialSystem, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + // CMaterialSystem::FindMaterial + // make the game always use the error material + module.Offset(0x5F0F1).Patch("E9 34 03 00"); +} diff --git a/NorthstarDLL/dedicatedmaterialsystem.cpp b/NorthstarDLL/dedicatedmaterialsystem.cpp deleted file mode 100644 index 28ee9b76..00000000 --- a/NorthstarDLL/dedicatedmaterialsystem.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include "pch.h" -#include "dedicated.h" -#include "tier0.h" - -AUTOHOOK_INIT() - -// clang-format off -AUTOHOOK(D3D11CreateDevice, materialsystem_dx11.dll + 0xD9A0E, -HRESULT, __stdcall, ( - void* pAdapter, - int DriverType, - HMODULE Software, - UINT Flags, - int* pFeatureLevels, - UINT FeatureLevels, - UINT SDKVersion, - void** ppDevice, - int* pFeatureLevel, - 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 - - // 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 (Tier0::CommandLine()->CheckParm("-softwared3d11")) - DriverType = 5; // D3D_DRIVER_TYPE_WARP - - return D3D11CreateDevice( - pAdapter, DriverType, Software, Flags, pFeatureLevels, FeatureLevels, SDKVersion, ppDevice, pFeatureLevel, ppImmediateContext); -} - -ON_DLL_LOAD_DEDI("materialsystem_dx11.dll", DedicatedServerMaterialSystem, (CModule module)) -{ - AUTOHOOK_DISPATCH() - - // CMaterialSystem::FindMaterial - // make the game always use the error material - module.Offset(0x5F0F1).Patch("E9 34 03 00"); -} diff --git a/NorthstarDLL/demofixes.cpp b/NorthstarDLL/demofixes.cpp deleted file mode 100644 index ab092484..00000000 --- a/NorthstarDLL/demofixes.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#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); -} diff --git a/NorthstarDLL/diskvmtfixes.cpp b/NorthstarDLL/diskvmtfixes.cpp deleted file mode 100644 index f5f3e2cd..00000000 --- a/NorthstarDLL/diskvmtfixes.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "pch.h" - -ON_DLL_LOAD_CLIENT("materialsystem_dx11.dll", DiskVMTFixes, (CModule module)) -{ - // in retail VMTs will never load if cache read is invalid due to a special case for them in KeyValues::LoadFromFile - // this effectively makes it impossible to load them from mods because we invalidate cache for doing this - // so uhh, stop that from happening - - // tbh idk why they even changed any of this what's the point it looks like it works fine who cares my god - - // matsystem KeyValues::LoadFromFile: patch special case on cache read failure for vmts - module.Offset(0x1281B9).Patch("EB"); - - // CMaterialSystem::FindMaterial: don't call function that crashes if previous patch is applied - module.Offset(0x5F55A).NOP(5); -} diff --git a/NorthstarDLL/dllmain.cpp b/NorthstarDLL/dllmain.cpp index 4a9da4af..cd52940d 100644 --- a/NorthstarDLL/dllmain.cpp +++ b/NorthstarDLL/dllmain.cpp @@ -1,14 +1,13 @@ #include "pch.h" -#include "main.h" -#include "logging.h" -#include "crashhandler.h" -#include "memalloc.h" -#include "nsprefix.h" -#include "plugin_abi.h" -#include "plugins.h" -#include "version.h" -#include "pch.h" -#include "squirrel.h" +#include "dllmain.h" +#include "logging/logging.h" +#include "logging/crashhandler.h" +#include "core/memalloc.h" +#include "config/profile.h" +#include "plugins/plugin_abi.h" +#include "plugins/plugins.h" +#include "util/version.h" +#include "squirrel/squirrel.h" #include "rapidjson/document.h" #include "rapidjson/stringbuffer.h" diff --git a/NorthstarDLL/dllmain.h b/NorthstarDLL/dllmain.h new file mode 100644 index 00000000..412f1e25 --- /dev/null +++ b/NorthstarDLL/dllmain.h @@ -0,0 +1,4 @@ +#pragma once + +extern "C" __declspec(dllexport) bool InitialiseNorthstar(); +extern "C" __declspec(dllexport) bool LoadPlugins(); diff --git a/NorthstarDLL/engine/host.cpp b/NorthstarDLL/engine/host.cpp new file mode 100644 index 00000000..a55c8dfd --- /dev/null +++ b/NorthstarDLL/engine/host.cpp @@ -0,0 +1,31 @@ +#include "pch.h" +#include "core/convar/convar.h" +#include "mods/modmanager.h" +#include "util/printcommands.h" +#include "util/printmaps.h" +#include "shared/misccommands.h" +#include "r2engine.h" +#include "core/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() +} diff --git a/NorthstarDLL/engine/hoststate.cpp b/NorthstarDLL/engine/hoststate.cpp new file mode 100644 index 00000000..f2318c78 --- /dev/null +++ b/NorthstarDLL/engine/hoststate.cpp @@ -0,0 +1,124 @@ +#include "pch.h" +#include "engine/hoststate.h" +#include "masterserver/masterserver.h" +#include "server/auth/serverauthentication.h" +#include "server/serverpresence.h" +#include "shared/playlist.h" +#include "core/tier0.h" +#include "engine/r2engine.h" +#include "shared/exploit_fixes/ns_limits.h" +#include "squirrel/squirrel.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"); + + 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); + } + + // Run Squirrel message buffer + if (g_pSquirrel->m_pSQVM != nullptr && g_pSquirrel->m_pSQVM->sqvm != nullptr) + g_pSquirrel->ProcessMessageBuffer(); + + if (g_pSquirrel->m_pSQVM != nullptr && g_pSquirrel->m_pSQVM->sqvm != nullptr) + g_pSquirrel->ProcessMessageBuffer(); + + if (g_pSquirrel->m_pSQVM != nullptr && g_pSquirrel->m_pSQVM->sqvm != nullptr) + g_pSquirrel->ProcessMessageBuffer(); +} + +ON_DLL_LOAD_RELIESON("engine.dll", HostState, ConVar, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + g_pHostState = module.Offset(0x7CF180).As(); + Cvar_hostport = module.Offset(0x13FA6070).As(); +} diff --git a/NorthstarDLL/engine/hoststate.h b/NorthstarDLL/engine/hoststate.h new file mode 100644 index 00000000..a77385ef --- /dev/null +++ b/NorthstarDLL/engine/hoststate.h @@ -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 diff --git a/NorthstarDLL/engine/r2engine.cpp b/NorthstarDLL/engine/r2engine.cpp new file mode 100644 index 00000000..11233a2d --- /dev/null +++ b/NorthstarDLL/engine/r2engine.cpp @@ -0,0 +1,36 @@ +#include "pch.h" +#include "r2engine.h" + +using namespace R2; + +// use the R2 namespace for game funcs +namespace R2 +{ + Cbuf_GetCurrentPlayerType Cbuf_GetCurrentPlayer; + Cbuf_AddTextType Cbuf_AddText; + Cbuf_ExecuteType Cbuf_Execute; + + CEngine* g_pEngine; + + void (*CBaseClient__Disconnect)(void* self, uint32_t unknownButAlways1, const char* reason, ...); + CBaseClient* g_pClientArray; + + server_state_t* g_pServerState; + + char* g_pModName = + nullptr; // we cant set this up here atm since we dont have an offset to it in engine, instead we store it in IsRespawnMod +} // namespace R2 + +ON_DLL_LOAD("engine.dll", R2Engine, (CModule module)) +{ + Cbuf_GetCurrentPlayer = module.Offset(0x120630).As(); + Cbuf_AddText = module.Offset(0x1203B0).As(); + Cbuf_Execute = module.Offset(0x1204B0).As(); + + g_pEngine = module.Offset(0x7D70C8).Deref().As(); + + CBaseClient__Disconnect = module.Offset(0x1012C0).As(); + g_pClientArray = module.Offset(0x12A53F90).As(); + + g_pServerState = module.Offset(0x12A53D48).As(); +} diff --git a/NorthstarDLL/engine/r2engine.h b/NorthstarDLL/engine/r2engine.h new file mode 100644 index 00000000..68b762e1 --- /dev/null +++ b/NorthstarDLL/engine/r2engine.h @@ -0,0 +1,185 @@ +#pragma once +#include "shared/keyvalues.h" + +// use the R2 namespace for game funcs +namespace R2 +{ + // Cbuf + 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_GAMEDLL_FOR_REMOTE_CLIENTS. + 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; + + 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; + + // CEngine + + enum EngineQuitState + { + QUIT_NOTQUITTING = 0, + QUIT_TODESKTOP, + QUIT_RESTART + }; + + enum class 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; + + extern void (*CBaseClient__Disconnect)(void* self, uint32_t unknownButAlways1, const char* reason, ...); + +#pragma once + typedef enum + { + NA_NULL = 0, + NA_LOOPBACK, + NA_IP, + } netadrtype_t; + +#pragma pack(push, 1) + typedef struct netadr_s + { + netadrtype_t type; + unsigned char ip[16]; // IPv6 + // IPv4's 127.0.0.1 is [::ffff:127.0.0.1], that is: + // 00 00 00 00 00 00 00 00 00 00 FF FF 7F 00 00 01 + unsigned short port; + } netadr_t; +#pragma pack(pop) + +#pragma pack(push, 1) + typedef struct netpacket_s + { + netadr_t adr; // sender address + // int source; // received source + char unk[10]; + double received_time; + unsigned char* data; // pointer to raw packet data + void* message; // easy bitbuf data access // 'inpacket.message' etc etc (pointer) + char unk2[16]; + int size; + + // bf_read message; // easy bitbuf data access // 'inpacket.message' etc etc (pointer) + // int size; // size in bytes + // int wiresize; // size in bytes before decompression + // bool stream; // was send as stream + // struct netpacket_s* pNext; // for internal use, should be NULL in public + } netpacket_t; +#pragma pack(pop) + + // #56169 $DB69 PData size + // #512 $200 Trailing data + // #100 $64 Safety buffer + const int PERSISTENCE_MAX_SIZE = 0xDDCD; + + // note: NOT_READY and READY are the only entries we have here that are defined by the vanilla game + // entries after this are custom and used to determine the source of persistence, e.g. whether it is local or remote + enum class ePersistenceReady : char + { + NOT_READY, + READY = 3, + READY_INSECURE = 3, + READY_REMOTE + }; + + // clang-format off + OFFSET_STRUCT(CBaseClient) + { + STRUCT_SIZE(0x2D728) + FIELD(0x16, char m_Name[64]) + FIELD(0x258, KeyValues* m_ConVars) + FIELD(0x4A0, ePersistenceReady m_iPersistenceReady) + FIELD(0x4FA, char m_PersistenceBuffer[PERSISTENCE_MAX_SIZE]) + FIELD(0xF500, char m_UID[32]) + }; + // clang-format on + + extern CBaseClient* g_pClientArray; + + enum server_state_t + { + ss_dead = 0, // Dead + ss_loading, // Spawning + ss_active, // Running + ss_paused, // Running, but paused + }; + + extern server_state_t* g_pServerState; + + extern char* g_pModName; +} // namespace R2 diff --git a/NorthstarDLL/engine/runframe.cpp b/NorthstarDLL/engine/runframe.cpp new file mode 100644 index 00000000..d81356f2 --- /dev/null +++ b/NorthstarDLL/engine/runframe.cpp @@ -0,0 +1,18 @@ +#include "pch.h" +#include "engine/r2engine.h" +#include "server/r2server.h" +#include "hoststate.h" +#include "server/serverpresence.h" + +AUTOHOOK_INIT() +// clang-format off +AUTOHOOK(CEngine__Frame, engine.dll + 0x1C8650, +void, __fastcall, (R2::CEngine* self)) +// clang-format on +{ + CEngine__Frame(self); +} +ON_DLL_LOAD("engine.dll", RunFrame, (CModule module)) +{ + AUTOHOOK_DISPATCH() +} diff --git a/NorthstarDLL/exploitfixes.cpp b/NorthstarDLL/exploitfixes.cpp deleted file mode 100644 index 240c352c..00000000 --- a/NorthstarDLL/exploitfixes.cpp +++ /dev/null @@ -1,458 +0,0 @@ -#include "pch.h" -#include "cvar.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; - -#define BLOCKED_INFO(s) \ - ( \ - [=]() -> bool \ - { \ - if (Cvar_ns_exploitfixes_log->GetBool()) \ - { \ - std::stringstream stream; \ - stream << "ExploitFixes.cpp: " << BLOCK_PREFIX << s; \ - spdlog::error(stream.str()); \ - } \ - return false; \ - }()) - -// block bad netmessages -// Servers can literally request a screenshot from any client, yeah no -// 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; -} - -// 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; -} - -// 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; - struct SetConVarEntry - { - char name[ENTRY_STR_LEN]; - char val[ENTRY_STR_LEN]; - }; - - struct NET_SetConVar - { - void* vtable; - void* unk1; - void* unk2; - void* m_pMessageHandler; - SetConVarEntry* m_ConVars; // convar entry array - void* unk5; // these 2 unks are just vector capacity or whatever - void* unk6; - int m_ConVars_count; // amount of cvar entries in array (this will not be out of bounds) - }; - - auto msg = (NET_SetConVar*)pMsg; - bool bIsServerFrame = Tier0::ThreadInServerFrameThread(); - - std::string BLOCK_PREFIX = - std::string {"NET_SetConVar ("} + (bIsServerFrame ? "server" : "client") + "): Blocked dangerous/invalid msg: "; - - if (bIsServerFrame) - { - constexpr int SETCONVAR_SANITY_AMOUNT_LIMIT = 69; - if (msg->m_ConVars_count < 1 || msg->m_ConVars_count > SETCONVAR_SANITY_AMOUNT_LIMIT) - { - return BLOCKED_INFO("Invalid m_ConVars_count (" << msg->m_ConVars_count << ")"); - } - } - - for (int i = 0; i < msg->m_ConVars_count; i++) - { - auto entry = msg->m_ConVars + i; - - // Safety check for memory access - if (MemoryAddress(entry).IsMemoryReadable(sizeof(*entry))) - { - // Find null terminators - bool nameValid = false, valValid = false; - for (int i = 0; i < ENTRY_STR_LEN; i++) - { - if (!entry->name[i]) - nameValid = true; - if (!entry->val[i]) - valValid = true; - } - - if (!nameValid || !valValid) - return BLOCKED_INFO("Missing null terminators"); - - ConVar* pVar = R2::g_pCVar->FindVar(entry->name); - - if (pVar) - { - memcpy( - entry->name, - pVar->m_ConCommandBase.m_pszName, - strlen(pVar->m_ConCommandBase.m_pszName) + 1); // Force name to match case - - int iFlags = bIsServerFrame ? FCVAR_USERINFO : FCVAR_REPLICATED; - if (!pVar->IsFlagSet(iFlags)) - return BLOCKED_INFO( - "Invalid flags (" << std::hex << "0x" << pVar->m_ConCommandBase.m_nFlags << "), var is " << entry->name); - } - } - else - { - return BLOCKED_INFO("Unreadable memory at " << (void*)entry); // Not risking that one, they all gotta be readable - } - } - - return CClient_ProcessSetConVar(msg); -} - -// 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 - { - BYTE gap0[24]; - void* m_pMessageHandler; - int m_nBackupCommands; - int m_nNewCommands; - int m_nLength; - // bf_read m_DataIn; - // bf_write m_DataOut; - }; - - auto msg = (CLC_Move*)pMsg; - - const char* BLOCK_PREFIX = "ProcessUserCmds: "; - - if (msg->m_nBackupCommands < 0) - { - return BLOCKED_INFO("Invalid m_nBackupCommands (" << msg->m_nBackupCommands << ")"); - } - - if (msg->m_nNewCommands < 0) - { - return BLOCKED_INFO("Invalid m_nNewCommands (" << msg->m_nNewCommands << ")"); - } - - if (msg->m_nLength <= 0) - return BLOCKED_INFO("Invalid message length (" << msg->m_nLength << ")"); - - return CClient_ProcessUsercmds(thisptr, pMsg); -} - -// 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 - 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 - { - 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; - }; - - auto cmd = (SV_CUserCmd*)pCmd_move; - auto fromCmd = (SV_CUserCmd*)pCmd_from; - - std::string BLOCK_PREFIX = - "ReadUsercmd (command_number delta: " + std::to_string(cmd->command_number - fromCmd->command_number) + "): "; - - // fix invalid player angles - cmd->worldViewAngles.MakeValid(); - cmd->attackangles.MakeValid(); - cmd->localViewAngles.MakeValid(); - - // Fix invalid camera angles - cmd->cameraPos.MakeValid(); - cmd->cameraAngles.MakeValid(); - - // Fix invaid movement vector - cmd->move.MakeValid(); - - 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 - << ", commandTime : " << cmd->command_time << ")"); - goto INVALID_CMD; // No simulation of bogus-timed cmds - } - - 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}; - cmd->tick_count = cmd->frameTime = 0; - cmd->move = cmd->cameraPos = {0, 0, 0}; - cmd->buttons = 0; - cmd->meleetarget = 0; -} - -// 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 -{ - // 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"); -} - -// 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); - - if (!g_pServerLimits->CheckStringCommandLimits(self)) - { - 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; - } - - // 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()) - { - 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() - }; - - int iCmdLength = strlen(tempCommand.Arg(0)); - - bool bIsBadCommand = false; - for (auto& blockedCommand : blockedCommands) - { - if (iCmdLength != strlen(blockedCommand)) - continue; - - 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 - - // this is a command we need to block - return false; - NEXT_COMMAND:; - } - } - - return CGameClient__ExecuteStringCommand(self, unknown, pCommandString); -} - -// 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"); -} - -// 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 (i >= MAX_ENT_IDX) - { - spdlog::warn("GetEntByIndex {} is out of bounds (max {})", i, MAX_ENT_IDX); - return nullptr; - } - - 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; - }; - - CEntityReadInfo* pReadInfo = (CEntityReadInfo*)a1; - if (pReadInfo->nNewEntity >= 0x1000 || pReadInfo->nNewEntity < 0) - { - // 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; - } - - return CL_CopyExistingEntity(a1); -} - -ON_DLL_LOAD("engine.dll", EngineExploitFixes, (CModule module)) -{ - AUTOHOOK_DISPATCH_MODULE(engine.dll) - - CCommand__Tokenize = module.Offset(0x418380).As(); - - // 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) - { - // Just return, none of them have any args or are userpurge - exportAddr.Patch("C3"); - spdlog::info("Patched AntiTamper function export \"{}\"", exportName); - } - } - - 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"); -} diff --git a/NorthstarDLL/exploitfixes_lzss.cpp b/NorthstarDLL/exploitfixes_lzss.cpp deleted file mode 100644 index 4205133a..00000000 --- a/NorthstarDLL/exploitfixes_lzss.cpp +++ /dev/null @@ -1,79 +0,0 @@ -#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() -} diff --git a/NorthstarDLL/exploitfixes_utf8parser.cpp b/NorthstarDLL/exploitfixes_utf8parser.cpp deleted file mode 100644 index e2510765..00000000 --- a/NorthstarDLL/exploitfixes_utf8parser.cpp +++ /dev/null @@ -1,200 +0,0 @@ -#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(); -} diff --git a/NorthstarDLL/filesystem.cpp b/NorthstarDLL/filesystem.cpp deleted file mode 100644 index 6ac6cc43..00000000 --- a/NorthstarDLL/filesystem.cpp +++ /dev/null @@ -1,186 +0,0 @@ -#include "pch.h" -#include "filesystem.h" -#include "sourceinterface.h" -#include "modmanager.h" - -#include -#include - -AUTOHOOK_INIT() - -using namespace R2; - -bool bReadingOriginalFile = false; -std::string sCurrentModPath; - -ConVar* Cvar_ns_fs_log_reads; - -// use the R2 namespace for game funcs -namespace R2 -{ - SourceInterface* g_pFilesystem; - - std::string ReadVPKFile(const char* path) - { - // 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); - - 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)); - - (*g_pFilesystem)->m_vtable2->Close(*g_pFilesystem, fileHandle); - - 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 -{ - AddSearchPath(fileSystem, pPath, pathID, addType); - - // 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) -{ - // put our new path to the head if we need to read from a different mod path - // 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->m_ModDirectory) / MOD_OVERRIDE_DIR).string().compare(sCurrentModPath)) - { - NS::log::fs->info("Changing mod search path from {} to {}", sCurrentModPath, mod->m_ModDirectory.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 - AddSearchPath(&*(*g_pFilesystem), fs::absolute(GetCompiledAssetsPath()).string().c_str(), "GAME", PATH_ADD_TO_HEAD); -} - -bool TryReplaceFile(const char* pPath, bool shouldCompile) -{ - if (bReadingOriginalFile) - return false; - - if (shouldCompile) - 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_pModManager->m_ModFiles.find(g_pModManager->NormaliseModFilePath(fs::path(pPath))); - if (file != g_pModManager->m_ModFiles.end()) - { - SetNewModSearchPaths(file->second.m_pOwningMod); - return true; - } - - return false; -} - -// 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 -{ - if (TryReplaceFile(pPath, true)) - return false; - - 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); -} - -// 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 -{ - TryReplaceFile(pPath, true); - return CBaseFileSystem__OpenEx(filesystem, pPath, pOptions, flags, pPathID, ppszResolvedFilename); -} - -HOOK(MountVPKHook, MountVPK, VPKData*, , (IFileSystem * fileSystem, const char* pVpkPath)) -{ - NS::log::fs->info("MountVPK {}", pVpkPath); - VPKData* ret = MountVPK(fileSystem, pVpkPath); - - for (Mod mod : g_pModManager->m_LoadedMods) - { - if (!mod.m_bEnabled) - continue; - - for (ModVPKEntry& vpkEntry : mod.Vpks) - { - // if we're autoloading, just load no matter what - if (!vpkEntry.m_bAutoLoad) - { - // 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(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()); - if (!ret) // this is primarily for map vpks and stuff, so the map's vpk is what gets returned from here - ret = loaded; - } - } - - return ret; -} - -ON_DLL_LOAD("filesystem_stdio.dll", Filesystem, (CModule module)) -{ - AUTOHOOK_DISPATCH() - - R2::g_pFilesystem = new SourceInterface("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); -} diff --git a/NorthstarDLL/filesystem.h b/NorthstarDLL/filesystem.h deleted file mode 100644 index 9a22893a..00000000 --- a/NorthstarDLL/filesystem.h +++ /dev/null @@ -1,85 +0,0 @@ -#pragma once -#include "sourceinterface.h" - -// taken from ttf2sdk -typedef void* FileHandle_t; - -#pragma pack(push, 1) - -// clang-format off -OFFSET_STRUCT(VPKFileEntry) -{ - STRUCT_SIZE(0x44); - FIELDS(0x0, - char* directory; - char* filename; - char* extension; - ) -}; -// clang-format on -#pragma pack(pop) - -#pragma pack(push, 1) -// clang-format off -struct VPKData -{ - STRUCT_SIZE(0x50); - FIELDS(0x0, - char* directory; - char* filename; - char* extension; - ) -}; -// clang-format on -#pragma pack(pop) - -enum SearchPathAdd_t -{ - PATH_ADD_TO_HEAD, // First path searched - PATH_ADD_TO_TAIL, // Last path searched -}; - -class CSearchPath -{ - public: - unsigned char unknown[0x18]; - const char* debugPath; -}; - -class IFileSystem -{ - public: - struct VTable - { - void* unknown[10]; - void (*AddSearchPath)(IFileSystem* fileSystem, const char* pPath, const char* pathID, SearchPathAdd_t addType); - void* unknown2[84]; - bool (*ReadFromCache)(IFileSystem* fileSystem, const char* path, void* result); - void* unknown3[15]; - VPKData* (*MountVPK)(IFileSystem* fileSystem, const char* vpkPath); - }; - - struct VTable2 - { - int (*Read)(IFileSystem::VTable2** fileSystem, void* pOutput, int size, FileHandle_t file); - void* unknown[1]; - 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); - 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); - }; - - VTable* m_vtable; - VTable2* m_vtable2; -}; - -// use the R2 namespace for game funcs -namespace R2 -{ - extern SourceInterface* g_pFilesystem; - - std::string ReadVPKFile(const char* path); - std::string ReadVPKOriginalFile(const char* path); -} // namespace R2 diff --git a/NorthstarDLL/hooks.cpp b/NorthstarDLL/hooks.cpp deleted file mode 100644 index cc1c284d..00000000 --- a/NorthstarDLL/hooks.cpp +++ /dev/null @@ -1,426 +0,0 @@ -#include "pch.h" -#include "dedicated.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -AUTOHOOK_INIT() - -// called from the ON_DLL_LOAD macros -__dllLoadCallback::__dllLoadCallback( - eDllLoadCallbackSide side, const std::string dllName, DllLoadCallbackFuncType callback, std::string uniqueStr, std::string reliesOn) -{ - // parse reliesOn array from string - std::vector reliesOnArray; - - 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; - } - } -} - -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 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& GetDllLoadCallbacks() -{ - static std::vector vec = std::vector(); - return vec; -} - -void AddDllLoadCallback(std::string dll, DllLoadCallbackFuncType callback, std::string tag, std::vector 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 reliesOn) -{ - if (!IsDedicatedServer()) - return; - - AddDllLoadCallback(dll, callback, tag, reliesOn); -} - -void AddDllLoadCallbackForClient(std::string dll, DllLoadCallbackFuncType callback, std::string tag, std::vector 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 = _GetCommandLineA(); - bool isDedi = strstr(cmdlineOrg, "-dedicated"); // well, this one has to be a real argument - bool ignoreStartupArgs = strstr(cmdlineOrg, "-nostartupargs"); - - std::string args; - std::ifstream cmdlineArgFile; - - // it looks like CommandLine() prioritizes parameters apprearing first, so we want the real commandline to take priority - // not to mention that cmdlineOrg starts with the EXE path - args.append(cmdlineOrg); - args.append(" "); - - // append those from the file - - if (!ignoreStartupArgs) - { - - cmdlineArgFile = std::ifstream(!isDedi ? "ns_startup_args.txt" : "ns_startup_args_dedi.txt"); - - if (cmdlineArgFile) - { - std::stringstream argBuffer; - argBuffer << cmdlineArgFile.rdbuf(); - cmdlineArgFile.close(); - - // if some other command line option includes "-northstar" in the future then you have to refactor this check to check with - // both either space after or ending with - if (!isDedi && argBuffer.str().find("-northstar") != std::string::npos) - MessageBoxA( - NULL, - "The \"-northstar\" command line option is NOT supposed to go into ns_startup_args.txt file!\n\nThis option is " - "supposed to go into Origin/Steam game launch options, and then you are supposed to launch the original " - "Titanfall2.exe " - "rather than NorthstarLauncher.exe to make use of it.", - "Northstar Warning", - MB_ICONWARNING); - - args.append(argBuffer.str()); - } - } - - auto len = args.length(); - cmdlineModified = new char[len + 1]; - if (!cmdlineModified) - { - spdlog::error("malloc failed for command line"); - return cmdlineOrg; - } - memcpy(cmdlineModified, args.c_str(), len + 1); - - spdlog::info("Command line: {}", cmdlineModified); - } - - return cmdlineModified; -} - -std::vector calledTags; -void CallLoadLibraryACallbacks(LPCSTR lpLibFileName, HMODULE moduleAddress) -{ - CModule cModule(moduleAddress); - - while (true) - { - bool bDoneCalling = true; - - for (auto& callbackStruct : GetDllLoadCallbacks()) - { - 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) -{ - CModule cModule(moduleAddress); - - while (true) - { - bool bDoneCalling = true; - - for (auto& callbackStruct : GetDllLoadCallbacks()) - { - 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 CallAllPendingDLLLoadCallbacks() -{ - HMODULE hMods[1024]; - HANDLE hProcess = GetCurrentProcess(); - DWORD cbNeeded; - unsigned int i; - - // Get a list of all the modules in this process. - if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) - { - for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) - { - wchar_t szModName[MAX_PATH]; - - // Get the full path to the module's file. - if (GetModuleFileNameExW(hProcess, hMods[i], szModName, sizeof(szModName) / sizeof(TCHAR))) - { - CallLoadLibraryWCallbacks(szModName, hMods[i]); - } - } - } -} - -// 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)) - { - moduleAddress = _LoadLibraryExA("XInput9_1_0.dll", hFile, dwFlags); - - if (!moduleAddress) - { - MessageBoxA(0, "Could not find XInput9_1_0.dll", "Northstar", MB_ICONERROR); - exit(EXIT_FAILURE); - - return nullptr; - } - } - else - moduleAddress = _LoadLibraryExA(lpLibFileName, hFile, dwFlags); - - if (moduleAddress) - CallLoadLibraryACallbacks(lpLibFileName, moduleAddress); - - return moduleAddress; -} - -// clang-format off -AUTOHOOK_ABSOLUTEADDR(_LoadLibraryA, LoadLibraryA, -HMODULE, WINAPI, (LPCSTR lpLibFileName)) -// clang-format on -{ - HMODULE moduleAddress = _LoadLibraryA(lpLibFileName); - - if (moduleAddress) - CallLoadLibraryACallbacks(lpLibFileName, moduleAddress); - - return moduleAddress; -} - -// clang-format off -AUTOHOOK_ABSOLUTEADDR(_LoadLibraryExW, LoadLibraryExW, -HMODULE, WINAPI, (LPCWSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)) -// clang-format on -{ - 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() -} diff --git a/NorthstarDLL/hooks.h b/NorthstarDLL/hooks.h deleted file mode 100644 index f47791fb..00000000 --- a/NorthstarDLL/hooks.h +++ /dev/null @@ -1,311 +0,0 @@ -#pragma once -#include "memory.h" - -#include -#include - -void InstallInitialHooks(); - -typedef void (*DllLoadCallbackFuncType)(CModule moduleAddress); -void AddDllLoadCallback(std::string dll, DllLoadCallbackFuncType callback, std::string tag = "", std::vector reliesOn = {}); -void AddDllLoadCallbackForDedicatedServer( - std::string dll, DllLoadCallbackFuncType callback, std::string tag = "", std::vector reliesOn = {}); -void AddDllLoadCallbackForClient( - std::string dll, DllLoadCallbackFuncType callback, std::string tag = "", std::vector 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)) diff --git a/NorthstarDLL/host.cpp b/NorthstarDLL/host.cpp deleted file mode 100644 index 87b1ce4e..00000000 --- a/NorthstarDLL/host.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#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() -} diff --git a/NorthstarDLL/hoststate.cpp b/NorthstarDLL/hoststate.cpp deleted file mode 100644 index 24e4bf63..00000000 --- a/NorthstarDLL/hoststate.cpp +++ /dev/null @@ -1,124 +0,0 @@ -#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" -#include "squirrel.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"); - - 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); - } - - // Run Squirrel message buffer - if (g_pSquirrel->m_pSQVM != nullptr && g_pSquirrel->m_pSQVM->sqvm != nullptr) - g_pSquirrel->ProcessMessageBuffer(); - - if (g_pSquirrel->m_pSQVM != nullptr && g_pSquirrel->m_pSQVM->sqvm != nullptr) - g_pSquirrel->ProcessMessageBuffer(); - - if (g_pSquirrel->m_pSQVM != nullptr && g_pSquirrel->m_pSQVM->sqvm != nullptr) - g_pSquirrel->ProcessMessageBuffer(); -} - -ON_DLL_LOAD_RELIESON("engine.dll", HostState, ConVar, (CModule module)) -{ - AUTOHOOK_DISPATCH() - - g_pHostState = module.Offset(0x7CF180).As(); - Cvar_hostport = module.Offset(0x13FA6070).As(); -} diff --git a/NorthstarDLL/hoststate.h b/NorthstarDLL/hoststate.h deleted file mode 100644 index a77385ef..00000000 --- a/NorthstarDLL/hoststate.h +++ /dev/null @@ -1,45 +0,0 @@ -#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 diff --git a/NorthstarDLL/httprequesthandler.cpp b/NorthstarDLL/httprequesthandler.cpp deleted file mode 100644 index 2681b4a3..00000000 --- a/NorthstarDLL/httprequesthandler.cpp +++ /dev/null @@ -1,586 +0,0 @@ -#include "pch.h" -#include "httprequesthandler.h" -#include "version.h" -#include "squirrel.h" -#include "tier0.h" - -HttpRequestHandler* g_httpRequestHandler; - -bool IsHttpDisabled() -{ - const static bool bIsHttpDisabled = Tier0::CommandLine()->FindParm("-disablehttprequests"); - return bIsHttpDisabled; -} - -bool IsLocalHttpAllowed() -{ - const static bool bIsLocalHttpAllowed = Tier0::CommandLine()->FindParm("-allowlocalhttp"); - return bIsLocalHttpAllowed; -} - -bool DisableHttpSsl() -{ - const static bool bDisableHttpSsl = Tier0::CommandLine()->FindParm("-disablehttpssl"); - return bDisableHttpSsl; -} - -HttpRequestHandler::HttpRequestHandler() -{ - // Cache the launch parameters as early as possible in order to avoid possible exploits that change them at runtime. - IsHttpDisabled(); - IsLocalHttpAllowed(); - DisableHttpSsl(); -} - -void HttpRequestHandler::StartHttpRequestHandler() -{ - if (IsRunning()) - { - spdlog::warn("%s was called while IsRunning() is true!", __FUNCTION__); - return; - } - - m_bIsHttpRequestHandlerRunning = true; - spdlog::info("HttpRequestHandler started."); -} - -void HttpRequestHandler::StopHttpRequestHandler() -{ - if (!IsRunning()) - { - spdlog::warn("%s was called while IsRunning() is false", __FUNCTION__); - return; - } - - m_bIsHttpRequestHandlerRunning = false; - spdlog::info("HttpRequestHandler stopped."); -} - -bool IsHttpDestinationHostAllowed(const std::string& host, std::string& outHostname, std::string& outAddress, std::string& outPort) -{ - CURLU* url = curl_url(); - if (!url) - { - spdlog::error("Failed to call curl_url() for http request."); - return false; - } - - if (curl_url_set(url, CURLUPART_URL, host.c_str(), CURLU_DEFAULT_SCHEME) != CURLUE_OK) - { - spdlog::error("Failed to parse destination URL for http request."); - - curl_url_cleanup(url); - return false; - } - - char* urlHostname = nullptr; - if (curl_url_get(url, CURLUPART_HOST, &urlHostname, 0) != CURLUE_OK) - { - spdlog::error("Failed to parse hostname from destination URL for http request."); - - curl_url_cleanup(url); - return false; - } - - char* urlScheme = nullptr; - if (curl_url_get(url, CURLUPART_SCHEME, &urlScheme, CURLU_DEFAULT_SCHEME) != CURLUE_OK) - { - spdlog::error("Failed to parse scheme from destination URL for http request."); - - curl_url_cleanup(url); - curl_free(urlHostname); - return false; - } - - char* urlPort = nullptr; - if (curl_url_get(url, CURLUPART_PORT, &urlPort, CURLU_DEFAULT_PORT) != CURLUE_OK) - { - spdlog::error("Failed to parse port from destination URL for http request."); - - curl_url_cleanup(url); - curl_free(urlHostname); - curl_free(urlScheme); - return false; - } - - // Resolve the hostname into an address. - addrinfo* result; - addrinfo hints; - std::memset(&hints, 0, sizeof(addrinfo)); - hints.ai_family = AF_UNSPEC; - - if (getaddrinfo(urlHostname, urlScheme, &hints, &result) != 0) - { - spdlog::error("Failed to resolve http request destination {} using getaddrinfo().", urlHostname); - - curl_url_cleanup(url); - curl_free(urlHostname); - curl_free(urlScheme); - curl_free(urlPort); - return false; - } - - bool bFoundIPv6 = false; - sockaddr_in* sockaddr_ipv4 = nullptr; - for (addrinfo* info = result; info; info = info->ai_next) - { - if (info->ai_family == AF_INET) - { - sockaddr_ipv4 = (sockaddr_in*)info->ai_addr; - break; - } - - bFoundIPv6 = bFoundIPv6 || info->ai_family == AF_INET6; - } - - if (sockaddr_ipv4 == nullptr) - { - if (bFoundIPv6) - { - spdlog::error("Only IPv4 destinations are supported for HTTP requests. To allow IPv6, launch the game using -allowlocalhttp."); - } - else - { - spdlog::error("Failed to resolve http request destination {} into a valid IPv4 address.", urlHostname); - } - - curl_free(urlHostname); - curl_free(urlScheme); - curl_free(urlPort); - curl_url_cleanup(url); - - return false; - } - - // Fast checks for private ranges of IPv4. - // clang-format off - { - auto addrBytes = sockaddr_ipv4->sin_addr.S_un.S_un_b; - - if (addrBytes.s_b1 == 10 // 10.0.0.0 - 10.255.255.255 (Class A Private) - || addrBytes.s_b1 == 172 && addrBytes.s_b2 >= 16 && addrBytes.s_b2 <= 31 // 172.16.0.0 - 172.31.255.255 (Class B Private) - || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 168 // 192.168.0.0 - 192.168.255.255 (Class C Private) - || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 0 && addrBytes.s_b3 == 0 // 192.0.0.0 - 192.0.0.255 (IETF Assignment) - || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 0 && addrBytes.s_b3 == 2 // 192.0.2.0 - 192.0.2.255 (TEST-NET-1) - || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 88 && addrBytes.s_b3 == 99 // 192.88.99.0 - 192.88.99.255 (IPv4-IPv6 Relay) - || addrBytes.s_b1 == 192 && addrBytes.s_b2 >= 18 && addrBytes.s_b2 <= 19 // 192.18.0.0 - 192.19.255.255 (Internet Benchmark) - || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 51 && addrBytes.s_b3 == 100 // 192.51.100.0 - 192.51.100.255 (TEST-NET-2) - || addrBytes.s_b1 == 203 && addrBytes.s_b2 == 0 && addrBytes.s_b3 == 113 // 203.0.113.0 - 203.0.113.255 (TEST-NET-3) - || addrBytes.s_b1 == 169 && addrBytes.s_b2 == 254 // 169.254.00 - 169.254.255.255 (Link-local/APIPA) - || addrBytes.s_b1 == 127 // 127.0.0.0 - 127.255.255.255 (Loopback) - || addrBytes.s_b1 == 0 // 0.0.0.0 - 0.255.255.255 (Current network) - || addrBytes.s_b1 == 100 && addrBytes.s_b2 >= 64 && addrBytes.s_b2 <= 127 // 100.64.0.0 - 100.127.255.255 (Shared address space) - || sockaddr_ipv4->sin_addr.S_un.S_addr == 0xFFFFFFFF // 255.255.255.255 (Broadcast) - || addrBytes.s_b1 >= 224 && addrBytes.s_b2 <= 239 // 224.0.0.0 - 239.255.255.255 (Multicast) - || addrBytes.s_b1 == 233 && addrBytes.s_b2 == 252 && addrBytes.s_b3 == 0 // 233.252.0.0 - 233.252.0.255 (MCAST-TEST-NET) - || addrBytes.s_b1 >= 240 && addrBytes.s_b4 <= 254) // 240.0.0.0 - 255.255.255.254 (Future Use Class E) - { - curl_free(urlHostname); - curl_free(urlScheme); - curl_free(urlPort); - curl_url_cleanup(url); - - return false; - } - } - - // clang-format on - - char resolvedStr[INET_ADDRSTRLEN]; - inet_ntop(AF_INET, &sockaddr_ipv4->sin_addr, resolvedStr, INET_ADDRSTRLEN); - - // Use the resolved address as the new request host. - outHostname = urlHostname; - outAddress = resolvedStr; - outPort = urlPort; - - freeaddrinfo(result); - - curl_free(urlHostname); - curl_free(urlScheme); - curl_free(urlPort); - curl_url_cleanup(url); - - return true; -} - -size_t HttpCurlWriteToStringBufferCallback(char* contents, size_t size, size_t nmemb, void* userp) -{ - ((std::string*)userp)->append((char*)contents, size * nmemb); - return size * nmemb; -} - -template int HttpRequestHandler::MakeHttpRequest(const HttpRequest& requestParameters) -{ - if (!IsRunning()) - { - spdlog::warn("%s was called while IsRunning() is false!", __FUNCTION__); - return -1; - } - - if (IsHttpDisabled()) - { - spdlog::warn("NS_InternalMakeHttpRequest called while the game is running with -disablehttprequests." - " Please check if requests are allowed using NSIsHttpEnabled() first."); - return -1; - } - - bool bAllowLocalHttp = IsLocalHttpAllowed(); - - // This handle will be returned to Squirrel so it can wait for the response and assign a callback for it. - int handle = ++m_iLastRequestHandle; - - std::thread requestThread( - [this, handle, requestParameters, bAllowLocalHttp]() - { - std::string hostname, resolvedAddress, resolvedPort; - - if (!bAllowLocalHttp) - { - if (!IsHttpDestinationHostAllowed(requestParameters.baseUrl, hostname, resolvedAddress, resolvedPort)) - { - spdlog::warn( - "HttpRequestHandler::MakeHttpRequest attempted to make a request to a private network. This is only allowed when " - "running the game with -allowlocalhttp."); - g_pSquirrel->AsyncCall( - "NSHandleFailedHttpRequest", - handle, - (int)0, - "Cannot make HTTP requests to private network hosts without -allowlocalhttp. Check your console for more " - "information."); - return; - } - } - - CURL* curl = curl_easy_init(); - if (!curl) - { - spdlog::error("HttpRequestHandler::MakeHttpRequest failed to init libcurl for request."); - g_pSquirrel->AsyncCall( - "NSHandleFailedHttpRequest", handle, static_cast(CURLE_FAILED_INIT), curl_easy_strerror(CURLE_FAILED_INIT)); - return; - } - - // HEAD has no body. - if (requestParameters.method == HttpRequestMethod::HRM_HEAD) - { - curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); - } - - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, HttpRequestMethod::ToString(requestParameters.method).c_str()); - - // Only resolve to IPv4 if we don't allow private network requests. - curl_slist* host = nullptr; - if (!bAllowLocalHttp) - { - curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); - host = curl_slist_append(host, fmt::format("{}:{}:{}", hostname, resolvedPort, resolvedAddress).c_str()); - curl_easy_setopt(curl, CURLOPT_RESOLVE, host); - } - - // Ensure we only allow HTTP or HTTPS. - curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); - - // Allow redirects - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L); - - // Check if the url already contains a query. - // If so, we'll know to append with & instead of start with ? - std::string queryUrl = requestParameters.baseUrl; - bool bUrlContainsQuery = false; - - // If this fails, just ignore the parsing and trust what the user wants to query. - // Probably will fail but handling it here would be annoying. - CURLU* curlUrl = curl_url(); - if (curlUrl) - { - if (curl_url_set(curlUrl, CURLUPART_URL, queryUrl.c_str(), CURLU_DEFAULT_SCHEME) == CURLUE_OK) - { - char* currentQuery; - if (curl_url_get(curlUrl, CURLUPART_QUERY, ¤tQuery, 0) == CURLUE_OK) - { - if (currentQuery && std::strlen(currentQuery) != 0) - { - bUrlContainsQuery = true; - } - } - - curl_free(currentQuery); - } - - curl_url_cleanup(curlUrl); - } - - // GET requests, or POST-like requests with an empty body, can have query parameters. - // Append them to the base url. - if (HttpRequestMethod::CanHaveQueryParameters(requestParameters.method) && - !HttpRequestMethod::UsesCurlPostOptions(requestParameters.method) || - requestParameters.body.empty()) - { - bool isFirstValue = true; - for (const auto& kv : requestParameters.queryParameters) - { - char* key = curl_easy_escape(curl, kv.first.c_str(), kv.first.length()); - - for (const std::string& queryValue : kv.second) - { - char* value = curl_easy_escape(curl, queryValue.c_str(), queryValue.length()); - - if (isFirstValue && !bUrlContainsQuery) - { - queryUrl.append(fmt::format("?{}={}", key, value)); - isFirstValue = false; - } - else - { - queryUrl.append(fmt::format("&{}={}", key, value)); - } - - curl_free(value); - } - - curl_free(key); - } - } - - // If this method uses POST-like curl options, set those and set the body. - // The body won't be sent if it's empty anyway, meaning the query parameters above, if any, would be. - if (HttpRequestMethod::UsesCurlPostOptions(requestParameters.method)) - { - // Grab the body and set it as a POST field - curl_easy_setopt(curl, CURLOPT_POST, 1L); - - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, requestParameters.body.length()); - curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, requestParameters.body.c_str()); - } - - // Set the full URL for this http request. - curl_easy_setopt(curl, CURLOPT_URL, queryUrl.c_str()); - - std::string bodyBuffer; - std::string headerBuffer; - - // Set up buffers to write the response headers and body. - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, HttpCurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &bodyBuffer); - curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, HttpCurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headerBuffer); - - // Add all the headers for the request. - curl_slist* headers = nullptr; - - // Content-Type header for POST-like requests. - if (HttpRequestMethod::UsesCurlPostOptions(requestParameters.method) && !requestParameters.body.empty()) - { - headers = curl_slist_append(headers, fmt::format("Content-Type: {}", requestParameters.contentType).c_str()); - } - - for (const auto& kv : requestParameters.headers) - { - for (const std::string& headerValue : kv.second) - { - headers = curl_slist_append(headers, fmt::format("{}: {}", kv.first, headerValue).c_str()); - } - } - - if (headers != nullptr) - { - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - } - - // Disable SSL checks if requested by the user. - if (DisableHttpSsl()) - { - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYSTATUS, 0L); - } - - // Enforce the Northstar user agent, unless an override was specified. - if (requestParameters.userAgent.empty()) - { - curl_easy_setopt(curl, CURLOPT_USERAGENT, &NSUserAgent); - } - else - { - curl_easy_setopt(curl, CURLOPT_USERAGENT, requestParameters.userAgent.c_str()); - } - - // Set the timeout for this request. Max 60 seconds so mods can't just spin up native threads all the time. - curl_easy_setopt(curl, CURLOPT_TIMEOUT, std::clamp(requestParameters.timeout, 1, 60)); - - CURLcode result = curl_easy_perform(curl); - if (IsRunning()) - { - if (result == CURLE_OK) - { - // While the curl request is OK, it could return a non success code. - // Squirrel side will handle firing the correct callback. - long httpCode = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); - g_pSquirrel->AsyncCall( - "NSHandleSuccessfulHttpRequest", handle, static_cast(httpCode), bodyBuffer, headerBuffer); - } - else - { - // Pass CURL result code & error. - spdlog::error( - "curl_easy_perform() failed with code {}, error: {}", static_cast(result), curl_easy_strerror(result)); - - // If it's an SSL issue, tell the user they may disable SSL checks using -disablehttpssl. - if (result == CURLE_PEER_FAILED_VERIFICATION || result == CURLE_SSL_CERTPROBLEM || - result == CURLE_SSL_INVALIDCERTSTATUS) - { - spdlog::error("You can try disabling SSL verifications for this issue using the -disablehttpssl launch argument. " - "Keep in mind this is potentially dangerous!"); - } - - g_pSquirrel->AsyncCall( - "NSHandleFailedHttpRequest", handle, static_cast(result), curl_easy_strerror(result)); - } - } - - curl_easy_cleanup(curl); - curl_slist_free_all(headers); - curl_slist_free_all(host); - }); - - requestThread.detach(); - return handle; -} - -template void HttpRequestHandler::RegisterSQFuncs() -{ - g_pSquirrel->AddFuncRegistration( - "int", - "NS_InternalMakeHttpRequest", - "int method, string baseUrl, table > headers, table > queryParams, string contentType, " - "string body, " - "int timeout, string userAgent", - "[Internal use only] Passes the HttpRequest struct fields to be reconstructed in native and used for an http request", - SQ_InternalMakeHttpRequest); - - g_pSquirrel->AddFuncRegistration( - "bool", - "NSIsHttpEnabled", - "", - "Whether or not HTTP requests are enabled. You can opt-out by starting the game with -disablehttprequests.", - SQ_IsHttpEnabled); - - g_pSquirrel->AddFuncRegistration( - "bool", - "NSIsLocalHttpAllowed", - "", - "Whether or not HTTP requests can be made to a private network address. You can enable this by starting the game with " - "-allowlocalhttp.", - SQ_IsLocalHttpAllowed); -} - -// int NS_InternalMakeHttpRequest(int method, string baseUrl, table headers, table queryParams, -// string contentType, string body, int timeout, string userAgent) -template SQRESULT SQ_InternalMakeHttpRequest(HSquirrelVM* sqvm) -{ - if (!g_httpRequestHandler || !g_httpRequestHandler->IsRunning()) - { - spdlog::warn("NS_InternalMakeHttpRequest called while the http request handler isn't running."); - g_pSquirrel->pushinteger(sqvm, -1); - return SQRESULT_NOTNULL; - } - - if (IsHttpDisabled()) - { - spdlog::warn("NS_InternalMakeHttpRequest called while the game is running with -disablehttprequests." - " Please check if requests are allowed using NSIsHttpEnabled() first."); - g_pSquirrel->pushinteger(sqvm, -1); - return SQRESULT_NOTNULL; - } - - HttpRequest request; - request.method = static_cast(g_pSquirrel->getinteger(sqvm, 1)); - request.baseUrl = g_pSquirrel->getstring(sqvm, 2); - - // Read the tables for headers and query parameters. - SQTable* headerTable = sqvm->_stackOfCurrentFunction[3]._VAL.asTable; - for (int idx = 0; idx < headerTable->_numOfNodes; ++idx) - { - tableNode* node = &headerTable->_nodes[idx]; - - if (node->key._Type == OT_STRING && node->val._Type == OT_ARRAY) - { - SQArray* valueArray = node->val._VAL.asArray; - std::vector headerValues; - - for (int vIdx = 0; vIdx < valueArray->_usedSlots; ++vIdx) - { - if (valueArray->_values[vIdx]._Type == OT_STRING) - { - headerValues.push_back(valueArray->_values[vIdx]._VAL.asString->_val); - } - } - - request.headers[node->key._VAL.asString->_val] = headerValues; - } - } - - SQTable* queryTable = sqvm->_stackOfCurrentFunction[4]._VAL.asTable; - for (int idx = 0; idx < queryTable->_numOfNodes; ++idx) - { - tableNode* node = &queryTable->_nodes[idx]; - - if (node->key._Type == OT_STRING && node->val._Type == OT_ARRAY) - { - SQArray* valueArray = node->val._VAL.asArray; - std::vector queryValues; - - for (int vIdx = 0; vIdx < valueArray->_usedSlots; ++vIdx) - { - if (valueArray->_values[vIdx]._Type == OT_STRING) - { - queryValues.push_back(valueArray->_values[vIdx]._VAL.asString->_val); - } - } - - request.queryParameters[node->key._VAL.asString->_val] = queryValues; - } - } - - request.contentType = g_pSquirrel->getstring(sqvm, 5); - request.body = g_pSquirrel->getstring(sqvm, 6); - request.timeout = g_pSquirrel->getinteger(sqvm, 7); - request.userAgent = g_pSquirrel->getstring(sqvm, 8); - - int handle = g_httpRequestHandler->MakeHttpRequest(request); - g_pSquirrel->pushinteger(sqvm, handle); - return SQRESULT_NOTNULL; -} - -// bool NSIsHttpEnabled() -template SQRESULT SQ_IsHttpEnabled(HSquirrelVM* sqvm) -{ - g_pSquirrel->pushbool(sqvm, !IsHttpDisabled()); - return SQRESULT_NOTNULL; -} - -// bool NSIsLocalHttpAllowed() -template SQRESULT SQ_IsLocalHttpAllowed(HSquirrelVM* sqvm) -{ - g_pSquirrel->pushbool(sqvm, IsLocalHttpAllowed()); - return SQRESULT_NOTNULL; -} - -ON_DLL_LOAD_RELIESON("client.dll", HttpRequestHandler_ClientInit, ClientSquirrel, (CModule module)) -{ - g_httpRequestHandler->RegisterSQFuncs(); - g_httpRequestHandler->RegisterSQFuncs(); -} - -ON_DLL_LOAD_RELIESON("server.dll", HttpRequestHandler_ServerInit, ServerSquirrel, (CModule module)) -{ - g_httpRequestHandler->RegisterSQFuncs(); -} - -ON_DLL_LOAD("engine.dll", HttpRequestHandler_Init, (CModule module)) -{ - g_httpRequestHandler = new HttpRequestHandler; - g_httpRequestHandler->StartHttpRequestHandler(); -} diff --git a/NorthstarDLL/httprequesthandler.h b/NorthstarDLL/httprequesthandler.h deleted file mode 100644 index 0f888b6e..00000000 --- a/NorthstarDLL/httprequesthandler.h +++ /dev/null @@ -1,132 +0,0 @@ -#pragma once - -#include "pch.h" - -enum class ScriptContext; - -// These definitions below should match on the Squirrel side so we can easily pass them along through a function. - -/** - * Allowed methods for an HttpRequest. - */ -namespace HttpRequestMethod -{ - enum Type - { - HRM_GET = 0, - HRM_POST = 1, - HRM_HEAD = 2, - HRM_PUT = 3, - HRM_DELETE = 4, - HRM_PATCH = 5, - HRM_OPTIONS = 6, - }; - - /** Returns the HTTP string representation of the given method. */ - inline std::string ToString(HttpRequestMethod::Type method) - { - switch (method) - { - case HttpRequestMethod::HRM_GET: - return "GET"; - case HttpRequestMethod::HRM_POST: - return "POST"; - case HttpRequestMethod::HRM_HEAD: - return "HEAD"; - case HttpRequestMethod::HRM_PUT: - return "PUT"; - case HttpRequestMethod::HRM_DELETE: - return "DELETE"; - case HttpRequestMethod::HRM_PATCH: - return "PATCH"; - case HttpRequestMethod::HRM_OPTIONS: - return "OPTIONS"; - default: - return "INVALID"; - } - } - - /** Whether or not the given method should be treated like a POST for curlopts. */ - bool UsesCurlPostOptions(HttpRequestMethod::Type method) - { - switch (method) - { - case HttpRequestMethod::HRM_POST: - case HttpRequestMethod::HRM_PUT: - case HttpRequestMethod::HRM_DELETE: - case HttpRequestMethod::HRM_PATCH: - return true; - default: - return false; - } - } - - /** Whether or not the given http request method can have query parameters in the URL. */ - bool CanHaveQueryParameters(HttpRequestMethod::Type method) - { - return method == HttpRequestMethod::HRM_GET || UsesCurlPostOptions(method); - } -}; // namespace HttpRequestMethod - -/** Contains data about an http request that has been queued. */ -struct HttpRequest -{ - /** Method used for this http request. */ - HttpRequestMethod::Type method; - - /** Base URL of this http request. */ - std::string baseUrl; - - /** Headers used for this http request. Some may get overridden or ignored. */ - std::unordered_map> headers; - - /** Query parameters for this http request. */ - std::unordered_map> queryParameters; - - /** The content type of this http request. Defaults to text/plain & UTF-8 charset. */ - std::string contentType = "text/plain; charset=utf-8"; - - /** The body of this http request. If set, will override queryParameters.*/ - std::string body; - - /** The timeout for the http request, in seconds. Must be between 1 and 60. */ - int timeout; - - /** If set, the override to use for the User-Agent header. */ - std::string userAgent; -}; - -/** - * Handles making HTTP requests and sending the responses back to Squirrel. - */ -class HttpRequestHandler -{ - public: - HttpRequestHandler(); - - // Start/Stop the HTTP request handler. Right now this doesn't do much. - void StartHttpRequestHandler(); - void StopHttpRequestHandler(); - - // Whether or not this http request handler is currently running. - bool IsRunning() const - { - return m_bIsHttpRequestHandlerRunning; - } - - /** - * Creates a new thread to execute an HTTP request. - * @param requestParameters The parameters to use for this http request. - * @returns The handle for the http request being sent, or -1 if the request failed. - */ - template int MakeHttpRequest(const HttpRequest& requestParameters); - - /** Registers the HTTP request Squirrel functions for the given script context. */ - template void RegisterSQFuncs(); - - private: - int m_iLastRequestHandle = 0; - std::atomic_bool m_bIsHttpRequestHandlerRunning = false; -}; - -extern HttpRequestHandler* g_httpRequestHandler; diff --git a/NorthstarDLL/kb_act.cpp b/NorthstarDLL/kb_act.cpp deleted file mode 100644 index 5fe71cdb..00000000 --- a/NorthstarDLL/kb_act.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "pch.h" -#include "modmanager.h" -#include "filesystem.h" - -#include - -const char* KB_ACT_PATH = "scripts\\kb_act.lst"; - -// compiles the file kb_act.lst, that defines entries for keybindings in the options menu -void ModManager::BuildKBActionsList() -{ - spdlog::info("Building kb_act.lst"); - - fs::create_directories(GetCompiledAssetsPath() / "scripts"); - std::ofstream soCompiledKeys(GetCompiledAssetsPath() / KB_ACT_PATH, std::ios::binary); - - // write vanilla file's content to compiled file - soCompiledKeys << R2::ReadVPKOriginalFile(KB_ACT_PATH); - - for (Mod& mod : m_LoadedMods) - { - if (!mod.m_bEnabled) - continue; - - // write content of each modded file to compiled file - std::ifstream siModKeys(mod.m_ModDirectory / "kb_act.lst"); - - if (siModKeys.good()) - soCompiledKeys << siModKeys.rdbuf() << std::endl; - - siModKeys.close(); - } - - soCompiledKeys.close(); - - // push to overrides - ModOverrideFile overrideFile; - overrideFile.m_pOwningMod = nullptr; - overrideFile.m_Path = KB_ACT_PATH; - - if (m_ModFiles.find(KB_ACT_PATH) == m_ModFiles.end()) - m_ModFiles.insert(std::make_pair(KB_ACT_PATH, overrideFile)); - else - m_ModFiles[KB_ACT_PATH] = overrideFile; -} diff --git a/NorthstarDLL/keyvalues.cpp b/NorthstarDLL/keyvalues.cpp deleted file mode 100644 index afb7ae30..00000000 --- a/NorthstarDLL/keyvalues.cpp +++ /dev/null @@ -1,1316 +0,0 @@ -#include "pch.h" -#include "keyvalues.h" -#include - -// implementation of the ConVar class -// heavily based on https://github.com/Mauler125/r5sdk/blob/master/r5dev/vpc/keyvalues.cpp - -typedef int HKeySymbol; -#define INVALID_KEY_SYMBOL (-1) - -#define MAKE_3_BYTES_FROM_1_AND_2(x1, x2) ((((uint16_t)x2) << 8) | (uint8_t)(x1)) -#define SPLIT_3_BYTES_INTO_1_AND_2(x1, x2, x3) \ - do \ - { \ - x1 = (uint8_t)(x3); \ - x2 = (uint16_t)((x3) >> 8); \ - } while (0) - -struct CKeyValuesSystem -{ - public: - struct __VTable - { - char pad0[8 * 3]; // 2 methods - HKeySymbol (*GetSymbolForString)(CKeyValuesSystem* self, const char* name, bool bCreate); - const char* (*GetStringForSymbol)(CKeyValuesSystem* self, HKeySymbol symbol); - char pad1[8 * 5]; - HKeySymbol (*GetSymbolForStringCaseSensitive)( - CKeyValuesSystem* self, HKeySymbol& hCaseInsensitiveSymbol, const char* name, bool bCreate); - }; - - const __VTable* m_pVtable; -}; - -int (*V_UTF8ToUnicode)(const char* pUTF8, wchar_t* pwchDest, int cubDestSizeInBytes); -int (*V_UnicodeToUTF8)(const wchar_t* pUnicode, char* pUTF8, int cubDestSizeInBytes); -CKeyValuesSystem* (*KeyValuesSystem)(); - -KeyValues::KeyValues() {} // default constructor for copying and such - -//----------------------------------------------------------------------------- -// Purpose: Constructor -// Input : *pszSetName - -//----------------------------------------------------------------------------- -KeyValues::KeyValues(const char* pszSetName) -{ - Init(); - SetName(pszSetName); -} - -//----------------------------------------------------------------------------- -// Purpose: Constructor -// Input : *pszSetName - -// *pszFirstKey - -// *pszFirstValue - -//----------------------------------------------------------------------------- -KeyValues::KeyValues(const char* pszSsetName, const char* pszFirstKey, const char* pszFirstValue) -{ - Init(); - SetName(pszSsetName); - SetString(pszFirstKey, pszFirstValue); -} - -//----------------------------------------------------------------------------- -// Purpose: Constructor -// Input : *pszSetName - -// *pszFirstKey - -// *pwszFirstValue - -//----------------------------------------------------------------------------- -KeyValues::KeyValues(const char* pszSetName, const char* pszFirstKey, const wchar_t* pwszFirstValue) -{ - Init(); - SetName(pszSetName); - SetWString(pszFirstKey, pwszFirstValue); -} - -//----------------------------------------------------------------------------- -// Purpose: Constructor -// Input : *pszSetName - -// *pszFirstKey - -// iFirstValue - -//----------------------------------------------------------------------------- -KeyValues::KeyValues(const char* pszSetName, const char* pszFirstKey, int iFirstValue) -{ - Init(); - SetName(pszSetName); - SetInt(pszFirstKey, iFirstValue); -} - -//----------------------------------------------------------------------------- -// Purpose: Constructor -// Input : *pszSetName - -// *pszFirstKey - -// *pszFirstValue - -// *pszSecondKey - -// *pszSecondValue - -//----------------------------------------------------------------------------- -KeyValues::KeyValues( - const char* pszSetName, const char* pszFirstKey, const char* pszFirstValue, const char* pszSecondKey, const char* pszSecondValue) -{ - Init(); - SetName(pszSetName); - SetString(pszFirstKey, pszFirstValue); - SetString(pszSecondKey, pszSecondValue); -} - -//----------------------------------------------------------------------------- -// Purpose: Constructor -// Input : *pszSetName - -// *pszFirstKey - -// iFirstValue - -// *pszSecondKey - -// iSecondValue - -//----------------------------------------------------------------------------- -KeyValues::KeyValues(const char* pszSetName, const char* pszFirstKey, int iFirstValue, const char* pszSecondKey, int iSecondValue) -{ - Init(); - SetName(pszSetName); - SetInt(pszFirstKey, iFirstValue); - SetInt(pszSecondKey, iSecondValue); -} - -//----------------------------------------------------------------------------- -// Purpose: Destructor -//----------------------------------------------------------------------------- -KeyValues::~KeyValues(void) -{ - RemoveEverything(); -} - -//----------------------------------------------------------------------------- -// Purpose: Initialize member variables -//----------------------------------------------------------------------------- -void KeyValues::Init(void) -{ - m_iKeyName = 0; - m_iKeyNameCaseSensitive1 = 0; - m_iKeyNameCaseSensitive2 = 0; - m_iDataType = TYPE_NONE; - - m_pSub = nullptr; - m_pPeer = nullptr; - m_pChain = nullptr; - - m_sValue = nullptr; - m_wsValue = nullptr; - m_pValue = nullptr; - - m_bHasEscapeSequences = 0; -} - -//----------------------------------------------------------------------------- -// Purpose: Clear out all subkeys, and the current value -//----------------------------------------------------------------------------- -void KeyValues::Clear(void) -{ - delete m_pSub; - m_pSub = nullptr; - m_iDataType = TYPE_NONE; -} - -//----------------------------------------------------------------------------- -// for backwards compat - we used to need this to force the free to run from the same DLL -// as the alloc -//----------------------------------------------------------------------------- -void KeyValues::DeleteThis(void) -{ - delete this; -} - -//----------------------------------------------------------------------------- -// Purpose: remove everything -//----------------------------------------------------------------------------- -void KeyValues::RemoveEverything(void) -{ - KeyValues* dat; - KeyValues* datNext = nullptr; - for (dat = m_pSub; dat != nullptr; dat = datNext) - { - datNext = dat->m_pPeer; - dat->m_pPeer = nullptr; - delete dat; - } - - for (dat = m_pPeer; dat && dat != this; dat = datNext) - { - datNext = dat->m_pPeer; - dat->m_pPeer = nullptr; - delete dat; - } - - delete[] m_sValue; - m_sValue = nullptr; - delete[] m_wsValue; - m_wsValue = nullptr; -} - -//----------------------------------------------------------------------------- -// Purpose: Find a keyValue, create it if it is not found. -// Set bCreate to true to create the key if it doesn't already exist -// (which ensures a valid pointer will be returned) -// Input : *pszKeyName - -// bCreate - -// Output : *KeyValues -//----------------------------------------------------------------------------- -KeyValues* KeyValues::FindKey(const char* pszKeyName, bool bCreate) -{ - assert(this, "Member function called on NULL KeyValues"); - - if (!pszKeyName || !*pszKeyName) - return this; - - const char* pSubStr = strchr(pszKeyName, '/'); - - HKeySymbol iSearchStr = KeyValuesSystem()->m_pVtable->GetSymbolForString(KeyValuesSystem(), pszKeyName, bCreate); - if (iSearchStr == INVALID_KEY_SYMBOL) - { - // not found, couldn't possibly be in key value list - return nullptr; - } - - KeyValues* pLastKVs = nullptr; - KeyValues* pCurrentKVs; - // find the searchStr in the current peer list - for (pCurrentKVs = m_pSub; pCurrentKVs != NULL; pCurrentKVs = pCurrentKVs->m_pPeer) - { - pLastKVs = pCurrentKVs; // record the last item looked at (for if we need to append to the end of the list) - - // symbol compare - if (pLastKVs->m_iKeyName == (uint32_t)iSearchStr) - break; - } - - if (!pCurrentKVs && m_pChain) - pCurrentKVs = m_pChain->FindKey(pszKeyName, false); - - // make sure a key was found - if (!pCurrentKVs) - { - if (bCreate) - { - // we need to create a new key - pCurrentKVs = new KeyValues(pszKeyName); - // Assert(dat != NULL); - - // insert new key at end of list - if (pLastKVs) - pLastKVs->m_pPeer = pCurrentKVs; - else - m_pSub = pCurrentKVs; - - pCurrentKVs->m_pPeer = NULL; - - // a key graduates to be a submsg as soon as it's m_pSub is set - // this should be the only place m_pSub is set - m_iDataType = TYPE_NONE; - } - else - { - return NULL; - } - } - - // if we've still got a subStr we need to keep looking deeper in the tree - if (pSubStr) - { - // recursively chain down through the paths in the string - return pCurrentKVs->FindKey(pSubStr + 1, bCreate); - } - - return pCurrentKVs; -} - -//----------------------------------------------------------------------------- -// Purpose: Locate last child. Returns NULL if we have no children -// Output : *KeyValues -//----------------------------------------------------------------------------- -KeyValues* KeyValues::FindLastSubKey(void) const -{ - // No children? - if (m_pSub == nullptr) - return nullptr; - - // Scan for the last one - KeyValues* pLastChild = m_pSub; - while (pLastChild->m_pPeer) - pLastChild = pLastChild->m_pPeer; - return pLastChild; -} - -//----------------------------------------------------------------------------- -// Purpose: Adds a subkey. Make sure the subkey isn't a child of some other keyvalues -// Input : *pSubKey - -//----------------------------------------------------------------------------- -void KeyValues::AddSubKey(KeyValues* pSubkey) -{ - // Make sure the subkey isn't a child of some other keyvalues - assert(pSubkey != nullptr); - assert(pSubkey->m_pPeer == nullptr); - - // add into subkey list - if (m_pSub == nullptr) - { - m_pSub = pSubkey; - } - else - { - KeyValues* pTempDat = m_pSub; - while (pTempDat->GetNextKey() != nullptr) - { - pTempDat = pTempDat->GetNextKey(); - } - - pTempDat->SetNextKey(pSubkey); - } -} - -//----------------------------------------------------------------------------- -// Purpose: Remove a subkey from the list -// Input : *pSubKey - -//----------------------------------------------------------------------------- -void KeyValues::RemoveSubKey(KeyValues* pSubKey) -{ - if (!pSubKey) - return; - - // check the list pointer - if (m_pSub == pSubKey) - { - m_pSub = pSubKey->m_pPeer; - } - else - { - // look through the list - KeyValues* kv = m_pSub; - while (kv->m_pPeer) - { - if (kv->m_pPeer == pSubKey) - { - kv->m_pPeer = pSubKey->m_pPeer; - break; - } - - kv = kv->m_pPeer; - } - } - - pSubKey->m_pPeer = nullptr; -} - -//----------------------------------------------------------------------------- -// Purpose: Insert a subkey at index -// Input : nIndex - -// *pSubKey - -//----------------------------------------------------------------------------- -void KeyValues::InsertSubKey(int nIndex, KeyValues* pSubKey) -{ - // Sub key must be valid and not part of another chain - assert(pSubKey && pSubKey->m_pPeer == nullptr); - - if (nIndex == 0) - { - pSubKey->m_pPeer = m_pSub; - m_pSub = pSubKey; - return; - } - else - { - int nCurrentIndex = 0; - for (KeyValues* pIter = GetFirstSubKey(); pIter != nullptr; pIter = pIter->GetNextKey()) - { - ++nCurrentIndex; - if (nCurrentIndex == nIndex) - { - pSubKey->m_pPeer = pIter->m_pPeer; - pIter->m_pPeer = pSubKey; - return; - } - } - // Index is out of range if we get here - assert(0); - return; - } -} - -//----------------------------------------------------------------------------- -// Purpose: Checks if key contains a subkey -// Input : *pSubKey - -// Output : true if contains, false otherwise -//----------------------------------------------------------------------------- -bool KeyValues::ContainsSubKey(KeyValues* pSubKey) -{ - for (KeyValues* pIter = GetFirstSubKey(); pIter != nullptr; pIter = pIter->GetNextKey()) - { - if (pSubKey == pIter) - { - return true; - } - } - return false; -} - -//----------------------------------------------------------------------------- -// Purpose: Swaps existing subkey with another -// Input : *pExistingSubkey - -// *pNewSubKey - -//----------------------------------------------------------------------------- -void KeyValues::SwapSubKey(KeyValues* pExistingSubkey, KeyValues* pNewSubKey) -{ - assert(pExistingSubkey != nullptr && pNewSubKey != nullptr); - - // Make sure the new sub key isn't a child of some other keyvalues - assert(pNewSubKey->m_pPeer == nullptr); - - // Check the list pointer - if (m_pSub == pExistingSubkey) - { - pNewSubKey->m_pPeer = pExistingSubkey->m_pPeer; - pExistingSubkey->m_pPeer = nullptr; - m_pSub = pNewSubKey; - } - else - { - // Look through the list - KeyValues* kv = m_pSub; - while (kv->m_pPeer) - { - if (kv->m_pPeer == pExistingSubkey) - { - pNewSubKey->m_pPeer = pExistingSubkey->m_pPeer; - pExistingSubkey->m_pPeer = nullptr; - kv->m_pPeer = pNewSubKey; - break; - } - - kv = kv->m_pPeer; - } - // Existing sub key should always be found, otherwise it's a bug in the calling code. - assert(kv->m_pPeer != nullptr); - } -} - -//----------------------------------------------------------------------------- -// Purpose: Elides subkey -// Input : *pSubKey - -//----------------------------------------------------------------------------- -void KeyValues::ElideSubKey(KeyValues* pSubKey) -{ - // This pointer's "next" pointer needs to be fixed up when we elide the key - KeyValues** ppPointerToFix = &m_pSub; - for (KeyValues* pKeyIter = m_pSub; pKeyIter != nullptr; ppPointerToFix = &pKeyIter->m_pPeer, pKeyIter = pKeyIter->GetNextKey()) - { - if (pKeyIter == pSubKey) - { - if (pSubKey->m_pSub == nullptr) - { - // No children, simply remove the key - *ppPointerToFix = pSubKey->m_pPeer; - delete pSubKey; - } - else - { - *ppPointerToFix = pSubKey->m_pSub; - // Attach the remainder of this chain to the last child of pSubKey - KeyValues* pChildIter = pSubKey->m_pSub; - while (pChildIter->m_pPeer != nullptr) - { - pChildIter = pChildIter->m_pPeer; - } - // Now points to the last child of pSubKey - pChildIter->m_pPeer = pSubKey->m_pPeer; - // Detach the node to be elided - pSubKey->m_pSub = nullptr; - pSubKey->m_pPeer = nullptr; - delete pSubKey; - } - return; - } - } - // Key not found; that's caller error. - assert(0); -} - -//----------------------------------------------------------------------------- -// Purpose: Check if a keyName has no value assigned to it. -// Input : *pszKeyName - -// Output : true on success, false otherwise -//----------------------------------------------------------------------------- -bool KeyValues::IsEmpty(const char* pszKeyName) -{ - KeyValues* pKey = FindKey(pszKeyName, false); - if (!pKey) - return true; - - if (pKey->m_iDataType == TYPE_NONE && pKey->m_pSub == nullptr) - return true; - - return false; -} - -//----------------------------------------------------------------------------- -// Purpose: gets the first true sub key -// Output : *KeyValues -//----------------------------------------------------------------------------- -KeyValues* KeyValues::GetFirstTrueSubKey(void) const -{ - assert(this, "Member function called on NULL KeyValues"); - KeyValues* pRet = this ? m_pSub : nullptr; - while (pRet && pRet->m_iDataType != TYPE_NONE) - pRet = pRet->m_pPeer; - - return pRet; -} - -//----------------------------------------------------------------------------- -// Purpose: gets the next true sub key -// Output : *KeyValues -//----------------------------------------------------------------------------- -KeyValues* KeyValues::GetNextTrueSubKey(void) const -{ - assert(this, "Member function called on NULL KeyValues"); - KeyValues* pRet = this ? m_pPeer : nullptr; - while (pRet && pRet->m_iDataType != TYPE_NONE) - pRet = pRet->m_pPeer; - - return pRet; -} - -//----------------------------------------------------------------------------- -// Purpose: gets the first value -// Output : *KeyValues -//----------------------------------------------------------------------------- -KeyValues* KeyValues::GetFirstValue(void) const -{ - assert(this, "Member function called on NULL KeyValues"); - KeyValues* pRet = this ? m_pSub : nullptr; - while (pRet && pRet->m_iDataType == TYPE_NONE) - pRet = pRet->m_pPeer; - - return pRet; -} - -//----------------------------------------------------------------------------- -// Purpose: gets the next value -// Output : *KeyValues -//----------------------------------------------------------------------------- -KeyValues* KeyValues::GetNextValue(void) const -{ - assert(this, "Member function called on NULL KeyValues"); - KeyValues* pRet = this ? m_pPeer : nullptr; - while (pRet && pRet->m_iDataType == TYPE_NONE) - pRet = pRet->m_pPeer; - - return pRet; -} - -//----------------------------------------------------------------------------- -// Purpose: Return the first subkey in the list -//----------------------------------------------------------------------------- -KeyValues* KeyValues::GetFirstSubKey() const -{ - assert(this, "Member function called on NULL KeyValues"); - return this ? m_pSub : nullptr; -} - -//----------------------------------------------------------------------------- -// Purpose: Return the next subkey -//----------------------------------------------------------------------------- -KeyValues* KeyValues::GetNextKey() const -{ - assert(this, "Member function called on NULL KeyValues"); - return this ? m_pPeer : nullptr; -} - -//----------------------------------------------------------------------------- -// Purpose: Get the name of the current key section -// Output : const char* -//----------------------------------------------------------------------------- -const char* KeyValues::GetName(void) const -{ - return KeyValuesSystem()->m_pVtable->GetStringForSymbol( - KeyValuesSystem(), MAKE_3_BYTES_FROM_1_AND_2(m_iKeyNameCaseSensitive1, m_iKeyNameCaseSensitive2)); -} - -//----------------------------------------------------------------------------- -// Purpose: Get the integer value of a keyName. Default value is returned -// if the keyName can't be found. -// Input : *pszKeyName - -// nDefaultValue - -// Output : int -//----------------------------------------------------------------------------- -int KeyValues::GetInt(const char* pszKeyName, int iDefaultValue) -{ - KeyValues* pKey = FindKey(pszKeyName, false); - if (pKey) - { - switch (pKey->m_iDataType) - { - case TYPE_STRING: - return atoi(pKey->m_sValue); - case TYPE_WSTRING: - return _wtoi(pKey->m_wsValue); - case TYPE_FLOAT: - return static_cast(pKey->m_flValue); - case TYPE_UINT64: - // can't convert, since it would lose data - assert(0); - return 0; - case TYPE_INT: - case TYPE_PTR: - default: - return pKey->m_iValue; - }; - } - return iDefaultValue; -} - -//----------------------------------------------------------------------------- -// Purpose: Get the integer value of a keyName. Default value is returned -// if the keyName can't be found. -// Input : *pszKeyName - -// nDefaultValue - -// Output : uint64_t -//----------------------------------------------------------------------------- -uint64_t KeyValues::GetUint64(const char* pszKeyName, uint64_t nDefaultValue) -{ - KeyValues* pKey = FindKey(pszKeyName, false); - if (pKey) - { - switch (pKey->m_iDataType) - { - case TYPE_STRING: - { - uint64_t uiResult = 0ull; - sscanf(pKey->m_sValue, "%lld", &uiResult); - return uiResult; - } - case TYPE_WSTRING: - { - uint64_t uiResult = 0ull; - swscanf(pKey->m_wsValue, L"%lld", &uiResult); - return uiResult; - } - case TYPE_FLOAT: - return static_cast(pKey->m_flValue); - case TYPE_UINT64: - return *reinterpret_cast(pKey->m_sValue); - case TYPE_PTR: - return static_cast(reinterpret_cast(pKey->m_pValue)); - case TYPE_INT: - default: - return pKey->m_iValue; - }; - } - return nDefaultValue; -} - -//----------------------------------------------------------------------------- -// Purpose: Get the pointer value of a keyName. Default value is returned -// if the keyName can't be found. -// Input : *pszKeyName - -// pDefaultValue - -// Output : void* -//----------------------------------------------------------------------------- -void* KeyValues::GetPtr(const char* pszKeyName, void* pDefaultValue) -{ - KeyValues* pKey = FindKey(pszKeyName, false); - if (pKey) - { - switch (pKey->m_iDataType) - { - case TYPE_PTR: - return pKey->m_pValue; - - case TYPE_WSTRING: - case TYPE_STRING: - case TYPE_FLOAT: - case TYPE_INT: - case TYPE_UINT64: - default: - return nullptr; - }; - } - return pDefaultValue; -} - -//----------------------------------------------------------------------------- -// Purpose: Get the float value of a keyName. Default value is returned -// if the keyName can't be found. -// Input : *pszKeyName - -// flDefaultValue - -// Output : float -//----------------------------------------------------------------------------- -float KeyValues::GetFloat(const char* pszKeyName, float flDefaultValue) -{ - KeyValues* pKey = FindKey(pszKeyName, false); - if (pKey) - { - switch (pKey->m_iDataType) - { - case TYPE_STRING: - return static_cast(atof(pKey->m_sValue)); - case TYPE_WSTRING: - return static_cast(_wtof(pKey->m_wsValue)); // no wtof - case TYPE_FLOAT: - return pKey->m_flValue; - case TYPE_INT: - return static_cast(pKey->m_iValue); - case TYPE_UINT64: - return static_cast((*(reinterpret_cast(pKey->m_sValue)))); - case TYPE_PTR: - default: - return 0.0f; - }; - } - return flDefaultValue; -} - -//----------------------------------------------------------------------------- -// Purpose: Get the string pointer of a keyName. Default value is returned -// if the keyName can't be found. -// // Input : *pszKeyName - -// pszDefaultValue - -// Output : const char* -//----------------------------------------------------------------------------- -const char* KeyValues::GetString(const char* pszKeyName, const char* pszDefaultValue) -{ - KeyValues* pKey = FindKey(pszKeyName, false); - if (pKey) - { - // convert the data to string form then return it - char buf[64]; - switch (pKey->m_iDataType) - { - case TYPE_FLOAT: - snprintf(buf, sizeof(buf), "%f", pKey->m_flValue); - SetString(pszKeyName, buf); - break; - case TYPE_PTR: - snprintf(buf, sizeof(buf), "%lld", reinterpret_cast(pKey->m_pValue)); - SetString(pszKeyName, buf); - break; - case TYPE_INT: - snprintf(buf, sizeof(buf), "%d", pKey->m_iValue); - SetString(pszKeyName, buf); - break; - case TYPE_UINT64: - snprintf(buf, sizeof(buf), "%lld", *(reinterpret_cast(pKey->m_sValue))); - SetString(pszKeyName, buf); - break; - case TYPE_COLOR: - snprintf(buf, sizeof(buf), "%d %d %d %d", pKey->m_Color[0], pKey->m_Color[1], pKey->m_Color[2], pKey->m_Color[3]); - SetString(pszKeyName, buf); - break; - - case TYPE_WSTRING: - { - // convert the string to char *, set it for future use, and return it - char wideBuf[512]; - int result = V_UnicodeToUTF8(pKey->m_wsValue, wideBuf, 512); - if (result) - { - // note: this will copy wideBuf - SetString(pszKeyName, wideBuf); - } - else - { - return pszDefaultValue; - } - break; - } - case TYPE_STRING: - break; - default: - return pszDefaultValue; - }; - - return pKey->m_sValue; - } - return pszDefaultValue; -} - -//----------------------------------------------------------------------------- -// Purpose: Get the wide string pointer of a keyName. Default value is returned -// if the keyName can't be found. -// // Input : *pszKeyName - -// pwszDefaultValue - -// Output : const wchar_t* -//----------------------------------------------------------------------------- -const wchar_t* KeyValues::GetWString(const char* pszKeyName, const wchar_t* pwszDefaultValue) -{ - KeyValues* pKey = FindKey(pszKeyName, false); - if (pKey) - { - wchar_t wbuf[64]; - switch (pKey->m_iDataType) - { - case TYPE_FLOAT: - swprintf(wbuf, ARRAYSIZE(wbuf), L"%f", pKey->m_flValue); - SetWString(pszKeyName, wbuf); - break; - case TYPE_PTR: - swprintf(wbuf, ARRAYSIZE(wbuf), L"%lld", static_cast(reinterpret_cast(pKey->m_pValue))); - SetWString(pszKeyName, wbuf); - break; - case TYPE_INT: - swprintf(wbuf, ARRAYSIZE(wbuf), L"%d", pKey->m_iValue); - SetWString(pszKeyName, wbuf); - break; - case TYPE_UINT64: - { - swprintf(wbuf, ARRAYSIZE(wbuf), L"%lld", *(reinterpret_cast(pKey->m_sValue))); - SetWString(pszKeyName, wbuf); - } - break; - case TYPE_COLOR: - swprintf(wbuf, ARRAYSIZE(wbuf), L"%d %d %d %d", pKey->m_Color[0], pKey->m_Color[1], pKey->m_Color[2], pKey->m_Color[3]); - SetWString(pszKeyName, wbuf); - break; - - case TYPE_WSTRING: - break; - case TYPE_STRING: - { - size_t bufSize = strlen(pKey->m_sValue) + 1; - wchar_t* pWBuf = new wchar_t[bufSize]; - int result = V_UTF8ToUnicode(pKey->m_sValue, pWBuf, static_cast(bufSize * sizeof(wchar_t))); - if (result >= 0) // may be a zero length string - { - SetWString(pszKeyName, pWBuf); - delete[] pWBuf; - } - else - { - delete[] pWBuf; - return pwszDefaultValue; - } - - break; - } - default: - return pwszDefaultValue; - }; - - return reinterpret_cast(pKey->m_wsValue); - } - return pwszDefaultValue; -} - -//----------------------------------------------------------------------------- -// Purpose: Gets a color -// Input : *pszKeyName - -// &defaultColor - -// Output : Color -//----------------------------------------------------------------------------- -Color KeyValues::GetColor(const char* pszKeyName, const Color& defaultColor) -{ - Color color = defaultColor; - KeyValues* pKey = FindKey(pszKeyName, false); - if (pKey) - { - if (pKey->m_iDataType == TYPE_COLOR) - { - color[0] = pKey->m_Color[0]; - color[1] = pKey->m_Color[1]; - color[2] = pKey->m_Color[2]; - color[3] = pKey->m_Color[3]; - } - else if (pKey->m_iDataType == TYPE_FLOAT) - { - color[0] = static_cast(pKey->m_flValue); - } - else if (pKey->m_iDataType == TYPE_INT) - { - color[0] = static_cast(pKey->m_iValue); - } - else if (pKey->m_iDataType == TYPE_STRING) - { - // parse the colors out of the string - float a = 0, b = 0, c = 0, d = 0; - sscanf(pKey->m_sValue, "%f %f %f %f", &a, &b, &c, &d); - color[0] = static_cast(a); - color[1] = static_cast(b); - color[2] = static_cast(c); - color[3] = static_cast(d); - } - } - return color; -} - -//----------------------------------------------------------------------------- -// Purpose: Get the data type of the value stored in a keyName -// Input : *pszKeyName - -//----------------------------------------------------------------------------- -KeyValuesTypes_t KeyValues::GetDataType(const char* pszKeyName) -{ - KeyValues* pKey = FindKey(pszKeyName, false); - if (pKey) - return static_cast(pKey->m_iDataType); - - return TYPE_NONE; -} - -//----------------------------------------------------------------------------- -// Purpose: Get the data type of the value stored in this keyName -//----------------------------------------------------------------------------- -KeyValuesTypes_t KeyValues::GetDataType(void) const -{ - return static_cast(m_iDataType); -} - -//----------------------------------------------------------------------------- -// Purpose: Set the integer value of a keyName. -// Input : *pszKeyName - -// iValue - -//----------------------------------------------------------------------------- -void KeyValues::SetInt(const char* pszKeyName, int iValue) -{ - KeyValues* pKey = FindKey(pszKeyName, true); - if (pKey) - { - pKey->m_iValue = iValue; - pKey->m_iDataType = TYPE_INT; - } -} - -//----------------------------------------------------------------------------- -// Purpose: Set the integer value of a keyName. -//----------------------------------------------------------------------------- -void KeyValues::SetUint64(const char* pszKeyName, uint64_t nValue) -{ - KeyValues* pKey = FindKey(pszKeyName, true); - - if (pKey) - { - // delete the old value - delete[] pKey->m_sValue; - // make sure we're not storing the WSTRING - as we're converting over to STRING - delete[] pKey->m_wsValue; - pKey->m_wsValue = nullptr; - - pKey->m_sValue = new char[sizeof(uint64_t)]; - *(reinterpret_cast(pKey->m_sValue)) = nValue; - pKey->m_iDataType = TYPE_UINT64; - } -} - -//----------------------------------------------------------------------------- -// Purpose: Set the float value of a keyName. -// Input : *pszKeyName - -// flValue - -//----------------------------------------------------------------------------- -void KeyValues::SetFloat(const char* pszKeyName, float flValue) -{ - KeyValues* pKey = FindKey(pszKeyName, true); - if (pKey) - { - pKey->m_flValue = flValue; - pKey->m_iDataType = TYPE_FLOAT; - } -} - -//----------------------------------------------------------------------------- -// Purpose: Set the name value of a keyName. -// Input : *pszSetName - -//----------------------------------------------------------------------------- -void KeyValues::SetName(const char* pszSetName) -{ - HKeySymbol hCaseSensitiveKeyName = INVALID_KEY_SYMBOL, hCaseInsensitiveKeyName = INVALID_KEY_SYMBOL; - hCaseSensitiveKeyName = - KeyValuesSystem()->m_pVtable->GetSymbolForStringCaseSensitive(KeyValuesSystem(), hCaseInsensitiveKeyName, pszSetName, false); - - m_iKeyName = hCaseInsensitiveKeyName; - SPLIT_3_BYTES_INTO_1_AND_2(m_iKeyNameCaseSensitive1, m_iKeyNameCaseSensitive2, hCaseSensitiveKeyName); -} - -//----------------------------------------------------------------------------- -// Purpose: Set the pointer value of a keyName. -// Input : *pszKeyName - -// *pValue - -//----------------------------------------------------------------------------- -void KeyValues::SetPtr(const char* pszKeyName, void* pValue) -{ - KeyValues* pKey = FindKey(pszKeyName, true); - - if (pKey) - { - pKey->m_pValue = pValue; - pKey->m_iDataType = TYPE_PTR; - } -} - -//----------------------------------------------------------------------------- -// Purpose: Set the string value (internal) -// Input : *pszValue - -//----------------------------------------------------------------------------- -void KeyValues::SetStringValue(char const* pszValue) -{ - // delete the old value - delete[] m_sValue; - // make sure we're not storing the WSTRING - as we're converting over to STRING - delete[] m_wsValue; - m_wsValue = nullptr; - - if (!pszValue) - { - // ensure a valid value - pszValue = ""; - } - - // allocate memory for the new value and copy it in - size_t len = strlen(pszValue); - m_sValue = new char[len + 1]; - memcpy(m_sValue, pszValue, len + 1); - - m_iDataType = TYPE_STRING; -} - -//----------------------------------------------------------------------------- -// Purpose: Sets this key's peer to the KeyValues passed in -// Input : *pDat - -//----------------------------------------------------------------------------- -void KeyValues::SetNextKey(KeyValues* pDat) -{ - m_pPeer = pDat; -} - -//----------------------------------------------------------------------------- -// Purpose: Set the string value of a keyName. -// Input : *pszKeyName - -// *pszValue - -//----------------------------------------------------------------------------- -void KeyValues::SetString(const char* pszKeyName, const char* pszValue) -{ - if (KeyValues* pKey = FindKey(pszKeyName, true)) - { - pKey->SetStringValue(pszValue); - } -} - -//----------------------------------------------------------------------------- -// Purpose: Set the string value of a keyName. -// Input : *pszKeyName - -// *pwszValue - -//----------------------------------------------------------------------------- -void KeyValues::SetWString(const char* pszKeyName, const wchar_t* pwszValue) -{ - KeyValues* pKey = FindKey(pszKeyName, true); - if (pKey) - { - // delete the old value - delete[] pKey->m_wsValue; - // make sure we're not storing the STRING - as we're converting over to WSTRING - delete[] pKey->m_sValue; - pKey->m_sValue = nullptr; - - if (!pwszValue) - { - // ensure a valid value - pwszValue = L""; - } - - // allocate memory for the new value and copy it in - size_t len = wcslen(pwszValue); - pKey->m_wsValue = new wchar_t[len + 1]; - memcpy(pKey->m_wsValue, pwszValue, (len + 1) * sizeof(wchar_t)); - - pKey->m_iDataType = TYPE_WSTRING; - } -} - -//----------------------------------------------------------------------------- -// Purpose: Sets a color -// Input : *pszKeyName - -// color - -//----------------------------------------------------------------------------- -void KeyValues::SetColor(const char* pszKeyName, Color color) -{ - KeyValues* pKey = FindKey(pszKeyName, true); - - if (pKey) - { - pKey->m_iDataType = TYPE_COLOR; - pKey->m_Color[0] = color[0]; - pKey->m_Color[1] = color[1]; - pKey->m_Color[2] = color[2]; - pKey->m_Color[3] = color[3]; - } -} - -//----------------------------------------------------------------------------- -// Purpose: -// Input : &src - -//----------------------------------------------------------------------------- -void KeyValues::RecursiveCopyKeyValues(KeyValues& src) -{ - // garymcthack - need to check this code for possible buffer overruns. - - m_iKeyName = src.m_iKeyName; - m_iKeyNameCaseSensitive1 = src.m_iKeyNameCaseSensitive1; - m_iKeyNameCaseSensitive2 = src.m_iKeyNameCaseSensitive2; - - if (!src.m_pSub) - { - m_iDataType = src.m_iDataType; - char buf[256]; - switch (src.m_iDataType) - { - case TYPE_NONE: - break; - case TYPE_STRING: - if (src.m_sValue) - { - size_t len = strlen(src.m_sValue) + 1; - m_sValue = new char[len]; - strncpy(m_sValue, src.m_sValue, len); - } - break; - case TYPE_INT: - { - m_iValue = src.m_iValue; - snprintf(buf, sizeof(buf), "%d", m_iValue); - size_t len = strlen(buf) + 1; - m_sValue = new char[len]; - strncpy(m_sValue, buf, len); - } - break; - case TYPE_FLOAT: - { - m_flValue = src.m_flValue; - snprintf(buf, sizeof(buf), "%f", m_flValue); - size_t len = strlen(buf) + 1; - m_sValue = new char[len]; - strncpy(m_sValue, buf, len); - } - break; - case TYPE_PTR: - { - m_pValue = src.m_pValue; - } - break; - case TYPE_UINT64: - { - m_sValue = new char[sizeof(uint64_t)]; - memcpy(m_sValue, src.m_sValue, sizeof(uint64_t)); - } - break; - case TYPE_COLOR: - { - m_Color[0] = src.m_Color[0]; - m_Color[1] = src.m_Color[1]; - m_Color[2] = src.m_Color[2]; - m_Color[3] = src.m_Color[3]; - } - break; - - default: - { - // do nothing . .what the heck is this? - assert(0); - } - break; - } - } - - // Handle the immediate child - if (src.m_pSub) - { - m_pSub = new KeyValues; - - m_pSub->Init(); - m_pSub->SetName(nullptr); - - m_pSub->RecursiveCopyKeyValues(*src.m_pSub); - } - - // Handle the immediate peer - if (src.m_pPeer) - { - m_pPeer = new KeyValues; - - m_pPeer->Init(); - m_pPeer->SetName(nullptr); - - m_pPeer->RecursiveCopyKeyValues(*src.m_pPeer); - } -} - -//----------------------------------------------------------------------------- -// Purpose: Make a new copy of all subkeys, add them all to the passed-in keyvalues -// Input : *pParent - -//----------------------------------------------------------------------------- -void KeyValues::CopySubkeys(KeyValues* pParent) const -{ - // recursively copy subkeys - // Also maintain ordering.... - KeyValues* pPrev = nullptr; - for (KeyValues* pSub = m_pSub; pSub != nullptr; pSub = pSub->m_pPeer) - { - // take a copy of the subkey - KeyValues* pKey = pSub->MakeCopy(); - - // add into subkey list - if (pPrev) - { - pPrev->m_pPeer = pKey; - } - else - { - pParent->m_pSub = pKey; - } - pKey->m_pPeer = nullptr; - pPrev = pKey; - } -} - -//----------------------------------------------------------------------------- -// Purpose: Makes a copy of the whole key-value pair set -// Output : KeyValues* -//----------------------------------------------------------------------------- -KeyValues* KeyValues::MakeCopy(void) const -{ - KeyValues* pNewKeyValue = new KeyValues; - - pNewKeyValue->Init(); - pNewKeyValue->SetName(GetName()); - - // copy data - pNewKeyValue->m_iDataType = m_iDataType; - switch (m_iDataType) - { - case TYPE_STRING: - { - if (m_sValue) - { - size_t len = strlen(m_sValue); - assert(!pNewKeyValue->m_sValue); - pNewKeyValue->m_sValue = new char[len + 1]; - memcpy(pNewKeyValue->m_sValue, m_sValue, len + 1); - } - } - break; - case TYPE_WSTRING: - { - if (m_wsValue) - { - size_t len = wcslen(m_wsValue); - pNewKeyValue->m_wsValue = new wchar_t[len + 1]; - memcpy(pNewKeyValue->m_wsValue, m_wsValue, len + 1 * sizeof(wchar_t)); - } - } - break; - - case TYPE_INT: - pNewKeyValue->m_iValue = m_iValue; - break; - - case TYPE_FLOAT: - pNewKeyValue->m_flValue = m_flValue; - break; - - case TYPE_PTR: - pNewKeyValue->m_pValue = m_pValue; - break; - - case TYPE_COLOR: - pNewKeyValue->m_Color[0] = m_Color[0]; - pNewKeyValue->m_Color[1] = m_Color[1]; - pNewKeyValue->m_Color[2] = m_Color[2]; - pNewKeyValue->m_Color[3] = m_Color[3]; - break; - - case TYPE_UINT64: - pNewKeyValue->m_sValue = new char[sizeof(uint64_t)]; - memcpy(pNewKeyValue->m_sValue, m_sValue, sizeof(uint64_t)); - break; - }; - - // recursively copy subkeys - CopySubkeys(pNewKeyValue); - return pNewKeyValue; -} - -ON_DLL_LOAD("vstdlib.dll", KeyValues, (CModule module)) -{ - V_UTF8ToUnicode = module.GetExport("V_UTF8ToUnicode").As(); - V_UnicodeToUTF8 = module.GetExport("V_UnicodeToUTF8").As(); - KeyValuesSystem = module.GetExport("KeyValuesSystem").As(); -} - -AUTOHOOK_INIT() - -// clang-format off -AUTOHOOK(KeyValues__LoadFromBuffer, engine.dll + 0x426C30, -char, __fastcall, (KeyValues* self, const char* pResourceName, 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(pResourceName, "playlists")) - pFileSystem = pSavedFilesystemPtr; - - return KeyValues__LoadFromBuffer(self, pResourceName, pBuffer, pFileSystem, a5, a6, a7); -} - -ON_DLL_LOAD("engine.dll", EngineKeyValues, (CModule module)) -{ - AUTOHOOK_DISPATCH() -} diff --git a/NorthstarDLL/keyvalues.h b/NorthstarDLL/keyvalues.h deleted file mode 100644 index 3ea682d6..00000000 --- a/NorthstarDLL/keyvalues.h +++ /dev/null @@ -1,134 +0,0 @@ -#pragma once -#include "color.h" - -enum KeyValuesTypes_t : char -{ - TYPE_NONE = 0x0, - TYPE_STRING = 0x1, - TYPE_INT = 0x2, - TYPE_FLOAT = 0x3, - TYPE_PTR = 0x4, - TYPE_WSTRING = 0x5, - TYPE_COLOR = 0x6, - TYPE_UINT64 = 0x7, - TYPE_COMPILED_INT_BYTE = 0x8, - TYPE_COMPILED_INT_0 = 0x9, - TYPE_COMPILED_INT_1 = 0xA, - TYPE_NUMTYPES = 0xB, -}; - -enum MergeKeyValuesOp_t -{ - MERGE_KV_ALL, - MERGE_KV_UPDATE, // update values are copied into storage, adding new keys to storage or updating existing ones - MERGE_KV_DELETE, // update values specify keys that get deleted from storage - MERGE_KV_BORROW, // update values only update existing keys in storage, keys in update that do not exist in storage are discarded -}; - -//----------------------------------------------------------------------------- -// Purpose: Simple recursive data access class -// Used in vgui for message parameters and resource files -// Destructor deletes all child KeyValues nodes -// Data is stored in key (string names) - (string/int/float)value pairs called nodes. -// -// About KeyValues Text File Format: - -// It has 3 control characters '{', '}' and '"'. Names and values may be quoted or -// not. The quote '"' character must not be used within name or values, only for -// quoting whole tokens. You may use escape sequences wile parsing and add within a -// quoted token a \" to add quotes within your name or token. When using Escape -// Sequence the parser must now that by setting KeyValues::UsesEscapeSequences( true ), -// which it's off by default. Non-quoted tokens ends with a whitespace, '{', '}' and '"'. -// So you may use '{' and '}' within quoted tokens, but not for non-quoted tokens. -// An open bracket '{' after a key name indicates a list of subkeys which is finished -// with a closing bracket '}'. Subkeys use the same definitions recursively. -// Whitespaces are space, return, newline and tabulator. Allowed Escape sequences -// are \n, \t, \\, \n and \". The number character '#' is used for macro purposes -// (eg #include), don't use it as first character in key names. -//----------------------------------------------------------------------------- -class KeyValues -{ - private: - KeyValues(); // for internal use only - - public: - // Constructors/destructors - KeyValues(const char* pszSetName); - KeyValues(const char* pszSetName, const char* pszFirstKey, const char* pszFirstValue); - KeyValues(const char* pszSetName, const char* pszFirstKey, const wchar_t* pwszFirstValue); - KeyValues(const char* pszSetName, const char* pszFirstKey, int iFirstValue); - KeyValues( - const char* pszSetName, const char* pszFirstKey, const char* pszFirstValue, const char* pszSecondKey, const char* pszSecondValue); - KeyValues(const char* pszSetName, const char* pszFirstKey, int iFirstValue, const char* pszSecondKey, int iSecondValue); - ~KeyValues(void); - - void Init(void); - void Clear(void); - void DeleteThis(void); - void RemoveEverything(); - - KeyValues* FindKey(const char* pKeyName, bool bCreate = false); - KeyValues* FindLastSubKey(void) const; - - void AddSubKey(KeyValues* pSubkey); - void RemoveSubKey(KeyValues* pSubKey); - void InsertSubKey(int nIndex, KeyValues* pSubKey); - bool ContainsSubKey(KeyValues* pSubKey); - void SwapSubKey(KeyValues* pExistingSubkey, KeyValues* pNewSubKey); - void ElideSubKey(KeyValues* pSubKey); - - // Data access - bool IsEmpty(const char* pszKeyName); - KeyValues* GetFirstTrueSubKey(void) const; - KeyValues* GetNextTrueSubKey(void) const; - KeyValues* GetFirstValue(void) const; - KeyValues* GetNextValue(void) const; - KeyValues* GetFirstSubKey() const; - KeyValues* GetNextKey() const; - const char* GetName(void) const; - int GetInt(const char* pszKeyName, int iDefaultValue); - uint64_t GetUint64(const char* pszKeyName, uint64_t nDefaultValue); - void* GetPtr(const char* pszKeyName, void* pDefaultValue); - float GetFloat(const char* pszKeyName, float flDefaultValue); - const char* GetString(const char* pszKeyName = nullptr, const char* pszDefaultValue = ""); - const wchar_t* GetWString(const char* pszKeyName = nullptr, const wchar_t* pwszDefaultValue = L""); - Color GetColor(const char* pszKeyName, const Color& defaultColor); - KeyValuesTypes_t GetDataType(const char* pszKeyName); - KeyValuesTypes_t GetDataType(void) const; - - // Key writing - void SetInt(const char* pszKeyName, int iValue); - void SetUint64(const char* pszKeyName, uint64_t nValue); - void SetPtr(const char* pszKeyName, void* pValue); - void SetNextKey(KeyValues* pDat); - void SetName(const char* pszName); - void SetString(const char* pszKeyName, const char* pszValue); - void SetWString(const char* pszKeyName, const wchar_t* pwszValue); - void SetStringValue(char const* pszValue); - void SetColor(const char* pszKeyName, Color color); - void SetFloat(const char* pszKeyName, float flValue); - - void RecursiveCopyKeyValues(KeyValues& src); - void CopySubkeys(KeyValues* pParent) const; - KeyValues* MakeCopy(void) const; - - public: - uint32_t m_iKeyName : 24; // 0x0000 - uint32_t m_iKeyNameCaseSensitive1 : 8; // 0x0003 - char* m_sValue; // 0x0008 - wchar_t* m_wsValue; // 0x0010 - union // 0x0018 - { - int m_iValue; - float m_flValue; - void* m_pValue; - unsigned char m_Color[4]; - }; - char m_szShortName[8]; // 0x0020 - char m_iDataType; // 0x0028 - char m_bHasEscapeSequences; // 0x0029 - uint16_t m_iKeyNameCaseSensitive2; // 0x002A - KeyValues* m_pPeer; // 0x0030 - KeyValues* m_pSub; // 0x0038 - KeyValues* m_pChain; // 0x0040 -}; diff --git a/NorthstarDLL/languagehooks.cpp b/NorthstarDLL/languagehooks.cpp deleted file mode 100644 index d00beb68..00000000 --- a/NorthstarDLL/languagehooks.cpp +++ /dev/null @@ -1,116 +0,0 @@ -#include "pch.h" -#include "tier0.h" - -#include -#include - -AUTOHOOK_INIT() - -typedef LANGID (*Tier0_DetectDefaultLanguageType)(); - -bool CheckLangAudioExists(char* lang) -{ - std::string path {"r2\\sound\\general_"}; - path += lang; - path += ".mstr"; - return fs::exists(path); -} - -std::vector file_list(fs::path dir, std::regex ext_pattern) -{ - std::vector result; - - if (!fs::exists(dir) || !fs::is_directory(dir)) - return result; - - using iterator = fs::directory_iterator; - - const iterator end; - for (iterator iter {dir}; iter != end; ++iter) - { - const std::string filename = iter->path().filename().string(); - std::smatch matches; - if (fs::is_regular_file(*iter) && std::regex_match(filename, matches, ext_pattern)) - { - result.push_back(std::move(matches.str(1))); - } - } - - return result; -} - -std::string GetAnyInstalledAudioLanguage() -{ - for (const auto& lang : file_list("r2\\sound\\", std::regex(".*?general_([a-z]+)_patch_1\\.mstr"))) - if (lang != "general" || lang != "") - return lang; - return "NO LANGUAGE DETECTED"; -} - -// clang-format off -AUTOHOOK(GetGameLanguage, tier0.dll + 0xF560, -char*, __fastcall, ()) -// clang-format on -{ - auto tier0Handle = GetModuleHandleA("tier0.dll"); - auto Tier0_DetectDefaultLanguageType = GetProcAddress(tier0Handle, "Tier0_DetectDefaultLanguage"); - char* ingameLang1 = (char*)tier0Handle + 0xA9B60; // one of the globals we need to override if overriding lang (size: 256) - bool& canOriginDictateLang = *(bool*)((char*)tier0Handle + 0xA9A90); - - const char* forcedLanguage; - if (Tier0::CommandLine()->CheckParm("-language", &forcedLanguage)) - { - if (!CheckLangAudioExists((char*)forcedLanguage)) - { - spdlog::info( - "User tried to force the language (-language) to \"{}\", but audio for this language doesn't exist and the game is bound " - "to error, falling back to next option...", - forcedLanguage); - } - else - { - spdlog::info("User forcing the language (-language) to: {}", forcedLanguage); - strncpy(ingameLang1, forcedLanguage, 256); - return ingameLang1; - } - } - - canOriginDictateLang = true; // let it try - { - auto lang = GetGameLanguage(); - if (!CheckLangAudioExists(lang)) - { - if (strcmp(lang, "russian") != - 0) // don't log for "russian" since it's the default and that means Origin detection just didn't change it most likely - spdlog::info( - "Origin detected language \"{}\", but we do not have audio for it installed, falling back to the next option", lang); - } - else - { - spdlog::info("Origin detected language: {}", lang); - return lang; - } - } - - Tier0_DetectDefaultLanguageType(); // force the global in tier0 to be populated with language inferred from user's system rather than - // defaulting to Russian - canOriginDictateLang = false; // Origin has no say anymore, we will fallback to user's system setup language - auto lang = GetGameLanguage(); - spdlog::info("Detected system language: {}", lang); - if (!CheckLangAudioExists(lang)) - { - spdlog::warn("Caution, audio for this language does NOT exist. You might want to override your game language with -language " - "command line option."); - auto lang = GetAnyInstalledAudioLanguage(); - spdlog::warn("Falling back to the first installed audio language: {}", lang.c_str()); - strncpy(ingameLang1, lang.c_str(), 256); - return ingameLang1; - } - - return lang; -} - -ON_DLL_LOAD_CLIENT("tier0.dll", LanguageHooks, (CModule module)) -{ - AUTOHOOK_DISPATCH() -} diff --git a/NorthstarDLL/latencyflex.cpp b/NorthstarDLL/latencyflex.cpp deleted file mode 100644 index 620e031a..00000000 --- a/NorthstarDLL/latencyflex.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "pch.h" -#include "convar.h" - -AUTOHOOK_INIT() - -ConVar* Cvar_r_latencyflex; - -void (*m_winelfx_WaitAndBeginFrame)(); - -// clang-format off -AUTOHOOK(OnRenderStart, client.dll + 0x1952C0, -void, __fastcall, ()) -// clang-format on -{ - if (Cvar_r_latencyflex->GetBool() && m_winelfx_WaitAndBeginFrame) - m_winelfx_WaitAndBeginFrame(); - - OnRenderStart(); -} - -ON_DLL_LOAD_CLIENT_RELIESON("client.dll", LatencyFlex, ConVar, (CModule module)) -{ - // Connect to the LatencyFleX service - // LatencyFleX is an open source vendor agnostic replacement for Nvidia Reflex input latency reduction technology. - // https://ishitatsuyuki.github.io/post/latencyflex/ - HMODULE pLfxModule; - - if (pLfxModule = LoadLibraryA("latencyflex_layer.dll")) - m_winelfx_WaitAndBeginFrame = - reinterpret_cast(reinterpret_cast(GetProcAddress(pLfxModule, "lfx_WaitAndBeginFrame"))); - else if (pLfxModule = LoadLibraryA("latencyflex_wine.dll")) - m_winelfx_WaitAndBeginFrame = - reinterpret_cast(reinterpret_cast(GetProcAddress(pLfxModule, "winelfx_WaitAndBeginFrame"))); - else - { - spdlog::info("Unable to load LatencyFleX library, LatencyFleX disabled."); - return; - } - - AUTOHOOK_DISPATCH() - - spdlog::info("LatencyFleX initialized."); - Cvar_r_latencyflex = new ConVar("r_latencyflex", "1", FCVAR_ARCHIVE, "Whether or not to use LatencyFleX input latency reduction."); -} diff --git a/NorthstarDLL/limits.cpp b/NorthstarDLL/limits.cpp deleted file mode 100644 index c254de52..00000000 --- a/NorthstarDLL/limits.cpp +++ /dev/null @@ -1,298 +0,0 @@ -#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; - -// 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 -{ - static const ConVar* Cvar_net_data_block_enabled = R2::g_pCVar->FindVar("net_data_block_enabled"); - - // don't ratelimit datablock packets as long as datablock is enabled - if (packet->adr.type == R2::NA_IP && - (!(packet->data[4] == 'N' && Cvar_net_data_block_enabled->GetBool()) || !Cvar_net_data_block_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"); -} - -ON_DLL_LOAD("server.dll", ServerLimitsServer, (CModule module)) -{ - AUTOHOOK_DISPATCH_MODULE(server.dll) -} diff --git a/NorthstarDLL/limits.h b/NorthstarDLL/limits.h deleted file mode 100644 index 068f91c9..00000000 --- a/NorthstarDLL/limits.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once -#include "r2engine.h" -#include "convar.h" -#include - -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 m_PlayerLimitData; - std::vector 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; diff --git a/NorthstarDLL/localchatwriter.cpp b/NorthstarDLL/localchatwriter.cpp deleted file mode 100644 index efa7eeee..00000000 --- a/NorthstarDLL/localchatwriter.cpp +++ /dev/null @@ -1,450 +0,0 @@ -#include "pch.h" -#include "localchatwriter.h" - -class vgui_BaseRichText_vtable; - -class vgui_BaseRichText -{ - public: - vgui_BaseRichText_vtable* vtable; -}; - -class vgui_BaseRichText_vtable -{ - public: - char unknown1[1880]; - - void(__fastcall* InsertChar)(vgui_BaseRichText* self, wchar_t ch); - - // yes these are swapped from the Source 2013 code, who knows why - void(__fastcall* InsertStringWide)(vgui_BaseRichText* self, const wchar_t* wszText); - void(__fastcall* InsertStringAnsi)(vgui_BaseRichText* self, const char* text); - - void(__fastcall* SelectNone)(vgui_BaseRichText* self); - void(__fastcall* SelectAllText)(vgui_BaseRichText* self); - void(__fastcall* SelectNoText)(vgui_BaseRichText* self); - void(__fastcall* CutSelected)(vgui_BaseRichText* self); - void(__fastcall* CopySelected)(vgui_BaseRichText* self); - void(__fastcall* SetPanelInteractive)(vgui_BaseRichText* self, bool bInteractive); - void(__fastcall* SetUnusedScrollbarInvisible)(vgui_BaseRichText* self, bool bInvis); - - void* unknown2; - - void(__fastcall* GotoTextStart)(vgui_BaseRichText* self); - void(__fastcall* GotoTextEnd)(vgui_BaseRichText* self); - - void* unknown3[3]; - - void(__fastcall* SetVerticalScrollbar)(vgui_BaseRichText* self, bool state); - void(__fastcall* SetMaximumCharCount)(vgui_BaseRichText* self, int maxChars); - void(__fastcall* InsertColorChange)(vgui_BaseRichText* self, Color col); - void(__fastcall* InsertIndentChange)(vgui_BaseRichText* self, int pixelsIndent); - void(__fastcall* InsertClickableTextStart)(vgui_BaseRichText* self, const char* pchClickAction); - void(__fastcall* InsertClickableTextEnd)(vgui_BaseRichText* self); - void(__fastcall* InsertPossibleURLString)(vgui_BaseRichText* self, const char* text, Color URLTextColor, Color normalTextColor); - void(__fastcall* InsertFade)(vgui_BaseRichText* self, float flSustain, float flLength); - void(__fastcall* ResetAllFades)(vgui_BaseRichText* self, bool bHold, bool bOnlyExpired, float flNewSustain); - void(__fastcall* SetToFullHeight)(vgui_BaseRichText* self); - int(__fastcall* GetNumLines)(vgui_BaseRichText* self); -}; - -class CGameSettings -{ - public: - char unknown1[92]; - int isChatEnabled; -}; - -// Not sure what this actually refers to but chatFadeLength and chatFadeSustain -// have their value at the same offset -class CGameFloatVar -{ - public: - char unknown1[88]; - float value; -}; - -CGameSettings** gGameSettings; -CGameFloatVar** gChatFadeLength; -CGameFloatVar** gChatFadeSustain; - -CHudChat** CHudChat::allHuds; - -typedef void(__fastcall* ConvertANSIToUnicodeType)(LPCSTR ansi, int ansiCharLength, LPWSTR unicode, int unicodeCharLength); -ConvertANSIToUnicodeType ConvertANSIToUnicode; - -LocalChatWriter::SwatchColor swatchColors[4] = { - LocalChatWriter::MainTextColor, - LocalChatWriter::SameTeamNameColor, - LocalChatWriter::EnemyTeamNameColor, - LocalChatWriter::NetworkNameColor, -}; - -Color darkColors[8] = { - Color {0, 0, 0, 255}, - Color {205, 49, 49, 255}, - Color {13, 188, 121, 255}, - Color {229, 229, 16, 255}, - Color {36, 114, 200, 255}, - Color {188, 63, 188, 255}, - Color {17, 168, 205, 255}, - Color {229, 229, 229, 255}}; - -Color lightColors[8] = { - Color {102, 102, 102, 255}, - Color {241, 76, 76, 255}, - Color {35, 209, 139, 255}, - Color {245, 245, 67, 255}, - Color {59, 142, 234, 255}, - Color {214, 112, 214, 255}, - Color {41, 184, 219, 255}, - Color {255, 255, 255, 255}}; - -class AnsiEscapeParser -{ - public: - explicit AnsiEscapeParser(LocalChatWriter* writer) : m_writer(writer) {} - - void HandleVal(unsigned long val) - { - switch (m_next) - { - case Next::ControlType: - m_next = HandleControlType(val); - break; - case Next::ForegroundType: - m_next = HandleForegroundType(val); - break; - case Next::Foreground8Bit: - m_next = HandleForeground8Bit(val); - break; - case Next::ForegroundR: - m_next = HandleForegroundR(val); - break; - case Next::ForegroundG: - m_next = HandleForegroundG(val); - break; - case Next::ForegroundB: - m_next = HandleForegroundB(val); - break; - } - } - - private: - enum class Next - { - ControlType, - ForegroundType, - Foreground8Bit, - ForegroundR, - ForegroundG, - ForegroundB - }; - - LocalChatWriter* m_writer; - Next m_next = Next::ControlType; - Color m_expandedColor {0, 0, 0, 0}; - - Next HandleControlType(unsigned long val) - { - // Reset - if (val == 0 || val == 39) - { - m_writer->InsertSwatchColorChange(LocalChatWriter::MainTextColor); - return Next::ControlType; - } - - // Dark foreground color - if (val >= 30 && val < 38) - { - m_writer->InsertColorChange(darkColors[val - 30]); - return Next::ControlType; - } - - // Light foreground color - if (val >= 90 && val < 98) - { - m_writer->InsertColorChange(lightColors[val - 90]); - return Next::ControlType; - } - - // Game swatch color - if (val >= 110 && val < 114) - { - m_writer->InsertSwatchColorChange(swatchColors[val - 110]); - return Next::ControlType; - } - - // Expanded foreground color - if (val == 38) - { - return Next::ForegroundType; - } - - return Next::ControlType; - } - - Next HandleForegroundType(unsigned long val) - { - // Next values are r,g,b - if (val == 2) - { - m_expandedColor.SetColor(0, 0, 0, 255); - return Next::ForegroundR; - } - // Next value is 8-bit swatch color - if (val == 5) - { - return Next::Foreground8Bit; - } - - // Invalid - return Next::ControlType; - } - - Next HandleForeground8Bit(unsigned long val) - { - if (val < 8) - { - m_writer->InsertColorChange(darkColors[val]); - } - else if (val < 16) - { - m_writer->InsertColorChange(lightColors[val - 8]); - } - else if (val < 232) - { - unsigned char code = val - 16; - unsigned char blue = code % 6; - unsigned char green = ((code - blue) / 6) % 6; - unsigned char red = (code - blue - (green * 6)) / 36; - m_writer->InsertColorChange(Color {(unsigned char)(red * 51), (unsigned char)(green * 51), (unsigned char)(blue * 51), 255}); - } - else if (val < UCHAR_MAX) - { - unsigned char brightness = (val - 232) * 10 + 8; - m_writer->InsertColorChange(Color {brightness, brightness, brightness, 255}); - } - - return Next::ControlType; - } - - Next HandleForegroundR(unsigned long val) - { - if (val >= UCHAR_MAX) - return Next::ControlType; - - m_expandedColor[0] = (unsigned char)val; - return Next::ForegroundG; - } - - Next HandleForegroundG(unsigned long val) - { - if (val >= UCHAR_MAX) - return Next::ControlType; - - m_expandedColor[1] = (unsigned char)val; - return Next::ForegroundB; - } - - Next HandleForegroundB(unsigned long val) - { - if (val >= UCHAR_MAX) - return Next::ControlType; - - m_expandedColor[2] = (unsigned char)val; - m_writer->InsertColorChange(m_expandedColor); - return Next::ControlType; - } -}; - -LocalChatWriter::LocalChatWriter(Context context) : m_context(context) {} - -void LocalChatWriter::Write(const char* str) -{ - char writeBuffer[256]; - - while (true) - { - const char* startOfEscape = strstr(str, "\033["); - - if (startOfEscape == NULL) - { - // No more escape sequences, write the remaining text and exit - InsertText(str); - break; - } - - if (startOfEscape != str) - { - // There is some text before the escape sequence, just print that - size_t copyChars = startOfEscape - str; - if (copyChars > 255) - copyChars = 255; - - strncpy_s(writeBuffer, copyChars + 1, str, copyChars); - - InsertText(writeBuffer); - } - - const char* escape = startOfEscape + 2; - str = ApplyAnsiEscape(escape); - } -} - -void LocalChatWriter::WriteLine(const char* str) -{ - InsertChar(L'\n'); - InsertSwatchColorChange(MainTextColor); - Write(str); -} - -void LocalChatWriter::InsertChar(wchar_t ch) -{ - for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) - { - if (hud->m_unknownContext != (int)m_context) - continue; - - hud->m_richText->vtable->InsertChar(hud->m_richText, ch); - } - - if (ch != L'\n') - { - InsertDefaultFade(); - } -} - -void LocalChatWriter::InsertText(const char* str) -{ - spdlog::info(str); - - WCHAR messageUnicode[288]; - ConvertANSIToUnicode(str, -1, messageUnicode, 274); - - for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) - { - if (hud->m_unknownContext != (int)m_context) - continue; - - hud->m_richText->vtable->InsertStringWide(hud->m_richText, messageUnicode); - } - - InsertDefaultFade(); -} - -void LocalChatWriter::InsertText(const wchar_t* str) -{ - for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) - { - if (hud->m_unknownContext != (int)m_context) - continue; - - hud->m_richText->vtable->InsertStringWide(hud->m_richText, str); - } - - InsertDefaultFade(); -} - -void LocalChatWriter::InsertColorChange(Color color) -{ - for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) - { - if (hud->m_unknownContext != (int)m_context) - continue; - - hud->m_richText->vtable->InsertColorChange(hud->m_richText, color); - } -} - -static Color GetHudSwatchColor(CHudChat* hud, LocalChatWriter::SwatchColor swatchColor) -{ - switch (swatchColor) - { - case LocalChatWriter::MainTextColor: - return hud->m_mainTextColor; - - case LocalChatWriter::SameTeamNameColor: - return hud->m_sameTeamColor; - - case LocalChatWriter::EnemyTeamNameColor: - return hud->m_enemyTeamColor; - - case LocalChatWriter::NetworkNameColor: - return hud->m_networkNameColor; - } - - return Color(0, 0, 0, 0); -} - -void LocalChatWriter::InsertSwatchColorChange(SwatchColor swatchColor) -{ - for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) - { - if (hud->m_unknownContext != (int)m_context) - continue; - hud->m_richText->vtable->InsertColorChange(hud->m_richText, GetHudSwatchColor(hud, swatchColor)); - } -} - -const char* LocalChatWriter::ApplyAnsiEscape(const char* escape) -{ - AnsiEscapeParser decoder(this); - while (true) - { - char* afterControlType = NULL; - unsigned long controlType = strtoul(escape, &afterControlType, 10); - - // Malformed cases: - // afterControlType = NULL: strtoul errored - // controlType = 0 and escape doesn't actually start with 0: wasn't a number - if (afterControlType == NULL || (controlType == 0 && escape[0] != '0')) - { - return escape; - } - - decoder.HandleVal(controlType); - - // m indicates the end of the sequence - if (afterControlType[0] == 'm') - { - return afterControlType + 1; - } - - // : or ; indicates more values remain, anything else is malformed - if (afterControlType[0] != ':' && afterControlType[0] != ';') - { - return afterControlType; - } - - escape = afterControlType + 1; - } -} - -void LocalChatWriter::InsertDefaultFade() -{ - float fadeLength = 0.f; - float fadeSustain = 0.f; - if ((*gGameSettings)->isChatEnabled) - { - fadeLength = (*gChatFadeLength)->value; - fadeSustain = (*gChatFadeSustain)->value; - } - - for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) - { - if (hud->m_unknownContext != (int)m_context) - continue; - hud->m_richText->vtable->InsertFade(hud->m_richText, fadeSustain, fadeLength); - } -} - -ON_DLL_LOAD_CLIENT("client.dll", LocalChatWriter, (CModule module)) -{ - gGameSettings = module.Offset(0x11BAA48).As(); - gChatFadeLength = module.Offset(0x11BAB78).As(); - gChatFadeSustain = module.Offset(0x11BAC08).As(); - CHudChat::allHuds = module.Offset(0x11BA9E8).As(); - - ConvertANSIToUnicode = module.Offset(0x7339A0).As(); -} diff --git a/NorthstarDLL/localchatwriter.h b/NorthstarDLL/localchatwriter.h deleted file mode 100644 index 0df0cac8..00000000 --- a/NorthstarDLL/localchatwriter.h +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once -#include "pch.h" -#include "color.h" - -class vgui_BaseRichText; - -class CHudChat -{ - public: - static CHudChat** allHuds; - - char unknown1[720]; - - Color m_sameTeamColor; - Color m_enemyTeamColor; - Color m_mainTextColor; - Color m_networkNameColor; - - char unknown2[12]; - - int m_unknownContext; - - char unknown3[8]; - - vgui_BaseRichText* m_richText; - - CHudChat* next; - CHudChat* previous; -}; - -class LocalChatWriter -{ - public: - enum Context - { - NetworkContext = 0, - GameContext = 1 - }; - enum SwatchColor - { - MainTextColor, - SameTeamNameColor, - EnemyTeamNameColor, - NetworkNameColor - }; - - explicit LocalChatWriter(Context context); - - // Custom chat writing with ANSI escape codes - void Write(const char* str); - void WriteLine(const char* str); - - // Low-level RichText access - void InsertChar(wchar_t ch); - void InsertText(const char* str); - void InsertText(const wchar_t* str); - void InsertColorChange(Color color); - void InsertSwatchColorChange(SwatchColor color); - - private: - Context m_context; - - const char* ApplyAnsiEscape(const char* escape); - void InsertDefaultFade(); -}; diff --git a/NorthstarDLL/logging.cpp b/NorthstarDLL/logging.cpp deleted file mode 100644 index 2184bd3f..00000000 --- a/NorthstarDLL/logging.cpp +++ /dev/null @@ -1,205 +0,0 @@ -#include "pch.h" -#include "logging.h" -#include "convar.h" -#include "concommand.h" -#include "nsprefix.h" -#include "tier0.h" -#include "spdlog/sinks/basic_file_sink.h" - -#include -#include - -AUTOHOOK_INIT() - -std::vector> loggers {}; - -namespace NS::log -{ - std::shared_ptr SCRIPT_UI; - std::shared_ptr SCRIPT_CL; - std::shared_ptr SCRIPT_SV; - - std::shared_ptr NATIVE_UI; - std::shared_ptr NATIVE_CL; - std::shared_ptr NATIVE_SV; - std::shared_ptr NATIVE_EN; - - std::shared_ptr fs; - std::shared_ptr rpak; - std::shared_ptr echo; - - std::shared_ptr NORTHSTAR; -}; // namespace NS::log - -// 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(¤tTime, (GetNorthstarPrefix() + "/logs/nslog%Y-%m-%d %H-%M-%S.txt").c_str()); - auto sink = std::make_shared(stream.str(), false); - sink->set_pattern("[%H:%M:%S] [%n] [%l] %v"); - for (auto& logger : loggers) - { - logger->sinks().push_back(sink); - } - 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 ExternalConsoleSink::sink_it_(const spdlog::details::log_msg& msg) -{ - throw std::runtime_error("sink_it_ called on SourceConsoleSink with pure log_msg. This is an error!"); -} - -void ExternalConsoleSink::custom_sink_it_(const custom_log_msg& msg) -{ - spdlog::memory_buf_t formatted; - spdlog::sinks::base_sink::formatter_->format(msg, formatted); - - std::string out = ""; - // if ansi colour is turned off, just use WriteConsoleA and return - if (!g_bSpdLog_UseAnsiColor) - { - out += fmt::to_string(formatted); - } - - // print to the console with colours - else - { - // get message string - std::string str = fmt::to_string(formatted); - - std::string levelColor = m_LogColours[msg.level]; - std::string name {msg.logger_name.begin(), msg.logger_name.end()}; - - std::string name_str = "[NAME]"; - int name_pos = str.find(name_str); - str.replace(name_pos, name_str.length(), msg.origin->ANSIColor + "[" + name + "]" + default_color); - - std::string level_str = "[LVL]"; - int level_pos = str.find(level_str); - str.replace(level_pos, level_str.length(), levelColor + "[" + std::string(level_names[msg.level]) + "]" + default_color); - - out += str; - } - // print the string to the console - this is definitely bad i think - HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); - auto ignored = WriteConsoleA(handle, out.c_str(), std::strlen(out.c_str()), nullptr, nullptr); - (void)ignored; -} - -void ExternalConsoleSink::flush_() -{ - std::cout << std::flush; -} - -void CustomSink::custom_log(const custom_log_msg& msg) -{ - std::lock_guard lock(mutex_); - custom_sink_it_(msg); -} - -void InitialiseConsole() -{ - if (AllocConsole() == FALSE) - { - std::cout << "[*] Failed to create a console window, maybe a console already exists?" << std::endl; - } - else - { - freopen("CONOUT$", "w", stdout); - freopen("CONOUT$", "w", stderr); - } - - // this if statement is adapted from r5sdk - if (!strstr(GetCommandLineA(), "-noansiclr")) - { - g_bSpdLog_UseAnsiColor = true; - DWORD dwMode = NULL; - HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); - - GetConsoleMode(hOutput, &dwMode); - dwMode |= ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING; - - if (!SetConsoleMode(hOutput, dwMode)) // Some editions of Windows have 'VirtualTerminalLevel' disabled by default. - { - // If 'VirtualTerminalLevel' can't be set, just disable ANSI color, since it wouldnt work anyway. - spdlog::warn("could not set VirtualTerminalLevel. Disabling color output"); - g_bSpdLog_UseAnsiColor = false; - } - } -} - -void RegisterCustomSink(std::shared_ptr sink) -{ - for (auto& logger : loggers) - { - logger->custom_sinks_.push_back(sink); - } -}; - -void InitialiseLogging() -{ - // create a logger, and set it to default - NS::log::NORTHSTAR = std::make_shared("NORTHSTAR", NS::Colors::NORTHSTAR, true); - NS::log::NORTHSTAR->sinks().clear(); - loggers.push_back(NS::log::NORTHSTAR); - spdlog::set_default_logger(NS::log::NORTHSTAR); - - // create our console sink - auto sink = std::make_shared(); - // set the pattern - if (g_bSpdLog_UseAnsiColor) - // dont put the log level in the pattern if we are using colours, as the colour will show the log level - sink->set_pattern("[%H:%M:%S] [NAME] [LVL] %v"); - else - sink->set_pattern("[%H:%M:%S] [%n] [%l] %v"); - - // add our sink to the logger - NS::log::NORTHSTAR->custom_sinks_.push_back(sink); - - NS::log::SCRIPT_UI = std::make_shared("SCRIPT UI", NS::Colors::SCRIPT_UI); - NS::log::SCRIPT_CL = std::make_shared("SCRIPT CL", NS::Colors::SCRIPT_CL); - NS::log::SCRIPT_SV = std::make_shared("SCRIPT SV", NS::Colors::SCRIPT_SV); - - NS::log::NATIVE_UI = std::make_shared("NATIVE UI", NS::Colors::NATIVE_UI); - NS::log::NATIVE_CL = std::make_shared("NATIVE CL", NS::Colors::NATIVE_CL); - NS::log::NATIVE_SV = std::make_shared("NATIVE SV", NS::Colors::NATIVE_SV); - NS::log::NATIVE_EN = std::make_shared("NATIVE EN", NS::Colors::NATIVE_ENGINE); - - NS::log::fs = std::make_shared("FILESYSTM", NS::Colors::FILESYSTEM); - NS::log::rpak = std::make_shared("RPAK_FSYS", NS::Colors::RPAK); - NS::log::echo = std::make_shared("ECHO", NS::Colors::ECHO); - - loggers.push_back(NS::log::SCRIPT_UI); - loggers.push_back(NS::log::SCRIPT_CL); - loggers.push_back(NS::log::SCRIPT_SV); - - loggers.push_back(NS::log::NATIVE_UI); - loggers.push_back(NS::log::NATIVE_CL); - loggers.push_back(NS::log::NATIVE_SV); - loggers.push_back(NS::log::NATIVE_EN); - - loggers.push_back(NS::log::fs); - loggers.push_back(NS::log::rpak); - loggers.push_back(NS::log::echo); -} diff --git a/NorthstarDLL/logging.h b/NorthstarDLL/logging.h deleted file mode 100644 index 78f65e18..00000000 --- a/NorthstarDLL/logging.h +++ /dev/null @@ -1,130 +0,0 @@ -#pragma once -#include "pch.h" -#include "spdlog/sinks/base_sink.h" -#include "spdlog/logger.h" -#include "color.h" - -void CreateLogFiles(); -void InitialiseLogging(); -void InitialiseConsole(); - -class ColoredLogger; - -struct custom_log_msg : spdlog::details::log_msg -{ - public: - custom_log_msg(ColoredLogger* origin, spdlog::details::log_msg msg) : origin(origin), spdlog::details::log_msg(msg) {} - - ColoredLogger* origin; -}; - -class CustomSink : public spdlog::sinks::base_sink -{ - public: - void custom_log(const custom_log_msg& msg); - virtual void custom_sink_it_(const custom_log_msg& msg) - { - throw std::runtime_error("Pure virtual call to CustomSink::custom_sink_it_"); - } -}; - -class ColoredLogger : public spdlog::logger -{ - public: - std::string ANSIColor; - SourceColor SRCColor; - - std::vector> custom_sinks_; - - ColoredLogger(std::string name, Color color, bool first = false) : spdlog::logger(*spdlog::default_logger()) - { - name_ = std::move(name); - if (!first) - { - custom_sinks_ = dynamic_pointer_cast(spdlog::default_logger())->custom_sinks_; - } - - ANSIColor = color.ToANSIColor(); - SRCColor = color.ToSourceColor(); - } - - void sink_it_(const spdlog::details::log_msg& msg) - { - custom_log_msg custom_msg {this, msg}; - - // Ugh - for (auto& sink : sinks_) - { - SPDLOG_TRY - { - sink->log(custom_msg); - } - SPDLOG_LOGGER_CATCH() - } - - for (auto& sink : custom_sinks_) - { - SPDLOG_TRY - { - sink->custom_log(custom_msg); - } - SPDLOG_LOGGER_CATCH() - } - - if (should_flush_(custom_msg)) - { - flush_(); - } - } -}; - -namespace NS::log -{ - // Squirrel - extern std::shared_ptr SCRIPT_UI; - extern std::shared_ptr SCRIPT_CL; - extern std::shared_ptr SCRIPT_SV; - - // Native code - extern std::shared_ptr NATIVE_UI; - extern std::shared_ptr NATIVE_CL; - extern std::shared_ptr NATIVE_SV; - extern std::shared_ptr NATIVE_EN; - - // File system - extern std::shared_ptr fs; - // RPak - extern std::shared_ptr rpak; - // Echo - extern std::shared_ptr echo; - - extern std::shared_ptr NORTHSTAR; -}; // namespace NS::log - -void RegisterCustomSink(std::shared_ptr sink); - -inline bool g_bSpdLog_UseAnsiColor = true; - -// Could maybe use some different names here, idk -static const char* level_names[] {"trac", "dbug", "info", "warn", "errr", "crit", "off"}; - -// spdlog logger, for cool colour things -class ExternalConsoleSink : public CustomSink -{ - private: - std::map m_LogColours = { - {spdlog::level::trace, NS::Colors::TRACE.ToANSIColor()}, - {spdlog::level::debug, NS::Colors::DEBUG.ToANSIColor()}, - {spdlog::level::info, NS::Colors::INFO.ToANSIColor()}, - {spdlog::level::warn, NS::Colors::WARN.ToANSIColor()}, - {spdlog::level::err, NS::Colors::ERR.ToANSIColor()}, - {spdlog::level::critical, NS::Colors::CRIT.ToANSIColor()}, - {spdlog::level::off, NS::Colors::OFF.ToANSIColor()}}; - - std::string default_color = "\033[39;49m"; - - protected: - void sink_it_(const spdlog::details::log_msg& msg) override; - void custom_sink_it_(const custom_log_msg& msg); - void flush_() override; -}; diff --git a/NorthstarDLL/logging/crashhandler.cpp b/NorthstarDLL/logging/crashhandler.cpp new file mode 100644 index 00000000..d4a54169 --- /dev/null +++ b/NorthstarDLL/logging/crashhandler.cpp @@ -0,0 +1,376 @@ +#include "pch.h" +#include "crashhandler.h" +#include "dedicated/dedicated.h" +#include "config/profile.h" +#include "util/version.h" +#include "mods/modmanager.h" + +#include + +HANDLE hExceptionFilter; + +std::shared_ptr storedException {}; + +#define RUNTIME_EXCEPTION 3765269347 +// clang format did this :/ +std::map ExceptionNames = { + {EXCEPTION_ACCESS_VIOLATION, "Access Violation"}, {EXCEPTION_IN_PAGE_ERROR, "Access Violation"}, + {EXCEPTION_ARRAY_BOUNDS_EXCEEDED, "Array bounds exceeded"}, {EXCEPTION_DATATYPE_MISALIGNMENT, "Datatype misalignment"}, + {EXCEPTION_FLT_DENORMAL_OPERAND, "Denormal operand"}, {EXCEPTION_FLT_DIVIDE_BY_ZERO, "Divide by zero (float)"}, + {EXCEPTION_FLT_INEXACT_RESULT, "Inexact float result"}, {EXCEPTION_FLT_INVALID_OPERATION, "Invalid operation"}, + {EXCEPTION_FLT_OVERFLOW, "Numeric overflow (float)"}, {EXCEPTION_FLT_STACK_CHECK, "Stack check"}, + {EXCEPTION_FLT_UNDERFLOW, "Numeric underflow (float)"}, {EXCEPTION_ILLEGAL_INSTRUCTION, "Illegal instruction"}, + {EXCEPTION_INT_DIVIDE_BY_ZERO, "Divide by zero (int)"}, {EXCEPTION_INT_OVERFLOW, "Numeric overfloat (int)"}, + {EXCEPTION_INVALID_DISPOSITION, "Invalid disposition"}, {EXCEPTION_NONCONTINUABLE_EXCEPTION, "Non-continuable exception"}, + {EXCEPTION_PRIV_INSTRUCTION, "Priviledged instruction"}, {EXCEPTION_STACK_OVERFLOW, "Stack overflow"}, + {RUNTIME_EXCEPTION, "Uncaught runtime exception:"}, +}; + +void PrintExceptionLog(ExceptionLog& exc) +{ + // General crash message + spdlog::error("Northstar version: {}", version); + spdlog::error("Northstar has crashed! a minidump has been written and exception info is available below:"); + if (g_pModManager) + { + spdlog::error("Loaded mods: "); + for (const auto& mod : g_pModManager->m_LoadedMods) + { + if (mod.m_bEnabled) + { + spdlog::error("{} {}", mod.Name, mod.Version); + } + } + } + spdlog::error(exc.cause); + // If this was a runtime error, print the message + if (exc.runtimeInfo.length() != 0) + spdlog::error("\"{}\"", exc.runtimeInfo); + spdlog::error("At: {} + {}", exc.trace[0].name, exc.trace[0].relativeAddress); + spdlog::error(""); + spdlog::error("Stack trace:"); + + // Generate format string for stack trace + std::stringstream formatString; + formatString << " {:<" << exc.longestModuleNameLength + 2 << "} {:<" << exc.longestRelativeAddressLength << "} {}"; + std::string guide = fmt::format(formatString.str(), "Module Name", "Offset", "Full Address"); + std::string line(guide.length() + 2, '-'); + spdlog::error(guide); + spdlog::error(line); + + for (const auto& module : exc.trace) + spdlog::error(formatString.str(), module.name, module.relativeAddress, module.address); + + // Print dump of most CPU registers + spdlog::error(""); + for (const auto& reg : exc.registerDump) + spdlog::error(reg); + + if (!IsDedicatedServer()) + MessageBoxA( + 0, + "Northstar has crashed! Crash info can be found in R2Northstar/logs", + "Northstar has crashed!", + MB_ICONERROR | MB_OK | MB_SYSTEMMODAL); +} + +std::string GetExceptionName(ExceptionLog& exc) +{ + const DWORD exceptionCode = exc.exceptionRecord.ExceptionCode; + auto name = ExceptionNames[exceptionCode]; + if (exceptionCode == EXCEPTION_ACCESS_VIOLATION || exceptionCode == EXCEPTION_IN_PAGE_ERROR) + { + std::stringstream returnString; + returnString << name << ": "; + + auto exceptionInfo0 = exc.exceptionRecord.ExceptionInformation[0]; + auto exceptionInfo1 = exc.exceptionRecord.ExceptionInformation[1]; + + if (!exceptionInfo0) + returnString << "Attempted to read from: 0x" << (void*)exceptionInfo1; + else if (exceptionInfo0 == 1) + returnString << "Attempted to write to: 0x" << (void*)exceptionInfo1; + else if (exceptionInfo0 == 8) + returnString << "Data Execution Prevention (DEP) at: 0x" << (void*)std::hex << exceptionInfo1; + else + returnString << "Unknown access violation at: 0x" << (void*)exceptionInfo1; + return returnString.str(); + } + return name; +} + +// Custom formatter for the Xmm registers +template <> struct fmt::formatter : fmt::formatter +{ + template auto format(const M128A& obj, FormatContext& ctx) + { + // Masking the top and bottom half of the long long + int v1 = obj.Low & INT_MAX; + int v2 = obj.Low >> 32; + int v3 = obj.High & INT_MAX; + int v4 = obj.High >> 32; + return fmt::format_to( + ctx.out(), + "[ {:G}, {:G}, {:G}, {:G}], [ 0x{:x}, 0x{:x}, 0x{:x}, 0x{:x} ]", + *reinterpret_cast(&v1), + *reinterpret_cast(&v2), + *reinterpret_cast(&v3), + *reinterpret_cast(&v4), + v1, + v2, + v3, + v4); + } +}; + +void GenerateTrace(ExceptionLog& exc, bool skipErrorHandlingFrames = true, int numSkipFrames = 0) +{ + + MODULEINFO crashedModuleInfo; + GetModuleInformation(GetCurrentProcess(), exc.crashedModule, &crashedModuleInfo, sizeof(crashedModuleInfo)); + + char crashedModuleFullName[MAX_PATH]; + GetModuleFileNameExA(GetCurrentProcess(), exc.crashedModule, crashedModuleFullName, MAX_PATH); + char* crashedModuleName = strrchr(crashedModuleFullName, '\\') + 1; + + DWORD64 crashedModuleOffset = ((DWORD64)exc.exceptionRecord.ExceptionAddress) - ((DWORD64)crashedModuleInfo.lpBaseOfDll); + + PVOID framesToCapture[62]; + int frames = RtlCaptureStackBackTrace(0, 62, framesToCapture, NULL); + bool haveSkippedErrorHandlingFrames = false; + + for (int i = 0; i < frames; i++) + { + + HMODULE backtraceModuleHandle; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, static_cast(framesToCapture[i]), &backtraceModuleHandle); + + char backtraceModuleFullName[MAX_PATH]; + GetModuleFileNameExA(GetCurrentProcess(), backtraceModuleHandle, backtraceModuleFullName, MAX_PATH); + char* backtraceModuleName = strrchr(backtraceModuleFullName, '\\') + 1; + + if (!haveSkippedErrorHandlingFrames) + { + if (!strncmp(backtraceModuleFullName, crashedModuleFullName, MAX_PATH) && + !strncmp(backtraceModuleName, crashedModuleName, MAX_PATH)) + { + haveSkippedErrorHandlingFrames = true; + } + else + { + continue; + } + } + + if (numSkipFrames > 0) + { + numSkipFrames--; + continue; + } + + void* actualAddress = (void*)framesToCapture[i]; + void* relativeAddress = (void*)(uintptr_t(actualAddress) - uintptr_t(backtraceModuleHandle)); + std::string s_moduleName {backtraceModuleName}; + std::string s_relativeAddress {fmt::format("{}", relativeAddress)}; + // These are used for formatting later on + if (s_moduleName.length() > exc.longestModuleNameLength) + { + exc.longestModuleNameLength = s_moduleName.length(); + } + if (s_relativeAddress.length() > exc.longestRelativeAddressLength) + { + exc.longestRelativeAddressLength = s_relativeAddress.length(); + } + + exc.trace.push_back(BacktraceModule {s_moduleName, s_relativeAddress, fmt::format("{}", actualAddress)}); + } + + CONTEXT* exceptionContext = &exc.contextRecord; + + exc.registerDump.push_back(fmt::format("Flags: 0b{0:b}", exceptionContext->ContextFlags)); + exc.registerDump.push_back(fmt::format("RIP: 0x{0:x}", exceptionContext->Rip)); + exc.registerDump.push_back(fmt::format("CS : 0x{0:x}", exceptionContext->SegCs)); + exc.registerDump.push_back(fmt::format("DS : 0x{0:x}", exceptionContext->SegDs)); + exc.registerDump.push_back(fmt::format("ES : 0x{0:x}", exceptionContext->SegEs)); + exc.registerDump.push_back(fmt::format("SS : 0x{0:x}", exceptionContext->SegSs)); + exc.registerDump.push_back(fmt::format("FS : 0x{0:x}", exceptionContext->SegFs)); + exc.registerDump.push_back(fmt::format("GS : 0x{0:x}", exceptionContext->SegGs)); + + exc.registerDump.push_back(fmt::format("RAX: 0x{0:x}", exceptionContext->Rax)); + exc.registerDump.push_back(fmt::format("RBX: 0x{0:x}", exceptionContext->Rbx)); + exc.registerDump.push_back(fmt::format("RCX: 0x{0:x}", exceptionContext->Rcx)); + exc.registerDump.push_back(fmt::format("RDX: 0x{0:x}", exceptionContext->Rdx)); + exc.registerDump.push_back(fmt::format("RSI: 0x{0:x}", exceptionContext->Rsi)); + exc.registerDump.push_back(fmt::format("RDI: 0x{0:x}", exceptionContext->Rdi)); + exc.registerDump.push_back(fmt::format("RBP: 0x{0:x}", exceptionContext->Rbp)); + exc.registerDump.push_back(fmt::format("RSP: 0x{0:x}", exceptionContext->Rsp)); + exc.registerDump.push_back(fmt::format("R8 : 0x{0:x}", exceptionContext->R8)); + exc.registerDump.push_back(fmt::format("R9 : 0x{0:x}", exceptionContext->R9)); + exc.registerDump.push_back(fmt::format("R10: 0x{0:x}", exceptionContext->R10)); + exc.registerDump.push_back(fmt::format("R11: 0x{0:x}", exceptionContext->R11)); + exc.registerDump.push_back(fmt::format("R12: 0x{0:x}", exceptionContext->R12)); + exc.registerDump.push_back(fmt::format("R13: 0x{0:x}", exceptionContext->R13)); + exc.registerDump.push_back(fmt::format("R14: 0x{0:x}", exceptionContext->R14)); + exc.registerDump.push_back(fmt::format("R15: 0x{0:x}", exceptionContext->R15)); + + exc.registerDump.push_back(fmt::format("Xmm0 : {}", exceptionContext->Xmm0)); + exc.registerDump.push_back(fmt::format("Xmm1 : {}", exceptionContext->Xmm1)); + exc.registerDump.push_back(fmt::format("Xmm2 : {}", exceptionContext->Xmm2)); + exc.registerDump.push_back(fmt::format("Xmm3 : {}", exceptionContext->Xmm3)); + exc.registerDump.push_back(fmt::format("Xmm4 : {}", exceptionContext->Xmm4)); + exc.registerDump.push_back(fmt::format("Xmm5 : {}", exceptionContext->Xmm5)); + exc.registerDump.push_back(fmt::format("Xmm6 : {}", exceptionContext->Xmm6)); + exc.registerDump.push_back(fmt::format("Xmm7 : {}", exceptionContext->Xmm7)); + exc.registerDump.push_back(fmt::format("Xmm8 : {}", exceptionContext->Xmm8)); + exc.registerDump.push_back(fmt::format("Xmm9 : {}", exceptionContext->Xmm9)); + exc.registerDump.push_back(fmt::format("Xmm10: {}", exceptionContext->Xmm10)); + exc.registerDump.push_back(fmt::format("Xmm11: {}", exceptionContext->Xmm11)); + exc.registerDump.push_back(fmt::format("Xmm12: {}", exceptionContext->Xmm12)); + exc.registerDump.push_back(fmt::format("Xmm13: {}", exceptionContext->Xmm13)); + exc.registerDump.push_back(fmt::format("Xmm14: {}", exceptionContext->Xmm14)); + exc.registerDump.push_back(fmt::format("Xmm15: {}", exceptionContext->Xmm15)); +} + +void CreateMiniDump(EXCEPTION_POINTERS* exceptionInfo) +{ + time_t time = std::time(nullptr); + tm currentTime = *std::localtime(&time); + std::stringstream stream; + stream << std::put_time(¤tTime, (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()); +} + +long GenerateExceptionLog(EXCEPTION_POINTERS* exceptionInfo) +{ + storedException->exceptionRecord = *exceptionInfo->ExceptionRecord; + storedException->contextRecord = *exceptionInfo->ContextRecord; + const DWORD exceptionCode = exceptionInfo->ExceptionRecord->ExceptionCode; + + void* exceptionAddress = exceptionInfo->ExceptionRecord->ExceptionAddress; + + storedException->cause = GetExceptionName(*storedException); + + HMODULE crashedModuleHandle; + GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, static_cast(exceptionAddress), &crashedModuleHandle); + + storedException->crashedModule = crashedModuleHandle; + + // When encountering a runtime exception, we store the exception to be displayed later + // We then have to return EXCEPTION_CONTINUE_SEARCH so that our runtime handler may be called + // This might possibly cause some issues if client and server are crashing at the same time, but honestly i don't care + if (exceptionCode == RUNTIME_EXCEPTION) + { + GenerateTrace(*storedException, false, 2); + storedException = storedException; + return EXCEPTION_CONTINUE_SEARCH; + } + + GenerateTrace(*storedException, true, 0); + CreateMiniDump(exceptionInfo); + PrintExceptionLog(*storedException); + return EXCEPTION_EXECUTE_HANDLER; +} + +long __stdcall ExceptionFilter(EXCEPTION_POINTERS* exceptionInfo) +{ + if (!IsDebuggerPresent()) + { + // Check if we are capable of handling this type of exception + if (ExceptionNames.find(exceptionInfo->ExceptionRecord->ExceptionCode) == ExceptionNames.end()) + return EXCEPTION_CONTINUE_SEARCH; + if (exceptionInfo->ExceptionRecord->ExceptionCode == RUNTIME_EXCEPTION) + { + storedException = std::make_shared(); + storedException->exceptionRecord = *exceptionInfo->ExceptionRecord; + storedException->contextRecord = *exceptionInfo->ContextRecord; + } + else + { + CreateMiniDump(exceptionInfo); + return GenerateExceptionLog(exceptionInfo); + } + } + + return EXCEPTION_EXECUTE_HANDLER; +} + +void RuntimeExceptionHandler() +{ + auto ptr = std::current_exception(); + if (ptr) + { + try + { + // This is to generate an actual std::exception object that we can then inspect + std::rethrow_exception(ptr); + } + catch (std::exception& e) + { + storedException->runtimeInfo = e.what(); + } + catch (...) + { + storedException->runtimeInfo = "Unknown runtime exception type"; + } + EXCEPTION_POINTERS test {}; + test.ContextRecord = &storedException->contextRecord; + test.ExceptionRecord = &storedException->exceptionRecord; + CreateMiniDump(&test); + GenerateExceptionLog(&test); + PrintExceptionLog(*storedException); + exit(-1); + } + else + { + spdlog::error( + "std::current_exception() returned nullptr while being handled by RuntimeExceptionHandler. This should never happen!"); + std::abort(); + } +} + +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); + std::set_terminate(RuntimeExceptionHandler); +} + +void RemoveCrashHandler() +{ + RemoveVectoredExceptionHandler(hExceptionFilter); +} diff --git a/NorthstarDLL/logging/crashhandler.h b/NorthstarDLL/logging/crashhandler.h new file mode 100644 index 00000000..4d8a59ce --- /dev/null +++ b/NorthstarDLL/logging/crashhandler.h @@ -0,0 +1,26 @@ +#pragma once + +void InitialiseCrashHandler(); +void RemoveCrashHandler(); + +struct BacktraceModule +{ + std::string name; + std::string relativeAddress; + std::string address; +}; + +struct ExceptionLog +{ + std::string cause; + HMODULE crashedModule; + EXCEPTION_RECORD exceptionRecord; + CONTEXT contextRecord; + std::vector trace; + std::vector registerDump; + + std::string runtimeInfo; + + int longestModuleNameLength; + int longestRelativeAddressLength; +}; diff --git a/NorthstarDLL/logging/logging.cpp b/NorthstarDLL/logging/logging.cpp new file mode 100644 index 00000000..6bb57170 --- /dev/null +++ b/NorthstarDLL/logging/logging.cpp @@ -0,0 +1,205 @@ +#include "pch.h" +#include "logging.h" +#include "core/convar/convar.h" +#include "core/convar/concommand.h" +#include "config/profile.h" +#include "core/tier0.h" +#include "spdlog/sinks/basic_file_sink.h" + +#include +#include + +AUTOHOOK_INIT() + +std::vector> loggers {}; + +namespace NS::log +{ + std::shared_ptr SCRIPT_UI; + std::shared_ptr SCRIPT_CL; + std::shared_ptr SCRIPT_SV; + + std::shared_ptr NATIVE_UI; + std::shared_ptr NATIVE_CL; + std::shared_ptr NATIVE_SV; + std::shared_ptr NATIVE_EN; + + std::shared_ptr fs; + std::shared_ptr rpak; + std::shared_ptr echo; + + std::shared_ptr NORTHSTAR; +}; // namespace NS::log + +// 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(¤tTime, (GetNorthstarPrefix() + "/logs/nslog%Y-%m-%d %H-%M-%S.txt").c_str()); + auto sink = std::make_shared(stream.str(), false); + sink->set_pattern("[%H:%M:%S] [%n] [%l] %v"); + for (auto& logger : loggers) + { + logger->sinks().push_back(sink); + } + 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 ExternalConsoleSink::sink_it_(const spdlog::details::log_msg& msg) +{ + throw std::runtime_error("sink_it_ called on SourceConsoleSink with pure log_msg. This is an error!"); +} + +void ExternalConsoleSink::custom_sink_it_(const custom_log_msg& msg) +{ + spdlog::memory_buf_t formatted; + spdlog::sinks::base_sink::formatter_->format(msg, formatted); + + std::string out = ""; + // if ansi colour is turned off, just use WriteConsoleA and return + if (!g_bSpdLog_UseAnsiColor) + { + out += fmt::to_string(formatted); + } + + // print to the console with colours + else + { + // get message string + std::string str = fmt::to_string(formatted); + + std::string levelColor = m_LogColours[msg.level]; + std::string name {msg.logger_name.begin(), msg.logger_name.end()}; + + std::string name_str = "[NAME]"; + int name_pos = str.find(name_str); + str.replace(name_pos, name_str.length(), msg.origin->ANSIColor + "[" + name + "]" + default_color); + + std::string level_str = "[LVL]"; + int level_pos = str.find(level_str); + str.replace(level_pos, level_str.length(), levelColor + "[" + std::string(level_names[msg.level]) + "]" + default_color); + + out += str; + } + // print the string to the console - this is definitely bad i think + HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); + auto ignored = WriteConsoleA(handle, out.c_str(), std::strlen(out.c_str()), nullptr, nullptr); + (void)ignored; +} + +void ExternalConsoleSink::flush_() +{ + std::cout << std::flush; +} + +void CustomSink::custom_log(const custom_log_msg& msg) +{ + std::lock_guard lock(mutex_); + custom_sink_it_(msg); +} + +void InitialiseConsole() +{ + if (AllocConsole() == FALSE) + { + std::cout << "[*] Failed to create a console window, maybe a console already exists?" << std::endl; + } + else + { + freopen("CONOUT$", "w", stdout); + freopen("CONOUT$", "w", stderr); + } + + // this if statement is adapted from r5sdk + if (!strstr(GetCommandLineA(), "-noansiclr")) + { + g_bSpdLog_UseAnsiColor = true; + DWORD dwMode = NULL; + HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); + + GetConsoleMode(hOutput, &dwMode); + dwMode |= ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING; + + if (!SetConsoleMode(hOutput, dwMode)) // Some editions of Windows have 'VirtualTerminalLevel' disabled by default. + { + // If 'VirtualTerminalLevel' can't be set, just disable ANSI color, since it wouldnt work anyway. + spdlog::warn("could not set VirtualTerminalLevel. Disabling color output"); + g_bSpdLog_UseAnsiColor = false; + } + } +} + +void RegisterCustomSink(std::shared_ptr sink) +{ + for (auto& logger : loggers) + { + logger->custom_sinks_.push_back(sink); + } +}; + +void InitialiseLogging() +{ + // create a logger, and set it to default + NS::log::NORTHSTAR = std::make_shared("NORTHSTAR", NS::Colors::NORTHSTAR, true); + NS::log::NORTHSTAR->sinks().clear(); + loggers.push_back(NS::log::NORTHSTAR); + spdlog::set_default_logger(NS::log::NORTHSTAR); + + // create our console sink + auto sink = std::make_shared(); + // set the pattern + if (g_bSpdLog_UseAnsiColor) + // dont put the log level in the pattern if we are using colours, as the colour will show the log level + sink->set_pattern("[%H:%M:%S] [NAME] [LVL] %v"); + else + sink->set_pattern("[%H:%M:%S] [%n] [%l] %v"); + + // add our sink to the logger + NS::log::NORTHSTAR->custom_sinks_.push_back(sink); + + NS::log::SCRIPT_UI = std::make_shared("SCRIPT UI", NS::Colors::SCRIPT_UI); + NS::log::SCRIPT_CL = std::make_shared("SCRIPT CL", NS::Colors::SCRIPT_CL); + NS::log::SCRIPT_SV = std::make_shared("SCRIPT SV", NS::Colors::SCRIPT_SV); + + NS::log::NATIVE_UI = std::make_shared("NATIVE UI", NS::Colors::NATIVE_UI); + NS::log::NATIVE_CL = std::make_shared("NATIVE CL", NS::Colors::NATIVE_CL); + NS::log::NATIVE_SV = std::make_shared("NATIVE SV", NS::Colors::NATIVE_SV); + NS::log::NATIVE_EN = std::make_shared("NATIVE EN", NS::Colors::NATIVE_ENGINE); + + NS::log::fs = std::make_shared("FILESYSTM", NS::Colors::FILESYSTEM); + NS::log::rpak = std::make_shared("RPAK_FSYS", NS::Colors::RPAK); + NS::log::echo = std::make_shared("ECHO", NS::Colors::ECHO); + + loggers.push_back(NS::log::SCRIPT_UI); + loggers.push_back(NS::log::SCRIPT_CL); + loggers.push_back(NS::log::SCRIPT_SV); + + loggers.push_back(NS::log::NATIVE_UI); + loggers.push_back(NS::log::NATIVE_CL); + loggers.push_back(NS::log::NATIVE_SV); + loggers.push_back(NS::log::NATIVE_EN); + + loggers.push_back(NS::log::fs); + loggers.push_back(NS::log::rpak); + loggers.push_back(NS::log::echo); +} diff --git a/NorthstarDLL/logging/logging.h b/NorthstarDLL/logging/logging.h new file mode 100644 index 00000000..af20c3e8 --- /dev/null +++ b/NorthstarDLL/logging/logging.h @@ -0,0 +1,130 @@ +#pragma once +#include "pch.h" +#include "spdlog/sinks/base_sink.h" +#include "spdlog/logger.h" +#include "core/math/color.h" + +void CreateLogFiles(); +void InitialiseLogging(); +void InitialiseConsole(); + +class ColoredLogger; + +struct custom_log_msg : spdlog::details::log_msg +{ + public: + custom_log_msg(ColoredLogger* origin, spdlog::details::log_msg msg) : origin(origin), spdlog::details::log_msg(msg) {} + + ColoredLogger* origin; +}; + +class CustomSink : public spdlog::sinks::base_sink +{ + public: + void custom_log(const custom_log_msg& msg); + virtual void custom_sink_it_(const custom_log_msg& msg) + { + throw std::runtime_error("Pure virtual call to CustomSink::custom_sink_it_"); + } +}; + +class ColoredLogger : public spdlog::logger +{ + public: + std::string ANSIColor; + SourceColor SRCColor; + + std::vector> custom_sinks_; + + ColoredLogger(std::string name, Color color, bool first = false) : spdlog::logger(*spdlog::default_logger()) + { + name_ = std::move(name); + if (!first) + { + custom_sinks_ = dynamic_pointer_cast(spdlog::default_logger())->custom_sinks_; + } + + ANSIColor = color.ToANSIColor(); + SRCColor = color.ToSourceColor(); + } + + void sink_it_(const spdlog::details::log_msg& msg) + { + custom_log_msg custom_msg {this, msg}; + + // Ugh + for (auto& sink : sinks_) + { + SPDLOG_TRY + { + sink->log(custom_msg); + } + SPDLOG_LOGGER_CATCH() + } + + for (auto& sink : custom_sinks_) + { + SPDLOG_TRY + { + sink->custom_log(custom_msg); + } + SPDLOG_LOGGER_CATCH() + } + + if (should_flush_(custom_msg)) + { + flush_(); + } + } +}; + +namespace NS::log +{ + // Squirrel + extern std::shared_ptr SCRIPT_UI; + extern std::shared_ptr SCRIPT_CL; + extern std::shared_ptr SCRIPT_SV; + + // Native code + extern std::shared_ptr NATIVE_UI; + extern std::shared_ptr NATIVE_CL; + extern std::shared_ptr NATIVE_SV; + extern std::shared_ptr NATIVE_EN; + + // File system + extern std::shared_ptr fs; + // RPak + extern std::shared_ptr rpak; + // Echo + extern std::shared_ptr echo; + + extern std::shared_ptr NORTHSTAR; +}; // namespace NS::log + +void RegisterCustomSink(std::shared_ptr sink); + +inline bool g_bSpdLog_UseAnsiColor = true; + +// Could maybe use some different names here, idk +static const char* level_names[] {"trac", "dbug", "info", "warn", "errr", "crit", "off"}; + +// spdlog logger, for cool colour things +class ExternalConsoleSink : public CustomSink +{ + private: + std::map m_LogColours = { + {spdlog::level::trace, NS::Colors::TRACE.ToANSIColor()}, + {spdlog::level::debug, NS::Colors::DEBUG.ToANSIColor()}, + {spdlog::level::info, NS::Colors::INFO.ToANSIColor()}, + {spdlog::level::warn, NS::Colors::WARN.ToANSIColor()}, + {spdlog::level::err, NS::Colors::ERR.ToANSIColor()}, + {spdlog::level::critical, NS::Colors::CRIT.ToANSIColor()}, + {spdlog::level::off, NS::Colors::OFF.ToANSIColor()}}; + + std::string default_color = "\033[39;49m"; + + protected: + void sink_it_(const spdlog::details::log_msg& msg) override; + void custom_sink_it_(const custom_log_msg& msg); + void flush_() override; +}; diff --git a/NorthstarDLL/logging/loghooks.cpp b/NorthstarDLL/logging/loghooks.cpp new file mode 100644 index 00000000..d09f23cc --- /dev/null +++ b/NorthstarDLL/logging/loghooks.cpp @@ -0,0 +1,262 @@ +#include "pch.h" +#include "logging.h" +#include "loghooks.h" +#include "core/convar/convar.h" +#include "core/convar/concommand.h" +#include "core/math/bitbuf.h" +#include "config/profile.h" +#include "core/tier0.h" +#include "squirrel/squirrel.h" +#include +#include + +AUTOHOOK_INIT() + +ConVar* Cvar_spewlog_enable; +ConVar* Cvar_cl_showtextmsg; + +enum class TextMsgPrintType_t +{ + HUD_PRINTNOTIFY = 1, + HUD_PRINTCONSOLE, + HUD_PRINTTALK, + HUD_PRINTCENTER +}; + +class ICenterPrint +{ + public: + virtual void ctor() = 0; + virtual void Clear(void) = 0; + virtual void ColorPrint(int r, int g, int b, int a, wchar_t* text) = 0; + virtual void ColorPrint(int r, int g, int b, int a, char* text) = 0; + virtual void Print(wchar_t* text) = 0; + virtual void Print(char* text) = 0; + virtual void SetTextColor(int r, int g, int b, int a) = 0; +}; + +enum class SpewType_t +{ + SPEW_MESSAGE = 0, + + SPEW_WARNING, + SPEW_ASSERT, + SPEW_ERROR, + SPEW_LOG, + + SPEW_TYPE_COUNT +}; + +const std::unordered_map 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"}}; + +// these are used to define the base text colour for these things +const std::unordered_map PrintSpewLevels = { + {SpewType_t::SPEW_MESSAGE, spdlog::level::level_enum::info}, + {SpewType_t::SPEW_WARNING, spdlog::level::level_enum::warn}, + {SpewType_t::SPEW_ASSERT, spdlog::level::level_enum::err}, + {SpewType_t::SPEW_ERROR, spdlog::level::level_enum::err}, + {SpewType_t::SPEW_LOG, spdlog::level::level_enum::info}}; + +const std::unordered_map PrintSpewTypes_Short = { + {SpewType_t::SPEW_MESSAGE, 'M'}, + {SpewType_t::SPEW_WARNING, 'W'}, + {SpewType_t::SPEW_ASSERT, 'A'}, + {SpewType_t::SPEW_ERROR, 'E'}, + {SpewType_t::SPEW_LOG, 'L'}}; + +ICenterPrint* pInternalCenterPrint = NULL; + +// 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)); + + if (!Cvar_cl_showtextmsg->GetBool()) + return; + + switch (msg_dest) + { + case TextMsgPrintType_t::HUD_PRINTCENTER: + pInternalCenterPrint->Print(text); + break; + + default: + spdlog::warn("Unimplemented TextMsg type {}! printing to console", msg_dest); + [[fallthrough]]; + + 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; + } +} + +// clang-format off +AUTOHOOK(Hook_fprintf, engine.dll + 0x51B1F0, +int,, (void* const stream, const char* const format, ...)) +// clang-format on +{ + va_list va; + va_start(va, format); + + SQChar buf[1024]; + int charsWritten = vsnprintf_s(buf, _TRUNCATE, format, va); + + if (charsWritten > 0) + { + if (buf[charsWritten - 1] == '\n') + buf[charsWritten - 1] = '\0'; + NS::log::NATIVE_EN->info("{}", buf); + } + + va_end(va); + return 0; +} + +// clang-format off +AUTOHOOK(ConCommand_echo, engine.dll + 0x123680, +void,, (const CCommand& arg)) +// clang-format on +{ + if (arg.ArgC() >= 2) + NS::log::echo->info("{}", arg.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 = PrintSpewTypes.at(type); + char formatted[2048] = {0}; + 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 + // so we use this instead + for (int i = 0; format[i]; i++) + { + if (format[i] == '%') + { + switch (format[i + 1]) + { + // this is fucking awful lol + case 'd': + case 'i': + case 'u': + case 'x': + case 'X': + case 'f': + case 'F': + case 'g': + case 'G': + case 'a': + case 'A': + case 'c': + case 's': + case 'p': + case 'n': + case '%': + case '-': + case '+': + case ' ': + case '#': + case '*': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + break; + + default: + { + bShouldFormat = false; + break; + } + } + } + } + + 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') + formatted[endpos - 1] = '\0'; // cut off repeated newline + + NS::log::NATIVE_SV->log(PrintSpewLevels.at(type), "{}", formatted); +} + +// 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; + + va_start(list, text); + vsprintf_s(formatted, text, list); + va_end(list); + + auto endpos = strlen(formatted); + if (formatted[endpos - 1] == '\n') + formatted[endpos - 1] = '\0'; // cut off repeated newline + + spdlog::info(formatted); +} + +// clang-format off +AUTOHOOK(CClientState_ProcessPrint, engine.dll + 0x1A1530, +bool,, (void* thisptr, uintptr_t msg)) +// clang-format on +{ + char* text = *(char**)(msg + 0x20); + + auto endpos = strlen(text); + if (text[endpos - 1] == '\n') + text[endpos - 1] = '\0'; // cut off repeated newline + + spdlog::info(text); + return true; +} + +ON_DLL_LOAD_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(); +} diff --git a/NorthstarDLL/logging/loghooks.h b/NorthstarDLL/logging/loghooks.h new file mode 100644 index 00000000..6f70f09b --- /dev/null +++ b/NorthstarDLL/logging/loghooks.h @@ -0,0 +1 @@ +#pragma once diff --git a/NorthstarDLL/logging/sourceconsole.cpp b/NorthstarDLL/logging/sourceconsole.cpp new file mode 100644 index 00000000..62b41e1a --- /dev/null +++ b/NorthstarDLL/logging/sourceconsole.cpp @@ -0,0 +1,92 @@ +#include "pch.h" +#include "core/convar/convar.h" +#include "sourceconsole.h" +#include "shared/sourceinterface.h" +#include "core/convar/concommand.h" +#include "util/printcommands.h" + +SourceInterface* g_pSourceGameConsole; + +void ConCommand_toggleconsole(const CCommand& arg) +{ + if ((*g_pSourceGameConsole)->IsConsoleVisible()) + (*g_pSourceGameConsole)->Hide(); + else + (*g_pSourceGameConsole)->Activate(); +} + +void ConCommand_showconsole(const CCommand& arg) +{ + (*g_pSourceGameConsole)->Activate(); +} + +void ConCommand_hideconsole(const CCommand& arg) +{ + (*g_pSourceGameConsole)->Hide(); +} + +void SourceConsoleSink::custom_sink_it_(const custom_log_msg& msg) +{ + if (!(*g_pSourceGameConsole)->m_bInitialized) + return; + + spdlog::memory_buf_t formatted; + spdlog::sinks::base_sink::formatter_->format(msg, formatted); + + // get message string + std::string str = fmt::to_string(formatted); + + SourceColor levelColor = m_LogColours[msg.level]; + std::string name {msg.logger_name.begin(), msg.logger_name.end()}; + + (*g_pSourceGameConsole)->m_pConsole->m_pConsolePanel->ColorPrint(msg.origin->SRCColor, ("[" + name + "]").c_str()); + (*g_pSourceGameConsole)->m_pConsole->m_pConsolePanel->Print(" "); + (*g_pSourceGameConsole)->m_pConsole->m_pConsolePanel->ColorPrint(levelColor, ("[" + std::string(level_names[msg.level]) + "]").c_str()); + (*g_pSourceGameConsole)->m_pConsole->m_pConsolePanel->Print(" "); + (*g_pSourceGameConsole)->m_pConsole->m_pConsolePanel->Print(fmt::to_string(formatted).c_str()); +} + +void SourceConsoleSink::sink_it_(const spdlog::details::log_msg& msg) +{ + throw std::runtime_error("sink_it_ called on SourceConsoleSink with pure log_msg. This is an error!"); +} + +void SourceConsoleSink::flush_() {} + +// clang-format off +HOOK(OnCommandSubmittedHook, OnCommandSubmitted, +void, __fastcall, (CConsoleDialog* consoleDialog, const char* pCommand)) +// clang-format on +{ + consoleDialog->m_pConsolePanel->Print("] "); + consoleDialog->m_pConsolePanel->Print(pCommand); + consoleDialog->m_pConsolePanel->Print("\n"); + + TryPrintCvarHelpForCommand(pCommand); + + OnCommandSubmitted(consoleDialog, pCommand); +} + +// called from sourceinterface.cpp in client createinterface hooks, on GameClientExports001 +void InitialiseConsoleOnInterfaceCreation() +{ + (*g_pSourceGameConsole)->Initialize(); + // hook OnCommandSubmitted so we print inputted commands + OnCommandSubmittedHook.Dispatch((*g_pSourceGameConsole)->m_pConsole->m_vtable->OnCommandSubmitted); + + auto consoleSink = std::make_shared(); + if (g_bSpdLog_UseAnsiColor) + consoleSink->set_pattern("%v"); // no need to include the level in the game console, the text colour signifies it anyway + else + consoleSink->set_pattern("[%n] [%l] %v"); // no colour, so we should show the level for colourblind people + RegisterCustomSink(consoleSink); +} + +ON_DLL_LOAD_CLIENT_RELIESON("client.dll", SourceConsole, ConCommand, (CModule module)) +{ + g_pSourceGameConsole = new SourceInterface("client.dll", "GameConsole004"); + + RegisterConCommand("toggleconsole", ConCommand_toggleconsole, "Show/hide the console.", FCVAR_DONTRECORD); + RegisterConCommand("showconsole", ConCommand_showconsole, "Show the console.", FCVAR_DONTRECORD); + RegisterConCommand("hideconsole", ConCommand_hideconsole, "Hide the console.", FCVAR_DONTRECORD); +} diff --git a/NorthstarDLL/logging/sourceconsole.h b/NorthstarDLL/logging/sourceconsole.h new file mode 100644 index 00000000..00498054 --- /dev/null +++ b/NorthstarDLL/logging/sourceconsole.h @@ -0,0 +1,86 @@ +#pragma once +#include "pch.h" +#include "shared/sourceinterface.h" +#include "spdlog/sinks/base_sink.h" +#include + +class EditablePanel +{ + public: + virtual ~EditablePanel() = 0; + unsigned char unknown[0x2B0]; +}; + +class IConsoleDisplayFunc +{ + public: + virtual void ColorPrint(const SourceColor& clr, const char* pMessage) = 0; + virtual void Print(const char* pMessage) = 0; + virtual void DPrint(const char* pMessage) = 0; +}; + +class CConsolePanel : public EditablePanel, public IConsoleDisplayFunc +{ +}; + +class CConsoleDialog +{ + public: + struct VTable + { + void* unknown[298]; + void (*OnCommandSubmitted)(CConsoleDialog* consoleDialog, const char* pCommand); + }; + + VTable* m_vtable; + unsigned char unknown[0x398]; + CConsolePanel* m_pConsolePanel; +}; + +class CGameConsole +{ + public: + virtual ~CGameConsole() = 0; + + // activates the console, makes it visible and brings it to the foreground + virtual void Activate() = 0; + + virtual void Initialize() = 0; + + // hides the console + virtual void Hide() = 0; + + // clears the console + virtual void Clear() = 0; + + // return true if the console has focus + virtual bool IsConsoleVisible() = 0; + + virtual void SetParent(int parent) = 0; + + bool m_bInitialized; + CConsoleDialog* m_pConsole; +}; + +extern SourceInterface* g_pSourceGameConsole; + +// spdlog logger +class SourceConsoleSink : public CustomSink +{ + private: + std::map m_LogColours = { + {spdlog::level::trace, NS::Colors::TRACE.ToSourceColor()}, + {spdlog::level::debug, NS::Colors::DEBUG.ToSourceColor()}, + {spdlog::level::info, NS::Colors::INFO.ToSourceColor()}, + {spdlog::level::warn, NS::Colors::WARN.ToSourceColor()}, + {spdlog::level::err, NS::Colors::ERR.ToSourceColor()}, + {spdlog::level::critical, NS::Colors::CRIT.ToSourceColor()}, + {spdlog::level::off, NS::Colors::OFF.ToSourceColor()}}; + + protected: + void custom_sink_it_(const custom_log_msg& msg); + void sink_it_(const spdlog::details::log_msg& msg) override; + void flush_() override; +}; + +void InitialiseConsoleOnInterfaceCreation(); diff --git a/NorthstarDLL/loghooks.cpp b/NorthstarDLL/loghooks.cpp deleted file mode 100644 index a352f4c1..00000000 --- a/NorthstarDLL/loghooks.cpp +++ /dev/null @@ -1,263 +0,0 @@ -#include "pch.h" -#include "logging.h" -#include "loghooks.h" -#include "convar.h" -#include "concommand.h" -#include "bitbuf.h" -#include "nsprefix.h" -#include "tier0.h" -#include "squirrel.h" - -#include -#include - -AUTOHOOK_INIT() - -ConVar* Cvar_spewlog_enable; -ConVar* Cvar_cl_showtextmsg; - -enum class TextMsgPrintType_t -{ - HUD_PRINTNOTIFY = 1, - HUD_PRINTCONSOLE, - HUD_PRINTTALK, - HUD_PRINTCENTER -}; - -class ICenterPrint -{ - public: - virtual void ctor() = 0; - virtual void Clear(void) = 0; - virtual void ColorPrint(int r, int g, int b, int a, wchar_t* text) = 0; - virtual void ColorPrint(int r, int g, int b, int a, char* text) = 0; - virtual void Print(wchar_t* text) = 0; - virtual void Print(char* text) = 0; - virtual void SetTextColor(int r, int g, int b, int a) = 0; -}; - -enum class SpewType_t -{ - SPEW_MESSAGE = 0, - - SPEW_WARNING, - SPEW_ASSERT, - SPEW_ERROR, - SPEW_LOG, - - SPEW_TYPE_COUNT -}; - -const std::unordered_map 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"}}; - -// these are used to define the base text colour for these things -const std::unordered_map PrintSpewLevels = { - {SpewType_t::SPEW_MESSAGE, spdlog::level::level_enum::info}, - {SpewType_t::SPEW_WARNING, spdlog::level::level_enum::warn}, - {SpewType_t::SPEW_ASSERT, spdlog::level::level_enum::err}, - {SpewType_t::SPEW_ERROR, spdlog::level::level_enum::err}, - {SpewType_t::SPEW_LOG, spdlog::level::level_enum::info}}; - -const std::unordered_map PrintSpewTypes_Short = { - {SpewType_t::SPEW_MESSAGE, 'M'}, - {SpewType_t::SPEW_WARNING, 'W'}, - {SpewType_t::SPEW_ASSERT, 'A'}, - {SpewType_t::SPEW_ERROR, 'E'}, - {SpewType_t::SPEW_LOG, 'L'}}; - -ICenterPrint* pInternalCenterPrint = NULL; - -// 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)); - - if (!Cvar_cl_showtextmsg->GetBool()) - return; - - switch (msg_dest) - { - case TextMsgPrintType_t::HUD_PRINTCENTER: - pInternalCenterPrint->Print(text); - break; - - default: - spdlog::warn("Unimplemented TextMsg type {}! printing to console", msg_dest); - [[fallthrough]]; - - 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; - } -} - -// clang-format off -AUTOHOOK(Hook_fprintf, engine.dll + 0x51B1F0, -int,, (void* const stream, const char* const format, ...)) -// clang-format on -{ - va_list va; - va_start(va, format); - - SQChar buf[1024]; - int charsWritten = vsnprintf_s(buf, _TRUNCATE, format, va); - - if (charsWritten > 0) - { - if (buf[charsWritten - 1] == '\n') - buf[charsWritten - 1] = '\0'; - NS::log::NATIVE_EN->info("{}", buf); - } - - va_end(va); - return 0; -} - -// clang-format off -AUTOHOOK(ConCommand_echo, engine.dll + 0x123680, -void,, (const CCommand& arg)) -// clang-format on -{ - if (arg.ArgC() >= 2) - NS::log::echo->info("{}", arg.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 = PrintSpewTypes.at(type); - char formatted[2048] = {0}; - 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 - // so we use this instead - for (int i = 0; format[i]; i++) - { - if (format[i] == '%') - { - switch (format[i + 1]) - { - // this is fucking awful lol - case 'd': - case 'i': - case 'u': - case 'x': - case 'X': - case 'f': - case 'F': - case 'g': - case 'G': - case 'a': - case 'A': - case 'c': - case 's': - case 'p': - case 'n': - case '%': - case '-': - case '+': - case ' ': - case '#': - case '*': - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - break; - - default: - { - bShouldFormat = false; - break; - } - } - } - } - - 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') - formatted[endpos - 1] = '\0'; // cut off repeated newline - - NS::log::NATIVE_SV->log(PrintSpewLevels.at(type), "{}", formatted); -} - -// 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; - - va_start(list, text); - vsprintf_s(formatted, text, list); - va_end(list); - - auto endpos = strlen(formatted); - if (formatted[endpos - 1] == '\n') - formatted[endpos - 1] = '\0'; // cut off repeated newline - - spdlog::info(formatted); -} - -// clang-format off -AUTOHOOK(CClientState_ProcessPrint, engine.dll + 0x1A1530, -bool,, (void* thisptr, uintptr_t msg)) -// clang-format on -{ - char* text = *(char**)(msg + 0x20); - - auto endpos = strlen(text); - if (text[endpos - 1] == '\n') - text[endpos - 1] = '\0'; // cut off repeated newline - - spdlog::info(text); - return true; -} - -ON_DLL_LOAD_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(); -} diff --git a/NorthstarDLL/loghooks.h b/NorthstarDLL/loghooks.h deleted file mode 100644 index 6f70f09b..00000000 --- a/NorthstarDLL/loghooks.h +++ /dev/null @@ -1 +0,0 @@ -#pragma once diff --git a/NorthstarDLL/main.h b/NorthstarDLL/main.h deleted file mode 100644 index 412f1e25..00000000 --- a/NorthstarDLL/main.h +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once - -extern "C" __declspec(dllexport) bool InitialiseNorthstar(); -extern "C" __declspec(dllexport) bool LoadPlugins(); diff --git a/NorthstarDLL/masterserver.cpp b/NorthstarDLL/masterserver.cpp deleted file mode 100644 index 2e1c5321..00000000 --- a/NorthstarDLL/masterserver.cpp +++ /dev/null @@ -1,1224 +0,0 @@ -#include "pch.h" -#include "masterserver.h" -#include "concommand.h" -#include "playlist.h" -#include "serverauthentication.h" -#include "tier0.h" -#include "r2engine.h" -#include "modmanager.h" -#include "misccommands.h" -#include "version.h" - -#include "rapidjson/document.h" -#include "rapidjson/stringbuffer.h" -#include "rapidjson/writer.h" -#include "rapidjson/error/en.h" - -#include -#include - -using namespace std::chrono_literals; - -MasterServerManager* g_pMasterServerManager; - -ConVar* Cvar_ns_masterserver_hostname; -ConVar* Cvar_ns_curl_log_enable; - -RemoteServerInfo::RemoteServerInfo( - const char* newId, - const char* newName, - const char* newDescription, - const char* newMap, - const char* newPlaylist, - const char* newRegion, - int newPlayerCount, - int newMaxPlayers, - bool newRequiresPassword) -{ - // passworded servers don't have public ips - requiresPassword = newRequiresPassword; - - strncpy_s((char*)id, sizeof(id), newId, sizeof(id) - 1); - strncpy_s((char*)name, sizeof(name), newName, sizeof(name) - 1); - - description = std::string(newDescription); - - strncpy_s((char*)map, sizeof(map), newMap, sizeof(map) - 1); - strncpy_s((char*)playlist, sizeof(playlist), newPlaylist, sizeof(playlist) - 1); - - strncpy((char*)region, newRegion, sizeof(region)); - region[sizeof(region) - 1] = 0; - - playerCount = newPlayerCount; - maxPlayers = newMaxPlayers; -} - -void SetCommonHttpClientOptions(CURL* curl) -{ - curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); - curl_easy_setopt(curl, CURLOPT_VERBOSE, Cvar_ns_curl_log_enable->GetBool()); - curl_easy_setopt(curl, CURLOPT_USERAGENT, &NSUserAgent); - // Timeout since the MS has fucky async functions without await, making curl hang due to a successful connection but no response for ~90 - // seconds. - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); - // curl_easy_setopt(curl, CURLOPT_STDERR, stdout); - if (Tier0::CommandLine()->FindParm("-msinsecure")) // TODO: this check doesn't seem to work - { - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - } -} - -void MasterServerManager::ClearServerList() -{ - // this doesn't really do anything lol, probably isn't threadsafe - m_bRequestingServerList = true; - - m_vRemoteServers.clear(); - - m_bRequestingServerList = false; -} - -size_t CurlWriteToStringBufferCallback(char* contents, size_t size, size_t nmemb, void* userp) -{ - ((std::string*)userp)->append((char*)contents, size * nmemb); - return size * nmemb; -} - -void MasterServerManager::AuthenticateOriginWithMasterServer(const char* uid, const char* originToken) -{ - if (m_bOriginAuthWithMasterServerInProgress) - return; - - // do this here so it's instantly set - m_bOriginAuthWithMasterServerInProgress = true; - std::string uidStr(uid); - std::string tokenStr(originToken); - - std::thread requestThread( - [this, uidStr, tokenStr]() - { - spdlog::info("Trying to authenticate with northstar masterserver for user {}", uidStr); - - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - std::string readBuffer; - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format("{}/client/origin_auth?id={}&token={}", Cvar_ns_masterserver_hostname->GetString(), uidStr, tokenStr).c_str()); - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - { - m_bSuccessfullyConnected = true; - - rapidjson_document originAuthInfo; - originAuthInfo.Parse(readBuffer.c_str()); - - if (originAuthInfo.HasParseError()) - { - spdlog::error( - "Failed reading origin auth info response: encountered parse error \"{}\"", - rapidjson::GetParseError_En(originAuthInfo.GetParseError())); - goto REQUEST_END_CLEANUP; - } - - if (!originAuthInfo.IsObject() || !originAuthInfo.HasMember("success")) - { - spdlog::error("Failed reading origin auth info response: malformed response object {}", readBuffer); - goto REQUEST_END_CLEANUP; - } - - if (originAuthInfo["success"].IsTrue() && originAuthInfo.HasMember("token") && originAuthInfo["token"].IsString()) - { - strncpy_s( - m_sOwnClientAuthToken, - sizeof(m_sOwnClientAuthToken), - originAuthInfo["token"].GetString(), - sizeof(m_sOwnClientAuthToken) - 1); - spdlog::info("Northstar origin authentication completed successfully!"); - } - else - spdlog::error("Northstar origin authentication failed"); - } - else - { - spdlog::error("Failed performing northstar origin auth: error {}", curl_easy_strerror(result)); - m_bSuccessfullyConnected = false; - } - - // we goto this instead of returning so we always hit this - REQUEST_END_CLEANUP: - m_bOriginAuthWithMasterServerInProgress = false; - m_bOriginAuthWithMasterServerDone = true; - curl_easy_cleanup(curl); - }); - - requestThread.detach(); -} - -void MasterServerManager::RequestServerList() -{ - // do this here so it's instantly set on call for scripts - m_bScriptRequestingServerList = true; - - std::thread requestThread( - [this]() - { - // make sure we never have 2 threads writing at once - // i sure do hope this is actually threadsafe - while (m_bRequestingServerList) - Sleep(100); - - m_bRequestingServerList = true; - m_bScriptRequestingServerList = true; - - spdlog::info("Requesting server list from {}", Cvar_ns_masterserver_hostname->GetString()); - - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/client/servers", Cvar_ns_masterserver_hostname->GetString()).c_str()); - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - { - m_bSuccessfullyConnected = true; - - rapidjson_document serverInfoJson; - serverInfoJson.Parse(readBuffer.c_str()); - - if (serverInfoJson.HasParseError()) - { - spdlog::error( - "Failed reading masterserver response: encountered parse error \"{}\"", - rapidjson::GetParseError_En(serverInfoJson.GetParseError())); - goto REQUEST_END_CLEANUP; - } - - if (serverInfoJson.IsObject() && serverInfoJson.HasMember("error")) - { - spdlog::error("Failed reading masterserver response: got fastify error response"); - spdlog::error(readBuffer); - goto REQUEST_END_CLEANUP; - } - - if (!serverInfoJson.IsArray()) - { - spdlog::error("Failed reading masterserver response: root object is not an array"); - goto REQUEST_END_CLEANUP; - } - - rapidjson::GenericArray serverArray = serverInfoJson.GetArray(); - - spdlog::info("Got {} servers", serverArray.Size()); - - for (auto& serverObj : serverArray) - { - if (!serverObj.IsObject()) - { - spdlog::error("Failed reading masterserver response: member of server array is not an object"); - goto REQUEST_END_CLEANUP; - } - - // todo: verify json props are fine before adding to m_remoteServers - if (!serverObj.HasMember("id") || !serverObj["id"].IsString() || !serverObj.HasMember("name") || - !serverObj["name"].IsString() || !serverObj.HasMember("description") || !serverObj["description"].IsString() || - !serverObj.HasMember("map") || !serverObj["map"].IsString() || !serverObj.HasMember("playlist") || - !serverObj["playlist"].IsString() || !serverObj.HasMember("playerCount") || !serverObj["playerCount"].IsNumber() || - !serverObj.HasMember("maxPlayers") || !serverObj["maxPlayers"].IsNumber() || !serverObj.HasMember("hasPassword") || - !serverObj["hasPassword"].IsBool() || !serverObj.HasMember("modInfo") || !serverObj["modInfo"].HasMember("Mods") || - !serverObj["modInfo"]["Mods"].IsArray()) - { - spdlog::error("Failed reading masterserver response: malformed server object"); - continue; - }; - - const char* id = serverObj["id"].GetString(); - - RemoteServerInfo* newServer = nullptr; - - bool createNewServerInfo = true; - for (RemoteServerInfo& server : m_vRemoteServers) - { - // if server already exists, update info rather than adding to it - if (!strncmp((const char*)server.id, id, 32)) - { - server = RemoteServerInfo( - id, - serverObj["name"].GetString(), - serverObj["description"].GetString(), - serverObj["map"].GetString(), - serverObj["playlist"].GetString(), - (serverObj.HasMember("region") && serverObj["region"].IsString()) ? serverObj["region"].GetString() : "", - serverObj["playerCount"].GetInt(), - serverObj["maxPlayers"].GetInt(), - serverObj["hasPassword"].IsTrue()); - newServer = &server; - createNewServerInfo = false; - break; - } - } - - // server didn't exist - if (createNewServerInfo) - newServer = &m_vRemoteServers.emplace_back( - id, - serverObj["name"].GetString(), - serverObj["description"].GetString(), - serverObj["map"].GetString(), - serverObj["playlist"].GetString(), - (serverObj.HasMember("region") && serverObj["region"].IsString()) ? serverObj["region"].GetString() : "", - serverObj["playerCount"].GetInt(), - serverObj["maxPlayers"].GetInt(), - serverObj["hasPassword"].IsTrue()); - - newServer->requiredMods.clear(); - for (auto& requiredMod : serverObj["modInfo"]["Mods"].GetArray()) - { - RemoteModInfo modInfo; - - if (!requiredMod.HasMember("RequiredOnClient") || !requiredMod["RequiredOnClient"].IsTrue()) - continue; - - if (!requiredMod.HasMember("Name") || !requiredMod["Name"].IsString()) - continue; - modInfo.Name = requiredMod["Name"].GetString(); - - if (!requiredMod.HasMember("Version") || !requiredMod["Version"].IsString()) - continue; - modInfo.Version = requiredMod["Version"].GetString(); - - newServer->requiredMods.push_back(modInfo); - } - // Can probably re-enable this later with a -verbose flag, but slows down loading of the server browser quite a bit as - // is - // spdlog::info( - // "Server {} on map {} with playlist {} has {}/{} players", serverObj["name"].GetString(), - // serverObj["map"].GetString(), serverObj["playlist"].GetString(), serverObj["playerCount"].GetInt(), - // serverObj["maxPlayers"].GetInt()); - } - - std::sort( - m_vRemoteServers.begin(), - m_vRemoteServers.end(), - [](RemoteServerInfo& a, RemoteServerInfo& b) { return a.playerCount > b.playerCount; }); - } - else - { - spdlog::error("Failed requesting servers: error {}", curl_easy_strerror(result)); - m_bSuccessfullyConnected = false; - } - - // we goto this instead of returning so we always hit this - REQUEST_END_CLEANUP: - m_bRequestingServerList = false; - m_bScriptRequestingServerList = false; - curl_easy_cleanup(curl); - }); - - requestThread.detach(); -} - -void MasterServerManager::RequestMainMenuPromos() -{ - m_bHasMainMenuPromoData = false; - - std::thread requestThread( - [this]() - { - while (m_bOriginAuthWithMasterServerInProgress || !m_bOriginAuthWithMasterServerDone) - Sleep(500); - - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt( - curl, CURLOPT_URL, fmt::format("{}/client/mainmenupromos", Cvar_ns_masterserver_hostname->GetString()).c_str()); - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - { - m_bSuccessfullyConnected = true; - - rapidjson_document mainMenuPromoJson; - mainMenuPromoJson.Parse(readBuffer.c_str()); - - if (mainMenuPromoJson.HasParseError()) - { - spdlog::error( - "Failed reading masterserver main menu promos response: encountered parse error \"{}\"", - rapidjson::GetParseError_En(mainMenuPromoJson.GetParseError())); - goto REQUEST_END_CLEANUP; - } - - if (!mainMenuPromoJson.IsObject()) - { - spdlog::error("Failed reading masterserver main menu promos response: root object is not an object"); - goto REQUEST_END_CLEANUP; - } - - if (mainMenuPromoJson.HasMember("error")) - { - spdlog::error("Failed reading masterserver response: got fastify error response"); - spdlog::error(readBuffer); - goto REQUEST_END_CLEANUP; - } - - if (!mainMenuPromoJson.HasMember("newInfo") || !mainMenuPromoJson["newInfo"].IsObject() || - !mainMenuPromoJson["newInfo"].HasMember("Title1") || !mainMenuPromoJson["newInfo"]["Title1"].IsString() || - !mainMenuPromoJson["newInfo"].HasMember("Title2") || !mainMenuPromoJson["newInfo"]["Title2"].IsString() || - !mainMenuPromoJson["newInfo"].HasMember("Title3") || !mainMenuPromoJson["newInfo"]["Title3"].IsString() || - - !mainMenuPromoJson.HasMember("largeButton") || !mainMenuPromoJson["largeButton"].IsObject() || - !mainMenuPromoJson["largeButton"].HasMember("Title") || !mainMenuPromoJson["largeButton"]["Title"].IsString() || - !mainMenuPromoJson["largeButton"].HasMember("Text") || !mainMenuPromoJson["largeButton"]["Text"].IsString() || - !mainMenuPromoJson["largeButton"].HasMember("Url") || !mainMenuPromoJson["largeButton"]["Url"].IsString() || - !mainMenuPromoJson["largeButton"].HasMember("ImageIndex") || - !mainMenuPromoJson["largeButton"]["ImageIndex"].IsNumber() || - - !mainMenuPromoJson.HasMember("smallButton1") || !mainMenuPromoJson["smallButton1"].IsObject() || - !mainMenuPromoJson["smallButton1"].HasMember("Title") || !mainMenuPromoJson["smallButton1"]["Title"].IsString() || - !mainMenuPromoJson["smallButton1"].HasMember("Url") || !mainMenuPromoJson["smallButton1"]["Url"].IsString() || - !mainMenuPromoJson["smallButton1"].HasMember("ImageIndex") || - !mainMenuPromoJson["smallButton1"]["ImageIndex"].IsNumber() || - - !mainMenuPromoJson.HasMember("smallButton2") || !mainMenuPromoJson["smallButton2"].IsObject() || - !mainMenuPromoJson["smallButton2"].HasMember("Title") || !mainMenuPromoJson["smallButton2"]["Title"].IsString() || - !mainMenuPromoJson["smallButton2"].HasMember("Url") || !mainMenuPromoJson["smallButton2"]["Url"].IsString() || - !mainMenuPromoJson["smallButton2"].HasMember("ImageIndex") || - !mainMenuPromoJson["smallButton2"]["ImageIndex"].IsNumber()) - { - spdlog::error("Failed reading masterserver main menu promos response: malformed json object"); - goto REQUEST_END_CLEANUP; - } - - m_sMainMenuPromoData.newInfoTitle1 = mainMenuPromoJson["newInfo"]["Title1"].GetString(); - m_sMainMenuPromoData.newInfoTitle2 = mainMenuPromoJson["newInfo"]["Title2"].GetString(); - m_sMainMenuPromoData.newInfoTitle3 = mainMenuPromoJson["newInfo"]["Title3"].GetString(); - - m_sMainMenuPromoData.largeButtonTitle = mainMenuPromoJson["largeButton"]["Title"].GetString(); - m_sMainMenuPromoData.largeButtonText = mainMenuPromoJson["largeButton"]["Text"].GetString(); - m_sMainMenuPromoData.largeButtonUrl = mainMenuPromoJson["largeButton"]["Url"].GetString(); - m_sMainMenuPromoData.largeButtonImageIndex = mainMenuPromoJson["largeButton"]["ImageIndex"].GetInt(); - - m_sMainMenuPromoData.smallButton1Title = mainMenuPromoJson["smallButton1"]["Title"].GetString(); - m_sMainMenuPromoData.smallButton1Url = mainMenuPromoJson["smallButton1"]["Url"].GetString(); - m_sMainMenuPromoData.smallButton1ImageIndex = mainMenuPromoJson["smallButton1"]["ImageIndex"].GetInt(); - - m_sMainMenuPromoData.smallButton2Title = mainMenuPromoJson["smallButton2"]["Title"].GetString(); - m_sMainMenuPromoData.smallButton2Url = mainMenuPromoJson["smallButton2"]["Url"].GetString(); - m_sMainMenuPromoData.smallButton2ImageIndex = mainMenuPromoJson["smallButton2"]["ImageIndex"].GetInt(); - - m_bHasMainMenuPromoData = true; - } - else - { - spdlog::error("Failed requesting main menu promos: error {}", curl_easy_strerror(result)); - m_bSuccessfullyConnected = false; - } - - REQUEST_END_CLEANUP: - // nothing lol - curl_easy_cleanup(curl); - }); - - requestThread.detach(); -} - -void MasterServerManager::AuthenticateWithOwnServer(const char* uid, const char* playerToken) -{ - // dont wait, just stop if we're trying to do 2 auth requests at once - if (m_bAuthenticatingWithGameServer) - return; - - m_bAuthenticatingWithGameServer = true; - m_bScriptAuthenticatingWithGameServer = true; - m_bSuccessfullyAuthenticatedWithGameServer = false; - - std::string uidStr(uid); - std::string tokenStr(playerToken); - - std::thread requestThread( - [this, uidStr, tokenStr]() - { - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format("{}/client/auth_with_self?id={}&playerToken={}", Cvar_ns_masterserver_hostname->GetString(), uidStr, tokenStr) - .c_str()); - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - { - m_bSuccessfullyConnected = true; - - rapidjson_document authInfoJson; - authInfoJson.Parse(readBuffer.c_str()); - - if (authInfoJson.HasParseError()) - { - spdlog::error( - "Failed reading masterserver authentication response: encountered parse error \"{}\"", - rapidjson::GetParseError_En(authInfoJson.GetParseError())); - goto REQUEST_END_CLEANUP; - } - - if (!authInfoJson.IsObject()) - { - spdlog::error("Failed reading masterserver authentication response: root object is not an object"); - goto REQUEST_END_CLEANUP; - } - - if (authInfoJson.HasMember("error")) - { - spdlog::error("Failed reading masterserver response: got fastify error response"); - spdlog::error(readBuffer); - - if (authInfoJson["error"].HasMember("enum")) - m_sAuthFailureReason = authInfoJson["error"]["enum"].GetString(); - else - m_sAuthFailureReason = "No error message provided"; - - goto REQUEST_END_CLEANUP; - } - - if (!authInfoJson["success"].IsTrue()) - { - spdlog::error("Authentication with masterserver failed: \"success\" is not true"); - goto REQUEST_END_CLEANUP; - } - - if (!authInfoJson.HasMember("success") || !authInfoJson.HasMember("id") || !authInfoJson["id"].IsString() || - !authInfoJson.HasMember("authToken") || !authInfoJson["authToken"].IsString() || - !authInfoJson.HasMember("persistentData") || !authInfoJson["persistentData"].IsArray()) - { - spdlog::error("Failed reading masterserver authentication response: malformed json object"); - goto REQUEST_END_CLEANUP; - } - - RemoteAuthData newAuthData {}; - strncpy_s(newAuthData.uid, sizeof(newAuthData.uid), authInfoJson["id"].GetString(), sizeof(newAuthData.uid) - 1); - - newAuthData.pdataSize = authInfoJson["persistentData"].GetArray().Size(); - newAuthData.pdata = new char[newAuthData.pdataSize]; - // memcpy(newAuthData.pdata, authInfoJson["persistentData"].GetString(), newAuthData.pdataSize); - - int i = 0; - // note: persistentData is a uint8array because i had problems getting strings to behave, it sucks but it's just how it be - // unfortunately potentially refactor later - for (auto& byte : authInfoJson["persistentData"].GetArray()) - { - if (!byte.IsUint() || byte.GetUint() > 255) - { - spdlog::error("Failed reading masterserver authentication response: malformed json object"); - goto REQUEST_END_CLEANUP; - } - - newAuthData.pdata[i++] = static_cast(byte.GetUint()); - } - - std::lock_guard guard(g_pServerAuthentication->m_AuthDataMutex); - g_pServerAuthentication->m_RemoteAuthenticationData.clear(); - g_pServerAuthentication->m_RemoteAuthenticationData.insert( - std::make_pair(authInfoJson["authToken"].GetString(), newAuthData)); - - m_bSuccessfullyAuthenticatedWithGameServer = true; - } - else - { - spdlog::error("Failed authenticating with own server: error {}", curl_easy_strerror(result)); - m_bSuccessfullyConnected = false; - m_bSuccessfullyAuthenticatedWithGameServer = false; - m_bScriptAuthenticatingWithGameServer = false; - } - - REQUEST_END_CLEANUP: - m_bAuthenticatingWithGameServer = false; - m_bScriptAuthenticatingWithGameServer = false; - - if (m_bNewgameAfterSelfAuth) - { - // pretty sure this is threadsafe? - R2::Cbuf_AddText(R2::Cbuf_GetCurrentPlayer(), "ns_end_reauth_and_leave_to_lobby", R2::cmd_source_t::kCommandSrcCode); - m_bNewgameAfterSelfAuth = false; - } - - curl_easy_cleanup(curl); - }); - - requestThread.detach(); -} - -void MasterServerManager::AuthenticateWithServer(const char* uid, const char* playerToken, const char* serverId, const char* password) -{ - // dont wait, just stop if we're trying to do 2 auth requests at once - if (m_bAuthenticatingWithGameServer) - return; - - m_bAuthenticatingWithGameServer = true; - m_bScriptAuthenticatingWithGameServer = true; - m_bSuccessfullyAuthenticatedWithGameServer = false; - - std::string uidStr(uid); - std::string tokenStr(playerToken); - std::string serverIdStr(serverId); - std::string passwordStr(password); - - std::thread requestThread( - [this, uidStr, tokenStr, serverIdStr, passwordStr]() - { - // esnure that any persistence saving is done, so we know masterserver has newest - while (m_bSavingPersistentData) - Sleep(100); - - spdlog::info("Attempting authentication with server of id \"{}\"", serverIdStr); - - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - - { - char* escapedPassword = curl_easy_escape(curl, passwordStr.c_str(), passwordStr.length()); - - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format( - "{}/client/auth_with_server?id={}&playerToken={}&server={}&password={}", - Cvar_ns_masterserver_hostname->GetString(), - uidStr, - tokenStr, - serverIdStr, - escapedPassword) - .c_str()); - - curl_free(escapedPassword); - } - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - { - m_bSuccessfullyConnected = true; - - rapidjson_document connectionInfoJson; - connectionInfoJson.Parse(readBuffer.c_str()); - - if (connectionInfoJson.HasParseError()) - { - spdlog::error( - "Failed reading masterserver authentication response: encountered parse error \"{}\"", - rapidjson::GetParseError_En(connectionInfoJson.GetParseError())); - goto REQUEST_END_CLEANUP; - } - - if (!connectionInfoJson.IsObject()) - { - spdlog::error("Failed reading masterserver authentication response: root object is not an object"); - goto REQUEST_END_CLEANUP; - } - - if (connectionInfoJson.HasMember("error")) - { - spdlog::error("Failed reading masterserver response: got fastify error response"); - spdlog::error(readBuffer); - - if (connectionInfoJson["error"].HasMember("enum")) - m_sAuthFailureReason = connectionInfoJson["error"]["enum"].GetString(); - else - m_sAuthFailureReason = "No error message provided"; - - goto REQUEST_END_CLEANUP; - } - - if (!connectionInfoJson["success"].IsTrue()) - { - spdlog::error("Authentication with masterserver failed: \"success\" is not true"); - goto REQUEST_END_CLEANUP; - } - - if (!connectionInfoJson.HasMember("success") || !connectionInfoJson.HasMember("ip") || - !connectionInfoJson["ip"].IsString() || !connectionInfoJson.HasMember("port") || - !connectionInfoJson["port"].IsNumber() || !connectionInfoJson.HasMember("authToken") || - !connectionInfoJson["authToken"].IsString()) - { - spdlog::error("Failed reading masterserver authentication response: malformed json object"); - goto REQUEST_END_CLEANUP; - } - - m_pendingConnectionInfo.ip.S_un.S_addr = inet_addr(connectionInfoJson["ip"].GetString()); - m_pendingConnectionInfo.port = (unsigned short)connectionInfoJson["port"].GetUint(); - - strncpy_s( - m_pendingConnectionInfo.authToken, - sizeof(m_pendingConnectionInfo.authToken), - connectionInfoJson["authToken"].GetString(), - sizeof(m_pendingConnectionInfo.authToken) - 1); - - m_bHasPendingConnectionInfo = true; - m_bSuccessfullyAuthenticatedWithGameServer = true; - } - else - { - spdlog::error("Failed authenticating with server: error {}", curl_easy_strerror(result)); - m_bSuccessfullyConnected = false; - m_bSuccessfullyAuthenticatedWithGameServer = false; - m_bScriptAuthenticatingWithGameServer = false; - } - - REQUEST_END_CLEANUP: - m_bAuthenticatingWithGameServer = false; - m_bScriptAuthenticatingWithGameServer = false; - curl_easy_cleanup(curl); - }); - - requestThread.detach(); -} - -void MasterServerManager::WritePlayerPersistentData(const char* playerId, const char* pdata, size_t pdataSize) -{ - // still call this if we don't have a server id, since lobbies that aren't port forwarded need to be able to call it - m_bSavingPersistentData = true; - if (!pdataSize) - { - spdlog::warn("attempted to write pdata of size 0!"); - return; - } - - std::string strPlayerId(playerId); - std::string strPdata(pdata, pdataSize); - - std::thread requestThread( - [this, strPlayerId, strPdata, pdataSize] - { - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format( - "{}/accounts/write_persistence?id={}&serverId={}", - Cvar_ns_masterserver_hostname->GetString(), - strPlayerId, - m_sOwnServerId) - .c_str()); - curl_easy_setopt(curl, CURLOPT_POST, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - - curl_mime* mime = curl_mime_init(curl); - curl_mimepart* part = curl_mime_addpart(mime); - - curl_mime_data(part, strPdata.c_str(), pdataSize); - curl_mime_name(part, "pdata"); - curl_mime_filename(part, "file.pdata"); - curl_mime_type(part, "application/octet-stream"); - - curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - m_bSuccessfullyConnected = true; - else - m_bSuccessfullyConnected = false; - - curl_easy_cleanup(curl); - - m_bSavingPersistentData = false; - }); - - requestThread.detach(); -} - -void ConCommand_ns_fetchservers(const CCommand& args) -{ - g_pMasterServerManager->RequestServerList(); -} - -MasterServerManager::MasterServerManager() : m_pendingConnectionInfo {}, m_sOwnServerId {""}, m_sOwnClientAuthToken {""} {} - -ON_DLL_LOAD_RELIESON("engine.dll", MasterServer, (ConCommand, ServerPresence), (CModule module)) -{ - g_pMasterServerManager = new MasterServerManager; - - Cvar_ns_masterserver_hostname = new ConVar("ns_masterserver_hostname", "127.0.0.1", FCVAR_NONE, ""); - Cvar_ns_curl_log_enable = new ConVar("ns_curl_log_enable", "0", FCVAR_NONE, "Whether curl should log to the console"); - - RegisterConCommand("ns_fetchservers", ConCommand_ns_fetchservers, "Fetch all servers from the masterserver", FCVAR_CLIENTDLL); - - MasterServerPresenceReporter* presenceReporter = new MasterServerPresenceReporter; - g_pServerPresence->AddPresenceReporter(presenceReporter); -} - -void MasterServerPresenceReporter::CreatePresence(const ServerPresence* pServerPresence) -{ - m_nNumRegistrationAttempts = 0; -} - -void MasterServerPresenceReporter::ReportPresence(const ServerPresence* pServerPresence) -{ - // make a copy of presence for multithreading purposes - ServerPresence threadedPresence(pServerPresence); - - if (!*g_pMasterServerManager->m_sOwnServerId) - { - // Don't try if we've reached the max registration attempts. - // In the future, we should probably allow servers to re-authenticate after a while if the MS was down. - if (m_nNumRegistrationAttempts >= MAX_REGISTRATION_ATTEMPTS) - { - return; - } - - // Make sure to wait til the cooldown is over for DUPLICATE_SERVER failures. - if (Tier0::Plat_FloatTime() < m_fNextAddServerAttemptTime) - { - return; - } - - // If we're not running any InternalAddServer() attempt in the background. - if (!addServerFuture.valid()) - { - // Launch an attempt to add the local server to the master server. - InternalAddServer(pServerPresence); - } - } - else - { - // If we're not running any InternalUpdateServer() attempt in the background. - if (!updateServerFuture.valid()) - { - // Launch an attempt to update the local server on the master server. - InternalUpdateServer(pServerPresence); - } - } -} - -void MasterServerPresenceReporter::DestroyPresence(const ServerPresence* pServerPresence) -{ - // Don't call this if we don't have a server id. - if (!*g_pMasterServerManager->m_sOwnServerId) - { - return; - } - - // Not bothering with better thread safety in this case since DestroyPresence() is called when the game is shutting down. - *g_pMasterServerManager->m_sOwnServerId = 0; - - std::thread requestThread( - [this] - { - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format( - "{}/server/remove_server?id={}", Cvar_ns_masterserver_hostname->GetString(), g_pMasterServerManager->m_sOwnServerId) - .c_str()); - - CURLcode result = curl_easy_perform(curl); - curl_easy_cleanup(curl); - }); - - requestThread.detach(); -} - -void MasterServerPresenceReporter::RunFrame(double flCurrentTime, const ServerPresence* pServerPresence) -{ - // Check if we're already running an InternalAddServer() call in the background. - // If so, grab the result if it's ready. - if (addServerFuture.valid()) - { - std::future_status status = addServerFuture.wait_for(0ms); - if (status != std::future_status::ready) - { - // Still running, no need to do anything. - return; - } - - // Check the result. - auto resultData = addServerFuture.get(); - - g_pMasterServerManager->m_bSuccessfullyConnected = resultData.result != MasterServerReportPresenceResult::FailedNoConnect; - - switch (resultData.result) - { - case MasterServerReportPresenceResult::Success: - // Copy over the server id and auth token granted by the MS. - strncpy_s( - g_pMasterServerManager->m_sOwnServerId, - sizeof(g_pMasterServerManager->m_sOwnServerId), - resultData.id.value().c_str(), - sizeof(g_pMasterServerManager->m_sOwnServerId) - 1); - strncpy_s( - g_pMasterServerManager->m_sOwnServerAuthToken, - sizeof(g_pMasterServerManager->m_sOwnServerAuthToken), - resultData.serverAuthToken.value().c_str(), - sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1); - break; - case MasterServerReportPresenceResult::FailedNoRetry: - case MasterServerReportPresenceResult::FailedNoConnect: - // If we failed to connect to the master server, or failed with no retry, stop trying. - m_nNumRegistrationAttempts = MAX_REGISTRATION_ATTEMPTS; - break; - case MasterServerReportPresenceResult::Failed: - ++m_nNumRegistrationAttempts; - break; - case MasterServerReportPresenceResult::FailedDuplicateServer: - ++m_nNumRegistrationAttempts; - // Wait at least twenty seconds until we re-attempt to add the server. - m_fNextAddServerAttemptTime = Tier0::Plat_FloatTime() + 20.0f; - break; - } - - if (m_nNumRegistrationAttempts >= MAX_REGISTRATION_ATTEMPTS) - { - spdlog::error("Reached max ms server registration attempts."); - } - } - else if (updateServerFuture.valid()) - { - // Check if the InternalUpdateServer() call completed. - std::future_status status = updateServerFuture.wait_for(0ms); - if (status != std::future_status::ready) - { - // Still running, no need to do anything. - return; - } - - auto resultData = updateServerFuture.get(); - if (resultData.result == MasterServerReportPresenceResult::Success) - { - if (resultData.id) - { - strncpy_s( - g_pMasterServerManager->m_sOwnServerId, - sizeof(g_pMasterServerManager->m_sOwnServerId), - resultData.id.value().c_str(), - sizeof(g_pMasterServerManager->m_sOwnServerId) - 1); - } - - if (resultData.serverAuthToken) - { - strncpy_s( - g_pMasterServerManager->m_sOwnServerAuthToken, - sizeof(g_pMasterServerManager->m_sOwnServerAuthToken), - resultData.serverAuthToken.value().c_str(), - sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1); - } - } - } -} - -void MasterServerPresenceReporter::InternalAddServer(const ServerPresence* pServerPresence) -{ - const ServerPresence threadedPresence(pServerPresence); - // Never call this with an ongoing InternalAddServer() call. - assert(!addServerFuture.valid()); - - g_pMasterServerManager->m_sOwnServerId[0] = 0; - g_pMasterServerManager->m_sOwnServerAuthToken[0] = 0; - - std::string modInfo = g_pMasterServerManager->m_sOwnModInfoJson; - std::string hostname = Cvar_ns_masterserver_hostname->GetString(); - - spdlog::info("Attempting to register the local server to the master server."); - - addServerFuture = std::async( - std::launch::async, - [threadedPresence, modInfo, hostname] - { - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - std::string readBuffer; - curl_easy_setopt(curl, CURLOPT_POST, 1L); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - - curl_mime* mime = curl_mime_init(curl); - curl_mimepart* part = curl_mime_addpart(mime); - - // Lambda to quickly cleanup resources and return a value. - auto ReturnCleanup = - [curl, mime](MasterServerReportPresenceResult result, const char* id = "", const char* serverAuthToken = "") - { - curl_easy_cleanup(curl); - curl_mime_free(mime); - - MasterServerPresenceReporter::ReportPresenceResultData data; - data.result = result; - data.id = id; - data.serverAuthToken = serverAuthToken; - - return data; - }; - - curl_mime_data(part, modInfo.c_str(), modInfo.size()); - curl_mime_name(part, "modinfo"); - curl_mime_filename(part, "modinfo.json"); - curl_mime_type(part, "application/json"); - - curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); - - // format every paramter because computers hate me - { - char* nameEscaped = curl_easy_escape(curl, threadedPresence.m_sServerName.c_str(), NULL); - char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL); - char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL); - char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL); - char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL); - - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format( - "{}/server/" - "add_server?port={}&authPort={}&name={}&description={}&map={}&playlist={}&maxPlayers={}&password={}", - hostname.c_str(), - threadedPresence.m_iPort, - threadedPresence.m_iAuthPort, - nameEscaped, - descEscaped, - mapEscaped, - playlistEscaped, - threadedPresence.m_iMaxPlayers, - passwordEscaped) - .c_str()); - - curl_free(nameEscaped); - curl_free(descEscaped); - curl_free(mapEscaped); - curl_free(playlistEscaped); - curl_free(passwordEscaped); - } - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - { - rapidjson_document serverAddedJson; - serverAddedJson.Parse(readBuffer.c_str()); - - // If we could not parse the JSON or it isn't an object, assume the MS is either wrong or we're completely out of date. - // No retry. - if (serverAddedJson.HasParseError()) - { - spdlog::error( - "Failed reading masterserver authentication response: encountered parse error \"{}\"", - rapidjson::GetParseError_En(serverAddedJson.GetParseError())); - return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry); - } - - if (!serverAddedJson.IsObject()) - { - spdlog::error("Failed reading masterserver authentication response: root object is not an object"); - return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry); - } - - if (serverAddedJson.HasMember("error")) - { - spdlog::error("Failed reading masterserver response: got fastify error response"); - spdlog::error(readBuffer); - - // If this is DUPLICATE_SERVER, we'll retry adding the server every 20 seconds. - // The master server will only update its internal server list and clean up dead servers on certain events. - // And then again, only if a player requests the server list after the cooldown (1 second by default), or a server is - // added/updated/removed. In any case this needs to be fixed in the master server rewrite. - if (serverAddedJson["error"].HasMember("enum") && - strcmp(serverAddedJson["error"]["enum"].GetString(), "DUPLICATE_SERVER") == 0) - { - spdlog::error("Cooling down while the master server cleans the dead server entry, if any."); - return ReturnCleanup(MasterServerReportPresenceResult::FailedDuplicateServer); - } - - // Retry until we reach max retries. - return ReturnCleanup(MasterServerReportPresenceResult::Failed); - } - - if (!serverAddedJson["success"].IsTrue()) - { - spdlog::error("Adding server to masterserver failed: \"success\" is not true"); - return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry); - } - - if (!serverAddedJson.HasMember("id") || !serverAddedJson["id"].IsString() || - !serverAddedJson.HasMember("serverAuthToken") || !serverAddedJson["serverAuthToken"].IsString()) - { - spdlog::error("Failed reading masterserver response: malformed json object"); - return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry); - } - - spdlog::info("Successfully registered the local server to the master server."); - return ReturnCleanup( - MasterServerReportPresenceResult::Success, - serverAddedJson["id"].GetString(), - serverAddedJson["serverAuthToken"].GetString()); - } - else - { - spdlog::error("Failed adding self to server list: error {}", curl_easy_strerror(result)); - return ReturnCleanup(MasterServerReportPresenceResult::FailedNoConnect); - } - }); -} - -void MasterServerPresenceReporter::InternalUpdateServer(const ServerPresence* pServerPresence) -{ - const ServerPresence threadedPresence(pServerPresence); - - // Never call this with an ongoing InternalUpdateServer() call. - assert(!updateServerFuture.valid()); - - const std::string serverId = g_pMasterServerManager->m_sOwnServerId; - const std::string hostname = Cvar_ns_masterserver_hostname->GetString(); - const std::string modinfo = g_pMasterServerManager->m_sOwnModInfoJson; - - updateServerFuture = std::async( - std::launch::async, - [threadedPresence, serverId, hostname, modinfo] - { - CURL* curl = curl_easy_init(); - SetCommonHttpClientOptions(curl); - - // Lambda to quickly cleanup resources and return a value. - auto ReturnCleanup = [curl](MasterServerReportPresenceResult result, const char* id = "", const char* serverAuthToken = "") - { - curl_easy_cleanup(curl); - - MasterServerPresenceReporter::ReportPresenceResultData data; - data.result = result; - - if (id != nullptr) - { - data.id = id; - } - - if (serverAuthToken != nullptr) - { - data.serverAuthToken = serverAuthToken; - } - - return data; - }; - - std::string readBuffer; - curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); - curl_easy_setopt(curl, CURLOPT_VERBOSE, 0L); - - // send all registration info so we have all necessary info to reregister our server if masterserver goes down, - // without a restart this isn't threadsafe :terror: - { - char* nameEscaped = curl_easy_escape(curl, threadedPresence.m_sServerName.c_str(), NULL); - char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL); - char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL); - char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL); - char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL); - - curl_easy_setopt( - curl, - CURLOPT_URL, - fmt::format( - "{}/server/" - "update_values?id={}&port={}&authPort={}&name={}&description={}&map={}&playlist={}&playerCount={}&" - "maxPlayers={}&password={}", - hostname.c_str(), - serverId.c_str(), - threadedPresence.m_iPort, - threadedPresence.m_iAuthPort, - nameEscaped, - descEscaped, - mapEscaped, - playlistEscaped, - threadedPresence.m_iPlayerCount, - threadedPresence.m_iMaxPlayers, - passwordEscaped) - .c_str()); - - curl_free(nameEscaped); - curl_free(descEscaped); - curl_free(mapEscaped); - curl_free(playlistEscaped); - curl_free(passwordEscaped); - } - - curl_mime* mime = curl_mime_init(curl); - curl_mimepart* part = curl_mime_addpart(mime); - - curl_mime_data(part, modinfo.c_str(), modinfo.size()); - curl_mime_name(part, "modinfo"); - curl_mime_filename(part, "modinfo.json"); - curl_mime_type(part, "application/json"); - - curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); - - CURLcode result = curl_easy_perform(curl); - - if (result == CURLcode::CURLE_OK) - { - rapidjson_document serverAddedJson; - serverAddedJson.Parse(readBuffer.c_str()); - - const char* updatedId = nullptr; - const char* updatedAuthToken = nullptr; - - if (!serverAddedJson.HasParseError() && serverAddedJson.IsObject()) - { - if (serverAddedJson.HasMember("id") && serverAddedJson["id"].IsString()) - { - updatedId = serverAddedJson["id"].GetString(); - } - - if (serverAddedJson.HasMember("serverAuthToken") && serverAddedJson["serverAuthToken"].IsString()) - { - updatedAuthToken = serverAddedJson["serverAuthToken"].GetString(); - } - } - - return ReturnCleanup(MasterServerReportPresenceResult::Success, updatedId, updatedAuthToken); - } - else - { - spdlog::warn("Heartbeat failed with error {}", curl_easy_strerror(result)); - return ReturnCleanup(MasterServerReportPresenceResult::Failed); - } - }); -} diff --git a/NorthstarDLL/masterserver.h b/NorthstarDLL/masterserver.h deleted file mode 100644 index 4e649fae..00000000 --- a/NorthstarDLL/masterserver.h +++ /dev/null @@ -1,188 +0,0 @@ -#pragma once - -#include "convar.h" -#include "serverpresence.h" -#include -#include -#include -#include - -extern ConVar* Cvar_ns_masterserver_hostname; -extern ConVar* Cvar_ns_curl_log_enable; - -struct RemoteModInfo -{ - public: - std::string Name; - std::string Version; -}; - -class RemoteServerInfo -{ - public: - char id[33]; // 32 bytes + nullterminator - - // server info - char name[64]; - std::string description; - char map[32]; - char playlist[16]; - char region[32]; - std::vector requiredMods; - - int playerCount; - int maxPlayers; - - // connection stuff - bool requiresPassword; - - public: - RemoteServerInfo( - const char* newId, - const char* newName, - const char* newDescription, - const char* newMap, - const char* newPlaylist, - const char* newRegion, - int newPlayerCount, - int newMaxPlayers, - bool newRequiresPassword); -}; - -struct RemoteServerConnectionInfo -{ - public: - char authToken[32]; - - in_addr ip; - unsigned short port; -}; - -struct MainMenuPromoData -{ - public: - std::string newInfoTitle1; - std::string newInfoTitle2; - std::string newInfoTitle3; - - std::string largeButtonTitle; - std::string largeButtonText; - std::string largeButtonUrl; - int largeButtonImageIndex; - - std::string smallButton1Title; - std::string smallButton1Url; - int smallButton1ImageIndex; - - std::string smallButton2Title; - std::string smallButton2Url; - int smallButton2ImageIndex; -}; - -class MasterServerManager -{ - private: - bool m_bRequestingServerList = false; - bool m_bAuthenticatingWithGameServer = false; - - public: - char m_sOwnServerId[33]; - char m_sOwnServerAuthToken[33]; - char m_sOwnClientAuthToken[33]; - - std::string m_sOwnModInfoJson; - - bool m_bOriginAuthWithMasterServerDone = false; - bool m_bOriginAuthWithMasterServerInProgress = false; - - bool m_bSavingPersistentData = false; - - bool m_bScriptRequestingServerList = false; - bool m_bSuccessfullyConnected = true; - - bool m_bNewgameAfterSelfAuth = false; - bool m_bScriptAuthenticatingWithGameServer = false; - bool m_bSuccessfullyAuthenticatedWithGameServer = false; - std::string m_sAuthFailureReason {}; - - bool m_bHasPendingConnectionInfo = false; - RemoteServerConnectionInfo m_pendingConnectionInfo; - - std::vector m_vRemoteServers; - - bool m_bHasMainMenuPromoData = false; - MainMenuPromoData m_sMainMenuPromoData; - - public: - MasterServerManager(); - - void ClearServerList(); - void RequestServerList(); - void RequestMainMenuPromos(); - 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); -}; - -extern MasterServerManager* g_pMasterServerManager; -extern ConVar* Cvar_ns_masterserver_hostname; - -/** 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 id; - std::optional 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 addServerFuture; - - // The future used for InternalAddServer() calls. - std::future updateServerFuture; - - int m_nNumRegistrationAttempts; - - double m_fNextAddServerAttemptTime; -}; diff --git a/NorthstarDLL/masterserver/masterserver.cpp b/NorthstarDLL/masterserver/masterserver.cpp new file mode 100644 index 00000000..5679939b --- /dev/null +++ b/NorthstarDLL/masterserver/masterserver.cpp @@ -0,0 +1,1224 @@ +#include "pch.h" +#include "masterserver/masterserver.h" +#include "core/convar/concommand.h" +#include "playlist.h" +#include "server/auth/serverauthentication.h" +#include "core/tier0.h" +#include "engine/r2engine.h" +#include "mods/modmanager.h" +#include "shared/misccommands.h" +#include "util/version.h" + +#include "rapidjson/document.h" +#include "rapidjson/stringbuffer.h" +#include "rapidjson/writer.h" +#include "rapidjson/error/en.h" + +#include +#include + +using namespace std::chrono_literals; + +MasterServerManager* g_pMasterServerManager; + +ConVar* Cvar_ns_masterserver_hostname; +ConVar* Cvar_ns_curl_log_enable; + +RemoteServerInfo::RemoteServerInfo( + const char* newId, + const char* newName, + const char* newDescription, + const char* newMap, + const char* newPlaylist, + const char* newRegion, + int newPlayerCount, + int newMaxPlayers, + bool newRequiresPassword) +{ + // passworded servers don't have public ips + requiresPassword = newRequiresPassword; + + strncpy_s((char*)id, sizeof(id), newId, sizeof(id) - 1); + strncpy_s((char*)name, sizeof(name), newName, sizeof(name) - 1); + + description = std::string(newDescription); + + strncpy_s((char*)map, sizeof(map), newMap, sizeof(map) - 1); + strncpy_s((char*)playlist, sizeof(playlist), newPlaylist, sizeof(playlist) - 1); + + strncpy((char*)region, newRegion, sizeof(region)); + region[sizeof(region) - 1] = 0; + + playerCount = newPlayerCount; + maxPlayers = newMaxPlayers; +} + +void SetCommonHttpClientOptions(CURL* curl) +{ + curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + curl_easy_setopt(curl, CURLOPT_VERBOSE, Cvar_ns_curl_log_enable->GetBool()); + curl_easy_setopt(curl, CURLOPT_USERAGENT, &NSUserAgent); + // Timeout since the MS has fucky async functions without await, making curl hang due to a successful connection but no response for ~90 + // seconds. + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); + // curl_easy_setopt(curl, CURLOPT_STDERR, stdout); + if (Tier0::CommandLine()->FindParm("-msinsecure")) // TODO: this check doesn't seem to work + { + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + } +} + +void MasterServerManager::ClearServerList() +{ + // this doesn't really do anything lol, probably isn't threadsafe + m_bRequestingServerList = true; + + m_vRemoteServers.clear(); + + m_bRequestingServerList = false; +} + +size_t CurlWriteToStringBufferCallback(char* contents, size_t size, size_t nmemb, void* userp) +{ + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; +} + +void MasterServerManager::AuthenticateOriginWithMasterServer(const char* uid, const char* originToken) +{ + if (m_bOriginAuthWithMasterServerInProgress) + return; + + // do this here so it's instantly set + m_bOriginAuthWithMasterServerInProgress = true; + std::string uidStr(uid); + std::string tokenStr(originToken); + + std::thread requestThread( + [this, uidStr, tokenStr]() + { + spdlog::info("Trying to authenticate with northstar masterserver for user {}", uidStr); + + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); + std::string readBuffer; + curl_easy_setopt( + curl, + CURLOPT_URL, + fmt::format("{}/client/origin_auth?id={}&token={}", Cvar_ns_masterserver_hostname->GetString(), uidStr, tokenStr).c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); + + CURLcode result = curl_easy_perform(curl); + + if (result == CURLcode::CURLE_OK) + { + m_bSuccessfullyConnected = true; + + rapidjson_document originAuthInfo; + originAuthInfo.Parse(readBuffer.c_str()); + + if (originAuthInfo.HasParseError()) + { + spdlog::error( + "Failed reading origin auth info response: encountered parse error \"{}\"", + rapidjson::GetParseError_En(originAuthInfo.GetParseError())); + goto REQUEST_END_CLEANUP; + } + + if (!originAuthInfo.IsObject() || !originAuthInfo.HasMember("success")) + { + spdlog::error("Failed reading origin auth info response: malformed response object {}", readBuffer); + goto REQUEST_END_CLEANUP; + } + + if (originAuthInfo["success"].IsTrue() && originAuthInfo.HasMember("token") && originAuthInfo["token"].IsString()) + { + strncpy_s( + m_sOwnClientAuthToken, + sizeof(m_sOwnClientAuthToken), + originAuthInfo["token"].GetString(), + sizeof(m_sOwnClientAuthToken) - 1); + spdlog::info("Northstar origin authentication completed successfully!"); + } + else + spdlog::error("Northstar origin authentication failed"); + } + else + { + spdlog::error("Failed performing northstar origin auth: error {}", curl_easy_strerror(result)); + m_bSuccessfullyConnected = false; + } + + // we goto this instead of returning so we always hit this + REQUEST_END_CLEANUP: + m_bOriginAuthWithMasterServerInProgress = false; + m_bOriginAuthWithMasterServerDone = true; + curl_easy_cleanup(curl); + }); + + requestThread.detach(); +} + +void MasterServerManager::RequestServerList() +{ + // do this here so it's instantly set on call for scripts + m_bScriptRequestingServerList = true; + + std::thread requestThread( + [this]() + { + // make sure we never have 2 threads writing at once + // i sure do hope this is actually threadsafe + while (m_bRequestingServerList) + Sleep(100); + + m_bRequestingServerList = true; + m_bScriptRequestingServerList = true; + + spdlog::info("Requesting server list from {}", Cvar_ns_masterserver_hostname->GetString()); + + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); + + std::string readBuffer; + curl_easy_setopt(curl, CURLOPT_URL, fmt::format("{}/client/servers", Cvar_ns_masterserver_hostname->GetString()).c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); + + CURLcode result = curl_easy_perform(curl); + + if (result == CURLcode::CURLE_OK) + { + m_bSuccessfullyConnected = true; + + rapidjson_document serverInfoJson; + serverInfoJson.Parse(readBuffer.c_str()); + + if (serverInfoJson.HasParseError()) + { + spdlog::error( + "Failed reading masterserver response: encountered parse error \"{}\"", + rapidjson::GetParseError_En(serverInfoJson.GetParseError())); + goto REQUEST_END_CLEANUP; + } + + if (serverInfoJson.IsObject() && serverInfoJson.HasMember("error")) + { + spdlog::error("Failed reading masterserver response: got fastify error response"); + spdlog::error(readBuffer); + goto REQUEST_END_CLEANUP; + } + + if (!serverInfoJson.IsArray()) + { + spdlog::error("Failed reading masterserver response: root object is not an array"); + goto REQUEST_END_CLEANUP; + } + + rapidjson::GenericArray serverArray = serverInfoJson.GetArray(); + + spdlog::info("Got {} servers", serverArray.Size()); + + for (auto& serverObj : serverArray) + { + if (!serverObj.IsObject()) + { + spdlog::error("Failed reading masterserver response: member of server array is not an object"); + goto REQUEST_END_CLEANUP; + } + + // todo: verify json props are fine before adding to m_remoteServers + if (!serverObj.HasMember("id") || !serverObj["id"].IsString() || !serverObj.HasMember("name") || + !serverObj["name"].IsString() || !serverObj.HasMember("description") || !serverObj["description"].IsString() || + !serverObj.HasMember("map") || !serverObj["map"].IsString() || !serverObj.HasMember("playlist") || + !serverObj["playlist"].IsString() || !serverObj.HasMember("playerCount") || !serverObj["playerCount"].IsNumber() || + !serverObj.HasMember("maxPlayers") || !serverObj["maxPlayers"].IsNumber() || !serverObj.HasMember("hasPassword") || + !serverObj["hasPassword"].IsBool() || !serverObj.HasMember("modInfo") || !serverObj["modInfo"].HasMember("Mods") || + !serverObj["modInfo"]["Mods"].IsArray()) + { + spdlog::error("Failed reading masterserver response: malformed server object"); + continue; + }; + + const char* id = serverObj["id"].GetString(); + + RemoteServerInfo* newServer = nullptr; + + bool createNewServerInfo = true; + for (RemoteServerInfo& server : m_vRemoteServers) + { + // if server already exists, update info rather than adding to it + if (!strncmp((const char*)server.id, id, 32)) + { + server = RemoteServerInfo( + id, + serverObj["name"].GetString(), + serverObj["description"].GetString(), + serverObj["map"].GetString(), + serverObj["playlist"].GetString(), + (serverObj.HasMember("region") && serverObj["region"].IsString()) ? serverObj["region"].GetString() : "", + serverObj["playerCount"].GetInt(), + serverObj["maxPlayers"].GetInt(), + serverObj["hasPassword"].IsTrue()); + newServer = &server; + createNewServerInfo = false; + break; + } + } + + // server didn't exist + if (createNewServerInfo) + newServer = &m_vRemoteServers.emplace_back( + id, + serverObj["name"].GetString(), + serverObj["description"].GetString(), + serverObj["map"].GetString(), + serverObj["playlist"].GetString(), + (serverObj.HasMember("region") && serverObj["region"].IsString()) ? serverObj["region"].GetString() : "", + serverObj["playerCount"].GetInt(), + serverObj["maxPlayers"].GetInt(), + serverObj["hasPassword"].IsTrue()); + + newServer->requiredMods.clear(); + for (auto& requiredMod : serverObj["modInfo"]["Mods"].GetArray()) + { + RemoteModInfo modInfo; + + if (!requiredMod.HasMember("RequiredOnClient") || !requiredMod["RequiredOnClient"].IsTrue()) + continue; + + if (!requiredMod.HasMember("Name") || !requiredMod["Name"].IsString()) + continue; + modInfo.Name = requiredMod["Name"].GetString(); + + if (!requiredMod.HasMember("Version") || !requiredMod["Version"].IsString()) + continue; + modInfo.Version = requiredMod["Version"].GetString(); + + newServer->requiredMods.push_back(modInfo); + } + // Can probably re-enable this later with a -verbose flag, but slows down loading of the server browser quite a bit as + // is + // spdlog::info( + // "Server {} on map {} with playlist {} has {}/{} players", serverObj["name"].GetString(), + // serverObj["map"].GetString(), serverObj["playlist"].GetString(), serverObj["playerCount"].GetInt(), + // serverObj["maxPlayers"].GetInt()); + } + + std::sort( + m_vRemoteServers.begin(), + m_vRemoteServers.end(), + [](RemoteServerInfo& a, RemoteServerInfo& b) { return a.playerCount > b.playerCount; }); + } + else + { + spdlog::error("Failed requesting servers: error {}", curl_easy_strerror(result)); + m_bSuccessfullyConnected = false; + } + + // we goto this instead of returning so we always hit this + REQUEST_END_CLEANUP: + m_bRequestingServerList = false; + m_bScriptRequestingServerList = false; + curl_easy_cleanup(curl); + }); + + requestThread.detach(); +} + +void MasterServerManager::RequestMainMenuPromos() +{ + m_bHasMainMenuPromoData = false; + + std::thread requestThread( + [this]() + { + while (m_bOriginAuthWithMasterServerInProgress || !m_bOriginAuthWithMasterServerDone) + Sleep(500); + + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); + + std::string readBuffer; + curl_easy_setopt( + curl, CURLOPT_URL, fmt::format("{}/client/mainmenupromos", Cvar_ns_masterserver_hostname->GetString()).c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "GET"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); + + CURLcode result = curl_easy_perform(curl); + + if (result == CURLcode::CURLE_OK) + { + m_bSuccessfullyConnected = true; + + rapidjson_document mainMenuPromoJson; + mainMenuPromoJson.Parse(readBuffer.c_str()); + + if (mainMenuPromoJson.HasParseError()) + { + spdlog::error( + "Failed reading masterserver main menu promos response: encountered parse error \"{}\"", + rapidjson::GetParseError_En(mainMenuPromoJson.GetParseError())); + goto REQUEST_END_CLEANUP; + } + + if (!mainMenuPromoJson.IsObject()) + { + spdlog::error("Failed reading masterserver main menu promos response: root object is not an object"); + goto REQUEST_END_CLEANUP; + } + + if (mainMenuPromoJson.HasMember("error")) + { + spdlog::error("Failed reading masterserver response: got fastify error response"); + spdlog::error(readBuffer); + goto REQUEST_END_CLEANUP; + } + + if (!mainMenuPromoJson.HasMember("newInfo") || !mainMenuPromoJson["newInfo"].IsObject() || + !mainMenuPromoJson["newInfo"].HasMember("Title1") || !mainMenuPromoJson["newInfo"]["Title1"].IsString() || + !mainMenuPromoJson["newInfo"].HasMember("Title2") || !mainMenuPromoJson["newInfo"]["Title2"].IsString() || + !mainMenuPromoJson["newInfo"].HasMember("Title3") || !mainMenuPromoJson["newInfo"]["Title3"].IsString() || + + !mainMenuPromoJson.HasMember("largeButton") || !mainMenuPromoJson["largeButton"].IsObject() || + !mainMenuPromoJson["largeButton"].HasMember("Title") || !mainMenuPromoJson["largeButton"]["Title"].IsString() || + !mainMenuPromoJson["largeButton"].HasMember("Text") || !mainMenuPromoJson["largeButton"]["Text"].IsString() || + !mainMenuPromoJson["largeButton"].HasMember("Url") || !mainMenuPromoJson["largeButton"]["Url"].IsString() || + !mainMenuPromoJson["largeButton"].HasMember("ImageIndex") || + !mainMenuPromoJson["largeButton"]["ImageIndex"].IsNumber() || + + !mainMenuPromoJson.HasMember("smallButton1") || !mainMenuPromoJson["smallButton1"].IsObject() || + !mainMenuPromoJson["smallButton1"].HasMember("Title") || !mainMenuPromoJson["smallButton1"]["Title"].IsString() || + !mainMenuPromoJson["smallButton1"].HasMember("Url") || !mainMenuPromoJson["smallButton1"]["Url"].IsString() || + !mainMenuPromoJson["smallButton1"].HasMember("ImageIndex") || + !mainMenuPromoJson["smallButton1"]["ImageIndex"].IsNumber() || + + !mainMenuPromoJson.HasMember("smallButton2") || !mainMenuPromoJson["smallButton2"].IsObject() || + !mainMenuPromoJson["smallButton2"].HasMember("Title") || !mainMenuPromoJson["smallButton2"]["Title"].IsString() || + !mainMenuPromoJson["smallButton2"].HasMember("Url") || !mainMenuPromoJson["smallButton2"]["Url"].IsString() || + !mainMenuPromoJson["smallButton2"].HasMember("ImageIndex") || + !mainMenuPromoJson["smallButton2"]["ImageIndex"].IsNumber()) + { + spdlog::error("Failed reading masterserver main menu promos response: malformed json object"); + goto REQUEST_END_CLEANUP; + } + + m_sMainMenuPromoData.newInfoTitle1 = mainMenuPromoJson["newInfo"]["Title1"].GetString(); + m_sMainMenuPromoData.newInfoTitle2 = mainMenuPromoJson["newInfo"]["Title2"].GetString(); + m_sMainMenuPromoData.newInfoTitle3 = mainMenuPromoJson["newInfo"]["Title3"].GetString(); + + m_sMainMenuPromoData.largeButtonTitle = mainMenuPromoJson["largeButton"]["Title"].GetString(); + m_sMainMenuPromoData.largeButtonText = mainMenuPromoJson["largeButton"]["Text"].GetString(); + m_sMainMenuPromoData.largeButtonUrl = mainMenuPromoJson["largeButton"]["Url"].GetString(); + m_sMainMenuPromoData.largeButtonImageIndex = mainMenuPromoJson["largeButton"]["ImageIndex"].GetInt(); + + m_sMainMenuPromoData.smallButton1Title = mainMenuPromoJson["smallButton1"]["Title"].GetString(); + m_sMainMenuPromoData.smallButton1Url = mainMenuPromoJson["smallButton1"]["Url"].GetString(); + m_sMainMenuPromoData.smallButton1ImageIndex = mainMenuPromoJson["smallButton1"]["ImageIndex"].GetInt(); + + m_sMainMenuPromoData.smallButton2Title = mainMenuPromoJson["smallButton2"]["Title"].GetString(); + m_sMainMenuPromoData.smallButton2Url = mainMenuPromoJson["smallButton2"]["Url"].GetString(); + m_sMainMenuPromoData.smallButton2ImageIndex = mainMenuPromoJson["smallButton2"]["ImageIndex"].GetInt(); + + m_bHasMainMenuPromoData = true; + } + else + { + spdlog::error("Failed requesting main menu promos: error {}", curl_easy_strerror(result)); + m_bSuccessfullyConnected = false; + } + + REQUEST_END_CLEANUP: + // nothing lol + curl_easy_cleanup(curl); + }); + + requestThread.detach(); +} + +void MasterServerManager::AuthenticateWithOwnServer(const char* uid, const char* playerToken) +{ + // dont wait, just stop if we're trying to do 2 auth requests at once + if (m_bAuthenticatingWithGameServer) + return; + + m_bAuthenticatingWithGameServer = true; + m_bScriptAuthenticatingWithGameServer = true; + m_bSuccessfullyAuthenticatedWithGameServer = false; + + std::string uidStr(uid); + std::string tokenStr(playerToken); + + std::thread requestThread( + [this, uidStr, tokenStr]() + { + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); + + std::string readBuffer; + curl_easy_setopt( + curl, + CURLOPT_URL, + fmt::format("{}/client/auth_with_self?id={}&playerToken={}", Cvar_ns_masterserver_hostname->GetString(), uidStr, tokenStr) + .c_str()); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); + + CURLcode result = curl_easy_perform(curl); + + if (result == CURLcode::CURLE_OK) + { + m_bSuccessfullyConnected = true; + + rapidjson_document authInfoJson; + authInfoJson.Parse(readBuffer.c_str()); + + if (authInfoJson.HasParseError()) + { + spdlog::error( + "Failed reading masterserver authentication response: encountered parse error \"{}\"", + rapidjson::GetParseError_En(authInfoJson.GetParseError())); + goto REQUEST_END_CLEANUP; + } + + if (!authInfoJson.IsObject()) + { + spdlog::error("Failed reading masterserver authentication response: root object is not an object"); + goto REQUEST_END_CLEANUP; + } + + if (authInfoJson.HasMember("error")) + { + spdlog::error("Failed reading masterserver response: got fastify error response"); + spdlog::error(readBuffer); + + if (authInfoJson["error"].HasMember("enum")) + m_sAuthFailureReason = authInfoJson["error"]["enum"].GetString(); + else + m_sAuthFailureReason = "No error message provided"; + + goto REQUEST_END_CLEANUP; + } + + if (!authInfoJson["success"].IsTrue()) + { + spdlog::error("Authentication with masterserver failed: \"success\" is not true"); + goto REQUEST_END_CLEANUP; + } + + if (!authInfoJson.HasMember("success") || !authInfoJson.HasMember("id") || !authInfoJson["id"].IsString() || + !authInfoJson.HasMember("authToken") || !authInfoJson["authToken"].IsString() || + !authInfoJson.HasMember("persistentData") || !authInfoJson["persistentData"].IsArray()) + { + spdlog::error("Failed reading masterserver authentication response: malformed json object"); + goto REQUEST_END_CLEANUP; + } + + RemoteAuthData newAuthData {}; + strncpy_s(newAuthData.uid, sizeof(newAuthData.uid), authInfoJson["id"].GetString(), sizeof(newAuthData.uid) - 1); + + newAuthData.pdataSize = authInfoJson["persistentData"].GetArray().Size(); + newAuthData.pdata = new char[newAuthData.pdataSize]; + // memcpy(newAuthData.pdata, authInfoJson["persistentData"].GetString(), newAuthData.pdataSize); + + int i = 0; + // note: persistentData is a uint8array because i had problems getting strings to behave, it sucks but it's just how it be + // unfortunately potentially refactor later + for (auto& byte : authInfoJson["persistentData"].GetArray()) + { + if (!byte.IsUint() || byte.GetUint() > 255) + { + spdlog::error("Failed reading masterserver authentication response: malformed json object"); + goto REQUEST_END_CLEANUP; + } + + newAuthData.pdata[i++] = static_cast(byte.GetUint()); + } + + std::lock_guard guard(g_pServerAuthentication->m_AuthDataMutex); + g_pServerAuthentication->m_RemoteAuthenticationData.clear(); + g_pServerAuthentication->m_RemoteAuthenticationData.insert( + std::make_pair(authInfoJson["authToken"].GetString(), newAuthData)); + + m_bSuccessfullyAuthenticatedWithGameServer = true; + } + else + { + spdlog::error("Failed authenticating with own server: error {}", curl_easy_strerror(result)); + m_bSuccessfullyConnected = false; + m_bSuccessfullyAuthenticatedWithGameServer = false; + m_bScriptAuthenticatingWithGameServer = false; + } + + REQUEST_END_CLEANUP: + m_bAuthenticatingWithGameServer = false; + m_bScriptAuthenticatingWithGameServer = false; + + if (m_bNewgameAfterSelfAuth) + { + // pretty sure this is threadsafe? + R2::Cbuf_AddText(R2::Cbuf_GetCurrentPlayer(), "ns_end_reauth_and_leave_to_lobby", R2::cmd_source_t::kCommandSrcCode); + m_bNewgameAfterSelfAuth = false; + } + + curl_easy_cleanup(curl); + }); + + requestThread.detach(); +} + +void MasterServerManager::AuthenticateWithServer(const char* uid, const char* playerToken, const char* serverId, const char* password) +{ + // dont wait, just stop if we're trying to do 2 auth requests at once + if (m_bAuthenticatingWithGameServer) + return; + + m_bAuthenticatingWithGameServer = true; + m_bScriptAuthenticatingWithGameServer = true; + m_bSuccessfullyAuthenticatedWithGameServer = false; + + std::string uidStr(uid); + std::string tokenStr(playerToken); + std::string serverIdStr(serverId); + std::string passwordStr(password); + + std::thread requestThread( + [this, uidStr, tokenStr, serverIdStr, passwordStr]() + { + // esnure that any persistence saving is done, so we know masterserver has newest + while (m_bSavingPersistentData) + Sleep(100); + + spdlog::info("Attempting authentication with server of id \"{}\"", serverIdStr); + + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); + + std::string readBuffer; + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); + + { + char* escapedPassword = curl_easy_escape(curl, passwordStr.c_str(), passwordStr.length()); + + curl_easy_setopt( + curl, + CURLOPT_URL, + fmt::format( + "{}/client/auth_with_server?id={}&playerToken={}&server={}&password={}", + Cvar_ns_masterserver_hostname->GetString(), + uidStr, + tokenStr, + serverIdStr, + escapedPassword) + .c_str()); + + curl_free(escapedPassword); + } + + CURLcode result = curl_easy_perform(curl); + + if (result == CURLcode::CURLE_OK) + { + m_bSuccessfullyConnected = true; + + rapidjson_document connectionInfoJson; + connectionInfoJson.Parse(readBuffer.c_str()); + + if (connectionInfoJson.HasParseError()) + { + spdlog::error( + "Failed reading masterserver authentication response: encountered parse error \"{}\"", + rapidjson::GetParseError_En(connectionInfoJson.GetParseError())); + goto REQUEST_END_CLEANUP; + } + + if (!connectionInfoJson.IsObject()) + { + spdlog::error("Failed reading masterserver authentication response: root object is not an object"); + goto REQUEST_END_CLEANUP; + } + + if (connectionInfoJson.HasMember("error")) + { + spdlog::error("Failed reading masterserver response: got fastify error response"); + spdlog::error(readBuffer); + + if (connectionInfoJson["error"].HasMember("enum")) + m_sAuthFailureReason = connectionInfoJson["error"]["enum"].GetString(); + else + m_sAuthFailureReason = "No error message provided"; + + goto REQUEST_END_CLEANUP; + } + + if (!connectionInfoJson["success"].IsTrue()) + { + spdlog::error("Authentication with masterserver failed: \"success\" is not true"); + goto REQUEST_END_CLEANUP; + } + + if (!connectionInfoJson.HasMember("success") || !connectionInfoJson.HasMember("ip") || + !connectionInfoJson["ip"].IsString() || !connectionInfoJson.HasMember("port") || + !connectionInfoJson["port"].IsNumber() || !connectionInfoJson.HasMember("authToken") || + !connectionInfoJson["authToken"].IsString()) + { + spdlog::error("Failed reading masterserver authentication response: malformed json object"); + goto REQUEST_END_CLEANUP; + } + + m_pendingConnectionInfo.ip.S_un.S_addr = inet_addr(connectionInfoJson["ip"].GetString()); + m_pendingConnectionInfo.port = (unsigned short)connectionInfoJson["port"].GetUint(); + + strncpy_s( + m_pendingConnectionInfo.authToken, + sizeof(m_pendingConnectionInfo.authToken), + connectionInfoJson["authToken"].GetString(), + sizeof(m_pendingConnectionInfo.authToken) - 1); + + m_bHasPendingConnectionInfo = true; + m_bSuccessfullyAuthenticatedWithGameServer = true; + } + else + { + spdlog::error("Failed authenticating with server: error {}", curl_easy_strerror(result)); + m_bSuccessfullyConnected = false; + m_bSuccessfullyAuthenticatedWithGameServer = false; + m_bScriptAuthenticatingWithGameServer = false; + } + + REQUEST_END_CLEANUP: + m_bAuthenticatingWithGameServer = false; + m_bScriptAuthenticatingWithGameServer = false; + curl_easy_cleanup(curl); + }); + + requestThread.detach(); +} + +void MasterServerManager::WritePlayerPersistentData(const char* playerId, const char* pdata, size_t pdataSize) +{ + // still call this if we don't have a server id, since lobbies that aren't port forwarded need to be able to call it + m_bSavingPersistentData = true; + if (!pdataSize) + { + spdlog::warn("attempted to write pdata of size 0!"); + return; + } + + std::string strPlayerId(playerId); + std::string strPdata(pdata, pdataSize); + + std::thread requestThread( + [this, strPlayerId, strPdata, pdataSize] + { + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); + + std::string readBuffer; + curl_easy_setopt( + curl, + CURLOPT_URL, + fmt::format( + "{}/accounts/write_persistence?id={}&serverId={}", + Cvar_ns_masterserver_hostname->GetString(), + strPlayerId, + m_sOwnServerId) + .c_str()); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); + + curl_mime* mime = curl_mime_init(curl); + curl_mimepart* part = curl_mime_addpart(mime); + + curl_mime_data(part, strPdata.c_str(), pdataSize); + curl_mime_name(part, "pdata"); + curl_mime_filename(part, "file.pdata"); + curl_mime_type(part, "application/octet-stream"); + + curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); + + CURLcode result = curl_easy_perform(curl); + + if (result == CURLcode::CURLE_OK) + m_bSuccessfullyConnected = true; + else + m_bSuccessfullyConnected = false; + + curl_easy_cleanup(curl); + + m_bSavingPersistentData = false; + }); + + requestThread.detach(); +} + +void ConCommand_ns_fetchservers(const CCommand& args) +{ + g_pMasterServerManager->RequestServerList(); +} + +MasterServerManager::MasterServerManager() : m_pendingConnectionInfo {}, m_sOwnServerId {""}, m_sOwnClientAuthToken {""} {} + +ON_DLL_LOAD_RELIESON("engine.dll", MasterServer, (ConCommand, ServerPresence), (CModule module)) +{ + g_pMasterServerManager = new MasterServerManager; + + Cvar_ns_masterserver_hostname = new ConVar("ns_masterserver_hostname", "127.0.0.1", FCVAR_NONE, ""); + Cvar_ns_curl_log_enable = new ConVar("ns_curl_log_enable", "0", FCVAR_NONE, "Whether curl should log to the console"); + + RegisterConCommand("ns_fetchservers", ConCommand_ns_fetchservers, "Fetch all servers from the masterserver", FCVAR_CLIENTDLL); + + MasterServerPresenceReporter* presenceReporter = new MasterServerPresenceReporter; + g_pServerPresence->AddPresenceReporter(presenceReporter); +} + +void MasterServerPresenceReporter::CreatePresence(const ServerPresence* pServerPresence) +{ + m_nNumRegistrationAttempts = 0; +} + +void MasterServerPresenceReporter::ReportPresence(const ServerPresence* pServerPresence) +{ + // make a copy of presence for multithreading purposes + ServerPresence threadedPresence(pServerPresence); + + if (!*g_pMasterServerManager->m_sOwnServerId) + { + // Don't try if we've reached the max registration attempts. + // In the future, we should probably allow servers to re-authenticate after a while if the MS was down. + if (m_nNumRegistrationAttempts >= MAX_REGISTRATION_ATTEMPTS) + { + return; + } + + // Make sure to wait til the cooldown is over for DUPLICATE_SERVER failures. + if (Tier0::Plat_FloatTime() < m_fNextAddServerAttemptTime) + { + return; + } + + // If we're not running any InternalAddServer() attempt in the background. + if (!addServerFuture.valid()) + { + // Launch an attempt to add the local server to the master server. + InternalAddServer(pServerPresence); + } + } + else + { + // If we're not running any InternalUpdateServer() attempt in the background. + if (!updateServerFuture.valid()) + { + // Launch an attempt to update the local server on the master server. + InternalUpdateServer(pServerPresence); + } + } +} + +void MasterServerPresenceReporter::DestroyPresence(const ServerPresence* pServerPresence) +{ + // Don't call this if we don't have a server id. + if (!*g_pMasterServerManager->m_sOwnServerId) + { + return; + } + + // Not bothering with better thread safety in this case since DestroyPresence() is called when the game is shutting down. + *g_pMasterServerManager->m_sOwnServerId = 0; + + std::thread requestThread( + [this] + { + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); + + std::string readBuffer; + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); + curl_easy_setopt( + curl, + CURLOPT_URL, + fmt::format( + "{}/server/remove_server?id={}", Cvar_ns_masterserver_hostname->GetString(), g_pMasterServerManager->m_sOwnServerId) + .c_str()); + + CURLcode result = curl_easy_perform(curl); + curl_easy_cleanup(curl); + }); + + requestThread.detach(); +} + +void MasterServerPresenceReporter::RunFrame(double flCurrentTime, const ServerPresence* pServerPresence) +{ + // Check if we're already running an InternalAddServer() call in the background. + // If so, grab the result if it's ready. + if (addServerFuture.valid()) + { + std::future_status status = addServerFuture.wait_for(0ms); + if (status != std::future_status::ready) + { + // Still running, no need to do anything. + return; + } + + // Check the result. + auto resultData = addServerFuture.get(); + + g_pMasterServerManager->m_bSuccessfullyConnected = resultData.result != MasterServerReportPresenceResult::FailedNoConnect; + + switch (resultData.result) + { + case MasterServerReportPresenceResult::Success: + // Copy over the server id and auth token granted by the MS. + strncpy_s( + g_pMasterServerManager->m_sOwnServerId, + sizeof(g_pMasterServerManager->m_sOwnServerId), + resultData.id.value().c_str(), + sizeof(g_pMasterServerManager->m_sOwnServerId) - 1); + strncpy_s( + g_pMasterServerManager->m_sOwnServerAuthToken, + sizeof(g_pMasterServerManager->m_sOwnServerAuthToken), + resultData.serverAuthToken.value().c_str(), + sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1); + break; + case MasterServerReportPresenceResult::FailedNoRetry: + case MasterServerReportPresenceResult::FailedNoConnect: + // If we failed to connect to the master server, or failed with no retry, stop trying. + m_nNumRegistrationAttempts = MAX_REGISTRATION_ATTEMPTS; + break; + case MasterServerReportPresenceResult::Failed: + ++m_nNumRegistrationAttempts; + break; + case MasterServerReportPresenceResult::FailedDuplicateServer: + ++m_nNumRegistrationAttempts; + // Wait at least twenty seconds until we re-attempt to add the server. + m_fNextAddServerAttemptTime = Tier0::Plat_FloatTime() + 20.0f; + break; + } + + if (m_nNumRegistrationAttempts >= MAX_REGISTRATION_ATTEMPTS) + { + spdlog::error("Reached max ms server registration attempts."); + } + } + else if (updateServerFuture.valid()) + { + // Check if the InternalUpdateServer() call completed. + std::future_status status = updateServerFuture.wait_for(0ms); + if (status != std::future_status::ready) + { + // Still running, no need to do anything. + return; + } + + auto resultData = updateServerFuture.get(); + if (resultData.result == MasterServerReportPresenceResult::Success) + { + if (resultData.id) + { + strncpy_s( + g_pMasterServerManager->m_sOwnServerId, + sizeof(g_pMasterServerManager->m_sOwnServerId), + resultData.id.value().c_str(), + sizeof(g_pMasterServerManager->m_sOwnServerId) - 1); + } + + if (resultData.serverAuthToken) + { + strncpy_s( + g_pMasterServerManager->m_sOwnServerAuthToken, + sizeof(g_pMasterServerManager->m_sOwnServerAuthToken), + resultData.serverAuthToken.value().c_str(), + sizeof(g_pMasterServerManager->m_sOwnServerAuthToken) - 1); + } + } + } +} + +void MasterServerPresenceReporter::InternalAddServer(const ServerPresence* pServerPresence) +{ + const ServerPresence threadedPresence(pServerPresence); + // Never call this with an ongoing InternalAddServer() call. + assert(!addServerFuture.valid()); + + g_pMasterServerManager->m_sOwnServerId[0] = 0; + g_pMasterServerManager->m_sOwnServerAuthToken[0] = 0; + + std::string modInfo = g_pMasterServerManager->m_sOwnModInfoJson; + std::string hostname = Cvar_ns_masterserver_hostname->GetString(); + + spdlog::info("Attempting to register the local server to the master server."); + + addServerFuture = std::async( + std::launch::async, + [threadedPresence, modInfo, hostname] + { + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); + + std::string readBuffer; + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); + + curl_mime* mime = curl_mime_init(curl); + curl_mimepart* part = curl_mime_addpart(mime); + + // Lambda to quickly cleanup resources and return a value. + auto ReturnCleanup = + [curl, mime](MasterServerReportPresenceResult result, const char* id = "", const char* serverAuthToken = "") + { + curl_easy_cleanup(curl); + curl_mime_free(mime); + + MasterServerPresenceReporter::ReportPresenceResultData data; + data.result = result; + data.id = id; + data.serverAuthToken = serverAuthToken; + + return data; + }; + + curl_mime_data(part, modInfo.c_str(), modInfo.size()); + curl_mime_name(part, "modinfo"); + curl_mime_filename(part, "modinfo.json"); + curl_mime_type(part, "application/json"); + + curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); + + // format every paramter because computers hate me + { + char* nameEscaped = curl_easy_escape(curl, threadedPresence.m_sServerName.c_str(), NULL); + char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL); + char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL); + char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL); + char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL); + + curl_easy_setopt( + curl, + CURLOPT_URL, + fmt::format( + "{}/server/" + "add_server?port={}&authPort={}&name={}&description={}&map={}&playlist={}&maxPlayers={}&password={}", + hostname.c_str(), + threadedPresence.m_iPort, + threadedPresence.m_iAuthPort, + nameEscaped, + descEscaped, + mapEscaped, + playlistEscaped, + threadedPresence.m_iMaxPlayers, + passwordEscaped) + .c_str()); + + curl_free(nameEscaped); + curl_free(descEscaped); + curl_free(mapEscaped); + curl_free(playlistEscaped); + curl_free(passwordEscaped); + } + + CURLcode result = curl_easy_perform(curl); + + if (result == CURLcode::CURLE_OK) + { + rapidjson_document serverAddedJson; + serverAddedJson.Parse(readBuffer.c_str()); + + // If we could not parse the JSON or it isn't an object, assume the MS is either wrong or we're completely out of date. + // No retry. + if (serverAddedJson.HasParseError()) + { + spdlog::error( + "Failed reading masterserver authentication response: encountered parse error \"{}\"", + rapidjson::GetParseError_En(serverAddedJson.GetParseError())); + return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry); + } + + if (!serverAddedJson.IsObject()) + { + spdlog::error("Failed reading masterserver authentication response: root object is not an object"); + return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry); + } + + if (serverAddedJson.HasMember("error")) + { + spdlog::error("Failed reading masterserver response: got fastify error response"); + spdlog::error(readBuffer); + + // If this is DUPLICATE_SERVER, we'll retry adding the server every 20 seconds. + // The master server will only update its internal server list and clean up dead servers on certain events. + // And then again, only if a player requests the server list after the cooldown (1 second by default), or a server is + // added/updated/removed. In any case this needs to be fixed in the master server rewrite. + if (serverAddedJson["error"].HasMember("enum") && + strcmp(serverAddedJson["error"]["enum"].GetString(), "DUPLICATE_SERVER") == 0) + { + spdlog::error("Cooling down while the master server cleans the dead server entry, if any."); + return ReturnCleanup(MasterServerReportPresenceResult::FailedDuplicateServer); + } + + // Retry until we reach max retries. + return ReturnCleanup(MasterServerReportPresenceResult::Failed); + } + + if (!serverAddedJson["success"].IsTrue()) + { + spdlog::error("Adding server to masterserver failed: \"success\" is not true"); + return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry); + } + + if (!serverAddedJson.HasMember("id") || !serverAddedJson["id"].IsString() || + !serverAddedJson.HasMember("serverAuthToken") || !serverAddedJson["serverAuthToken"].IsString()) + { + spdlog::error("Failed reading masterserver response: malformed json object"); + return ReturnCleanup(MasterServerReportPresenceResult::FailedNoRetry); + } + + spdlog::info("Successfully registered the local server to the master server."); + return ReturnCleanup( + MasterServerReportPresenceResult::Success, + serverAddedJson["id"].GetString(), + serverAddedJson["serverAuthToken"].GetString()); + } + else + { + spdlog::error("Failed adding self to server list: error {}", curl_easy_strerror(result)); + return ReturnCleanup(MasterServerReportPresenceResult::FailedNoConnect); + } + }); +} + +void MasterServerPresenceReporter::InternalUpdateServer(const ServerPresence* pServerPresence) +{ + const ServerPresence threadedPresence(pServerPresence); + + // Never call this with an ongoing InternalUpdateServer() call. + assert(!updateServerFuture.valid()); + + const std::string serverId = g_pMasterServerManager->m_sOwnServerId; + const std::string hostname = Cvar_ns_masterserver_hostname->GetString(); + const std::string modinfo = g_pMasterServerManager->m_sOwnModInfoJson; + + updateServerFuture = std::async( + std::launch::async, + [threadedPresence, serverId, hostname, modinfo] + { + CURL* curl = curl_easy_init(); + SetCommonHttpClientOptions(curl); + + // Lambda to quickly cleanup resources and return a value. + auto ReturnCleanup = [curl](MasterServerReportPresenceResult result, const char* id = "", const char* serverAuthToken = "") + { + curl_easy_cleanup(curl); + + MasterServerPresenceReporter::ReportPresenceResultData data; + data.result = result; + + if (id != nullptr) + { + data.id = id; + } + + if (serverAuthToken != nullptr) + { + data.serverAuthToken = serverAuthToken; + } + + return data; + }; + + std::string readBuffer; + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST"); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &readBuffer); + curl_easy_setopt(curl, CURLOPT_VERBOSE, 0L); + + // send all registration info so we have all necessary info to reregister our server if masterserver goes down, + // without a restart this isn't threadsafe :terror: + { + char* nameEscaped = curl_easy_escape(curl, threadedPresence.m_sServerName.c_str(), NULL); + char* descEscaped = curl_easy_escape(curl, threadedPresence.m_sServerDesc.c_str(), NULL); + char* mapEscaped = curl_easy_escape(curl, threadedPresence.m_MapName, NULL); + char* playlistEscaped = curl_easy_escape(curl, threadedPresence.m_PlaylistName, NULL); + char* passwordEscaped = curl_easy_escape(curl, threadedPresence.m_Password, NULL); + + curl_easy_setopt( + curl, + CURLOPT_URL, + fmt::format( + "{}/server/" + "update_values?id={}&port={}&authPort={}&name={}&description={}&map={}&playlist={}&playerCount={}&" + "maxPlayers={}&password={}", + hostname.c_str(), + serverId.c_str(), + threadedPresence.m_iPort, + threadedPresence.m_iAuthPort, + nameEscaped, + descEscaped, + mapEscaped, + playlistEscaped, + threadedPresence.m_iPlayerCount, + threadedPresence.m_iMaxPlayers, + passwordEscaped) + .c_str()); + + curl_free(nameEscaped); + curl_free(descEscaped); + curl_free(mapEscaped); + curl_free(playlistEscaped); + curl_free(passwordEscaped); + } + + curl_mime* mime = curl_mime_init(curl); + curl_mimepart* part = curl_mime_addpart(mime); + + curl_mime_data(part, modinfo.c_str(), modinfo.size()); + curl_mime_name(part, "modinfo"); + curl_mime_filename(part, "modinfo.json"); + curl_mime_type(part, "application/json"); + + curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); + + CURLcode result = curl_easy_perform(curl); + + if (result == CURLcode::CURLE_OK) + { + rapidjson_document serverAddedJson; + serverAddedJson.Parse(readBuffer.c_str()); + + const char* updatedId = nullptr; + const char* updatedAuthToken = nullptr; + + if (!serverAddedJson.HasParseError() && serverAddedJson.IsObject()) + { + if (serverAddedJson.HasMember("id") && serverAddedJson["id"].IsString()) + { + updatedId = serverAddedJson["id"].GetString(); + } + + if (serverAddedJson.HasMember("serverAuthToken") && serverAddedJson["serverAuthToken"].IsString()) + { + updatedAuthToken = serverAddedJson["serverAuthToken"].GetString(); + } + } + + return ReturnCleanup(MasterServerReportPresenceResult::Success, updatedId, updatedAuthToken); + } + else + { + spdlog::warn("Heartbeat failed with error {}", curl_easy_strerror(result)); + return ReturnCleanup(MasterServerReportPresenceResult::Failed); + } + }); +} diff --git a/NorthstarDLL/masterserver/masterserver.h b/NorthstarDLL/masterserver/masterserver.h new file mode 100644 index 00000000..bded9952 --- /dev/null +++ b/NorthstarDLL/masterserver/masterserver.h @@ -0,0 +1,188 @@ +#pragma once + +#include "core/convar/convar.h" +#include "server/serverpresence.h" +#include +#include +#include +#include + +extern ConVar* Cvar_ns_masterserver_hostname; +extern ConVar* Cvar_ns_curl_log_enable; + +struct RemoteModInfo +{ + public: + std::string Name; + std::string Version; +}; + +class RemoteServerInfo +{ + public: + char id[33]; // 32 bytes + nullterminator + + // server info + char name[64]; + std::string description; + char map[32]; + char playlist[16]; + char region[32]; + std::vector requiredMods; + + int playerCount; + int maxPlayers; + + // connection stuff + bool requiresPassword; + + public: + RemoteServerInfo( + const char* newId, + const char* newName, + const char* newDescription, + const char* newMap, + const char* newPlaylist, + const char* newRegion, + int newPlayerCount, + int newMaxPlayers, + bool newRequiresPassword); +}; + +struct RemoteServerConnectionInfo +{ + public: + char authToken[32]; + + in_addr ip; + unsigned short port; +}; + +struct MainMenuPromoData +{ + public: + std::string newInfoTitle1; + std::string newInfoTitle2; + std::string newInfoTitle3; + + std::string largeButtonTitle; + std::string largeButtonText; + std::string largeButtonUrl; + int largeButtonImageIndex; + + std::string smallButton1Title; + std::string smallButton1Url; + int smallButton1ImageIndex; + + std::string smallButton2Title; + std::string smallButton2Url; + int smallButton2ImageIndex; +}; + +class MasterServerManager +{ + private: + bool m_bRequestingServerList = false; + bool m_bAuthenticatingWithGameServer = false; + + public: + char m_sOwnServerId[33]; + char m_sOwnServerAuthToken[33]; + char m_sOwnClientAuthToken[33]; + + std::string m_sOwnModInfoJson; + + bool m_bOriginAuthWithMasterServerDone = false; + bool m_bOriginAuthWithMasterServerInProgress = false; + + bool m_bSavingPersistentData = false; + + bool m_bScriptRequestingServerList = false; + bool m_bSuccessfullyConnected = true; + + bool m_bNewgameAfterSelfAuth = false; + bool m_bScriptAuthenticatingWithGameServer = false; + bool m_bSuccessfullyAuthenticatedWithGameServer = false; + std::string m_sAuthFailureReason {}; + + bool m_bHasPendingConnectionInfo = false; + RemoteServerConnectionInfo m_pendingConnectionInfo; + + std::vector m_vRemoteServers; + + bool m_bHasMainMenuPromoData = false; + MainMenuPromoData m_sMainMenuPromoData; + + public: + MasterServerManager(); + + void ClearServerList(); + void RequestServerList(); + void RequestMainMenuPromos(); + 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); +}; + +extern MasterServerManager* g_pMasterServerManager; +extern ConVar* Cvar_ns_masterserver_hostname; + +/** 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 id; + std::optional 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 addServerFuture; + + // The future used for InternalAddServer() calls. + std::future updateServerFuture; + + int m_nNumRegistrationAttempts; + + double m_fNextAddServerAttemptTime; +}; diff --git a/NorthstarDLL/maxplayers.cpp b/NorthstarDLL/maxplayers.cpp deleted file mode 100644 index ece8d14b..00000000 --- a/NorthstarDLL/maxplayers.cpp +++ /dev/null @@ -1,645 +0,0 @@ -#include "pch.h" -#include "tier0.h" -#include "maxplayers.h" - -AUTOHOOK_INIT() - -// never set this to anything below 32 -#define NEW_MAX_PLAYERS 64 -// dg note: the theoretical limit is actually 100, 76 works without entity issues, and 64 works without clientside prediction issues. - -#define PAD_NUMBER(number, boundary) (((number) + ((boundary)-1)) / (boundary)) * (boundary) - -// this is horrible -constexpr int PlayerResource_Name_Start = 0; // Start of modded allocated space. -constexpr int PlayerResource_Name_Size = ((NEW_MAX_PLAYERS + 1) * 8); // const char* m_szName[MAX_PLAYERS + 1]; - -constexpr int PlayerResource_Ping_Start = PlayerResource_Name_Start + PlayerResource_Name_Size; -constexpr int PlayerResource_Ping_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int m_iPing[MAX_PLAYERS + 1]; - -constexpr int PlayerResource_Team_Start = PlayerResource_Ping_Start + PlayerResource_Ping_Size; -constexpr int PlayerResource_Team_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int m_iTeam[MAX_PLAYERS + 1]; - -constexpr int PlayerResource_PRHealth_Start = PlayerResource_Team_Start + PlayerResource_Team_Size; -constexpr int PlayerResource_PRHealth_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int m_iPRHealth[MAX_PLAYERS + 1]; - -constexpr int PlayerResource_Connected_Start = PlayerResource_PRHealth_Start + PlayerResource_PRHealth_Size; -constexpr int PlayerResource_Connected_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int (used as a bool) m_bConnected[MAX_PLAYERS + 1]; - -constexpr int PlayerResource_Alive_Start = PlayerResource_Connected_Start + PlayerResource_Connected_Size; -constexpr int PlayerResource_Alive_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int (used as a bool) m_bAlive[MAX_PLAYERS + 1]; - -constexpr int PlayerResource_BoolStats_Start = PlayerResource_Alive_Start + PlayerResource_Alive_Size; -constexpr int PlayerResource_BoolStats_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int (used as a bool idk) m_boolStats[MAX_PLAYERS + 1]; - -constexpr int PlayerResource_KillStats_Start = PlayerResource_BoolStats_Start + PlayerResource_BoolStats_Size; -constexpr int PlayerResource_KillStats_Length = PAD_NUMBER((NEW_MAX_PLAYERS + 1) * 6, 4); -constexpr int PlayerResource_KillStats_Size = (PlayerResource_KillStats_Length * 6); // int m_killStats[MAX_PLAYERS + 1][6]; - -constexpr int PlayerResource_ScoreStats_Start = PlayerResource_KillStats_Start + PlayerResource_KillStats_Size; -constexpr int PlayerResource_ScoreStats_Length = PAD_NUMBER((NEW_MAX_PLAYERS + 1) * 5, 4); -constexpr int PlayerResource_ScoreStats_Size = (PlayerResource_ScoreStats_Length * 4); // int m_scoreStats[MAX_PLAYERS + 1][5]; - -// must be the usage of the last field to account for any possible paddings -constexpr int PlayerResource_TotalSize = PlayerResource_ScoreStats_Start + PlayerResource_ScoreStats_Size; - -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; - -bool MaxPlayersIncreaseEnabled() -{ - static bool bMaxPlayersIncreaseEnabled = Tier0::CommandLine()->CheckParm("-experimentalmaxplayersincrease"); - return bMaxPlayersIncreaseEnabled; -} - -// should we use R2 for this? not sure -namespace R2 // use R2 namespace for game funcs -{ - int GetMaxPlayers() - { - if (MaxPlayersIncreaseEnabled()) - return NEW_MAX_PLAYERS; - - return 32; - } -} // namespace R2 - -template void ChangeOffset(MemoryAddress addr, unsigned int offset) -{ - addr.Patch((BYTE*)&offset, sizeof(T)); -} - -// 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")) - { - int maxPlayersPowerOf2 = 1; - while (maxPlayersPowerOf2 < NEW_MAX_PLAYERS) - maxPlayersPowerOf2 <<= 1; - - maxentries = maxPlayersPowerOf2; - } - - return StringTables_CreateStringTable(thisptr, name, maxentries, userdatafixedsize, userdatanetworkbits, flags); -} - -ON_DLL_LOAD("engine.dll", MaxPlayersOverride_Engine, (CModule module)) -{ - if (!MaxPlayersIncreaseEnabled()) - return; - - AUTOHOOK_DISPATCH_MODULE(engine.dll) - - // patch GetPlayerLimits to ignore the boundary limit - module.Offset(0x116458).Patch("0xEB"); // jle => jmp - - // patch ED_Alloc to change nFirstIndex - ChangeOffset(module.Offset(0x18F46C + 1), NEW_MAX_PLAYERS + 8 + 1); // original: 41 (sv.GetMaxClients() + 1) - - // patch CGameServer::SpawnServer to change GetMaxClients inline - ChangeOffset(module.Offset(0x119543 + 2), NEW_MAX_PLAYERS + 8 + 1); // original: 41 (sv.GetMaxClients() + 1) - - // patch CGameServer::SpawnServer to change for loop - ChangeOffset(module.Offset(0x11957F + 2), NEW_MAX_PLAYERS); // original: 32 - - // patch CGameServer::SpawnServer to change for loop (there are two) - ChangeOffset(module.Offset(0x119586 + 2), NEW_MAX_PLAYERS + 1); // original: 33 (32 + 1) - - // patch max players somewhere in CClientState - ChangeOffset(module.Offset(0x1A162C + 2), NEW_MAX_PLAYERS - 1); // original: 31 (32 - 1) - - // patch max players in userinfo stringtable creation - /*{ - int maxPlayersPowerOf2 = 1; - while (maxPlayersPowerOf2 < NEW_MAX_PLAYERS) - maxPlayersPowerOf2 <<= 1; - ChangeOffset((char*)baseAddress + 0x114B79 + 3, maxPlayersPowerOf2); // original: 32 - }*/ - // this is not supposed to work at all but it does on 64 players (how) - // proper fix below - - // patch max players in userinfo stringtable creation loop - ChangeOffset(module.Offset(0x114C48 + 2), NEW_MAX_PLAYERS); // original: 32 - - // do not load prebaked SendTable message list - module.Offset(0x75859).Patch("EB"); // jnz -> jmp -} - -typedef void (*RunUserCmds_Type)(bool a1, float a2); -RunUserCmds_Type RunUserCmds_Original; - -HMODULE serverBase = 0; -auto RandomIntZeroMax = (__int64(__fastcall*)())0; - -// lazy rebuild -// clang-format off -AUTOHOOK(RunUserCmds, server.dll + 0x483D10, -void,, (bool a1, float a2)) -// clang-format on -{ - unsigned char v3; // bl - int v5; // er14 - int i; // edi - __int64 v7; // rax - DWORD* v8; // rbx - int v9; // edi - __int64* v10; // rsi - __int64 v11; // rax - int v12; // er12 - __int64 v13; // rdi - int v14; // ebx - int v15; // eax - __int64 v16; // r8 - int v17; // edx - char v18; // r15 - char v19; // bp - int v20; // esi - __int64* v21; // rdi - __int64 v22; // rcx - bool v23; // al - __int64 v24; // rax - __int64 v25[NEW_MAX_PLAYERS]; // [rsp+20h] [rbp-138h] BYREF - - uintptr_t base = (__int64)serverBase; - auto g_pGlobals = *(__int64*)(base + 0xBFBE08); - __int64 globals = g_pGlobals; - - auto g_pEngineServer = *(__int64*)(base + 0xBFBD98); - - auto qword_1814D9648 = *(__int64*)(base + 0x14D9648); - auto qword_1814DA408 = *(__int64*)(base + 0x14DA408); - auto qword_1812107E8 = *(__int64*)(base + 0x12107E8); - auto qword_1812105A8 = *(__int64*)(base + 0x12105A8); - - auto UTIL_PlayerByIndex = (__int64(__fastcall*)(int index))(base + 0x26AA10); - auto sub_180485590 = (void(__fastcall*)(__int64))(base + 0x485590); - auto sub_18058CD80 = (void(__fastcall*)(__int64))(base + 0x58CD80); - auto sub_1805A6D90 = (void(__fastcall*)(__int64))(base + 0x5A6D90); - auto sub_1805A6E50 = (bool(__fastcall*)(__int64, int, char))(base + 0x5A6E50); - auto sub_1805A6C20 = (void(__fastcall*)(__int64))(base + 0x5A6C20); - - v3 = *(unsigned char*)(g_pGlobals + 73); - if (*(DWORD*)(qword_1814D9648 + 92) && - ((*(unsigned __int8(__fastcall**)(__int64))(*(__int64*)g_pEngineServer + 32))(g_pEngineServer) || - !*(DWORD*)(qword_1814DA408 + 92)) && - v3) - { - globals = g_pGlobals; - v5 = 1; - for (i = 1; i <= *(DWORD*)(g_pGlobals + 52); ++i) - { - v7 = UTIL_PlayerByIndex(i); - v8 = (DWORD*)v7; - if (v7) - { - *(__int64*)(base + 0x1210420) = v7; - *(float*)(g_pGlobals + 16) = a2; - if (!a1) - sub_18058CD80(v7); - sub_1805A6D90((__int64)v8); - } - globals = g_pGlobals; - } - memset(v25, 0, sizeof(v25)); - v9 = 0; - if (*(int*)(globals + 52) > 0) - { - v10 = v25; - do - { - v11 = UTIL_PlayerByIndex(++v9); - globals = g_pGlobals; - *v10++ = v11; - } while (v9 < *(DWORD*)(globals + 52)); - } - v12 = *(DWORD*)(qword_1812107E8 + 92); - if (*(DWORD*)(qword_1812105A8 + 92)) - { - v13 = *(DWORD*)(globals + 52) - 1; - if (v13 >= 1) - { - v14 = *(DWORD*)(globals + 52); - do - { - v15 = RandomIntZeroMax(); - v16 = v25[v13--]; - v17 = v15 % v14--; - v25[v13 + 1] = v25[v17]; - v25[v17] = v16; - } while (v13 >= 1); - globals = g_pGlobals; - } - } - v18 = 1; - do - { - v19 = 0; - v20 = 0; - if (*(int*)(globals + 52) > 0) - { - v21 = v25; - do - { - v22 = *v21; - if (*v21) - { - *(__int64*)(base + 0x1210420) = *v21; - *(float*)(globals + 16) = a2; - v23 = sub_1805A6E50(v22, v12, v18); - globals = g_pGlobals; - if (v23) - v19 = 1; - else - *v21 = 0; - } - ++v20; - ++v21; - } while (v20 < *(DWORD*)(globals + 52)); - } - v18 = 0; - } while (v19); - if (*(int*)(globals + 52) >= 1) - { - do - { - v24 = UTIL_PlayerByIndex(v5); - if (v24) - { - *(__int64*)(base + 0x1210420) = v24; - *(float*)(g_pGlobals + 16) = a2; - sub_1805A6C20(v24); - } - ++v5; - } while (v5 <= *(DWORD*)(g_pGlobals + 52)); - } - sub_180485590(*(__int64*)(base + 0xB7B2D8)); - } -} - -// 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(recvProp, elements, flags, name, proxyFn, unk1); -} - -ON_DLL_LOAD("server.dll", MaxPlayersOverride_Server, (CModule module)) -{ - if (!MaxPlayersIncreaseEnabled()) - return; - - AUTOHOOK_DISPATCH_MODULE(server.dll) - - // get required data - serverBase = (HMODULE)module.m_nAddress; - RandomIntZeroMax = (decltype(RandomIntZeroMax))(GetProcAddress(GetModuleHandleA("vstdlib.dll"), "RandomIntZeroMax")); - - // patch max players amount - ChangeOffset(module.Offset(0x9A44D + 3), NEW_MAX_PLAYERS); // 0x20 (32) => 0x80 (128) - - // patch SpawnGlobalNonRewinding to change forced edict index - ChangeOffset(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(module.Offset(0x5C560A + 1), CPlayerResource_ModifiedSize); - - // DT_PlayerResource::m_iPing SendProp - ChangeOffset(module.Offset(0x5C5059 + 2), CPlayerResource_OriginalSize + PlayerResource_Ping_Start); - ChangeOffset(module.Offset(0x5C50A8 + 2), CPlayerResource_OriginalSize + PlayerResource_Ping_Start); - ChangeOffset(module.Offset(0x5C50E2 + 4), NEW_MAX_PLAYERS + 1); - - // DT_PlayerResource::m_iPing DataMap - ChangeOffset(module.Offset(0xB94598), CPlayerResource_OriginalSize + PlayerResource_Ping_Start); - ChangeOffset(module.Offset(0xB9459C), NEW_MAX_PLAYERS + 1); - ChangeOffset(module.Offset(0xB945C0), PlayerResource_Ping_Size); - - // DT_PlayerResource::m_iTeam SendProp - ChangeOffset(module.Offset(0x5C5110 + 2), CPlayerResource_OriginalSize + PlayerResource_Team_Start); - ChangeOffset(module.Offset(0x5C519C + 2), CPlayerResource_OriginalSize + PlayerResource_Team_Start); - ChangeOffset(module.Offset(0x5C517E + 4), NEW_MAX_PLAYERS + 1); - - // DT_PlayerResource::m_iTeam DataMap - ChangeOffset(module.Offset(0xB94600), CPlayerResource_OriginalSize + PlayerResource_Team_Start); - ChangeOffset(module.Offset(0xB94604), NEW_MAX_PLAYERS + 1); - ChangeOffset(module.Offset(0xB94628), PlayerResource_Team_Size); - - // DT_PlayerResource::m_iPRHealth SendProp - ChangeOffset(module.Offset(0x5C51C0 + 2), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start); - ChangeOffset(module.Offset(0x5C5204 + 2), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start); - ChangeOffset(module.Offset(0x5C523E + 4), NEW_MAX_PLAYERS + 1); - - // DT_PlayerResource::m_iPRHealth DataMap - ChangeOffset(module.Offset(0xB94668), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start); - ChangeOffset(module.Offset(0xB9466C), NEW_MAX_PLAYERS + 1); - ChangeOffset(module.Offset(0xB94690), PlayerResource_PRHealth_Size); - - // DT_PlayerResource::m_bConnected SendProp - ChangeOffset(module.Offset(0x5C526C + 2), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); - ChangeOffset(module.Offset(0x5C52B4 + 2), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); - ChangeOffset(module.Offset(0x5C52EE + 4), NEW_MAX_PLAYERS + 1); - - // DT_PlayerResource::m_bConnected DataMap - ChangeOffset(module.Offset(0xB946D0), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); - ChangeOffset(module.Offset(0xB946D4), NEW_MAX_PLAYERS + 1); - ChangeOffset(module.Offset(0xB946F8), PlayerResource_Connected_Size); - - // DT_PlayerResource::m_bAlive SendProp - ChangeOffset(module.Offset(0x5C5321 + 2), CPlayerResource_OriginalSize + PlayerResource_Alive_Start); - ChangeOffset(module.Offset(0x5C5364 + 2), CPlayerResource_OriginalSize + PlayerResource_Alive_Start); - ChangeOffset(module.Offset(0x5C539E + 4), NEW_MAX_PLAYERS + 1); - - // DT_PlayerResource::m_bAlive DataMap - ChangeOffset(module.Offset(0xB94738), CPlayerResource_OriginalSize + PlayerResource_Alive_Start); - ChangeOffset(module.Offset(0xB9473C), NEW_MAX_PLAYERS + 1); - ChangeOffset(module.Offset(0xB94760), PlayerResource_Alive_Size); - - // DT_PlayerResource::m_boolStats SendProp - ChangeOffset(module.Offset(0x5C53CC + 2), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start); - ChangeOffset(module.Offset(0x5C5414 + 2), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start); - ChangeOffset(module.Offset(0x5C544E + 4), NEW_MAX_PLAYERS + 1); - - // DT_PlayerResource::m_boolStats DataMap - ChangeOffset(module.Offset(0xB947A0), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start); - ChangeOffset(module.Offset(0xB947A4), NEW_MAX_PLAYERS + 1); - ChangeOffset(module.Offset(0xB947C8), PlayerResource_BoolStats_Size); - - // DT_PlayerResource::m_killStats SendProp - ChangeOffset(module.Offset(0x5C547C + 2), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start); - ChangeOffset(module.Offset(0x5C54E2 + 2), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start); - ChangeOffset(module.Offset(0x5C54FE + 4), PlayerResource_KillStats_Length); - - // DT_PlayerResource::m_killStats DataMap - ChangeOffset(module.Offset(0xB94808), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start); - ChangeOffset(module.Offset(0xB9480C), PlayerResource_KillStats_Length); - ChangeOffset(module.Offset(0xB94830), PlayerResource_KillStats_Size); - - // DT_PlayerResource::m_scoreStats SendProp - ChangeOffset(module.Offset(0x5C5528 + 2), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); - ChangeOffset(module.Offset(0x5C5576 + 2), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); - ChangeOffset(module.Offset(0x5C5584 + 4), PlayerResource_ScoreStats_Length); - - // DT_PlayerResource::m_scoreStats DataMap - ChangeOffset(module.Offset(0xB94870), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); - ChangeOffset(module.Offset(0xB94874), PlayerResource_ScoreStats_Length); - ChangeOffset(module.Offset(0xB94898), PlayerResource_ScoreStats_Size); - - // CPlayerResource::UpdatePlayerData - m_bConnected - ChangeOffset(module.Offset(0x5C66EE + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); - ChangeOffset(module.Offset(0x5C672E + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); - - // CPlayerResource::UpdatePlayerData - m_iPing - ChangeOffset(module.Offset(0x5C6394 + 4), CPlayerResource_OriginalSize + PlayerResource_Ping_Start); - ChangeOffset(module.Offset(0x5C63DB + 4), CPlayerResource_OriginalSize + PlayerResource_Ping_Start); - - // CPlayerResource::UpdatePlayerData - m_iTeam - ChangeOffset(module.Offset(0x5C63FD + 4), CPlayerResource_OriginalSize + PlayerResource_Team_Start); - ChangeOffset(module.Offset(0x5C6442 + 4), CPlayerResource_OriginalSize + PlayerResource_Team_Start); - - // CPlayerResource::UpdatePlayerData - m_iPRHealth - ChangeOffset(module.Offset(0x5C645B + 4), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start); - ChangeOffset(module.Offset(0x5C64A0 + 4), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start); - - // CPlayerResource::UpdatePlayerData - m_bConnected - ChangeOffset(module.Offset(0x5C64AA + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); - ChangeOffset(module.Offset(0x5C64F0 + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); - - // CPlayerResource::UpdatePlayerData - m_bAlive - ChangeOffset(module.Offset(0x5C650A + 4), CPlayerResource_OriginalSize + PlayerResource_Alive_Start); - ChangeOffset(module.Offset(0x5C654F + 4), CPlayerResource_OriginalSize + PlayerResource_Alive_Start); - - // CPlayerResource::UpdatePlayerData - m_boolStats - ChangeOffset(module.Offset(0x5C6557 + 4), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start); - ChangeOffset(module.Offset(0x5C65A5 + 4), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start); - - // CPlayerResource::UpdatePlayerData - m_scoreStats - ChangeOffset(module.Offset(0x5C65C2 + 3), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); - ChangeOffset(module.Offset(0x5C65E3 + 4), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); - - // CPlayerResource::UpdatePlayerData - m_killStats - ChangeOffset(module.Offset(0x5C6654 + 3), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start); - ChangeOffset(module.Offset(0x5C665B + 3), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start); - - *module.Offset(0x14E7390).As() = 0; - auto DT_PlayerResource_Construct = module.Offset(0x5C4FE0).As<__int64(__fastcall*)()>(); - DT_PlayerResource_Construct(); - - constexpr int CTeam_OriginalSize = 3336; - constexpr int CTeam_AddedSize = Team_AddedSize; - constexpr int CTeam_ModifiedSize = CTeam_OriginalSize + CTeam_AddedSize; - - // CTeam class allocation function - allocate a bigger amount to fit all new team player data - ChangeOffset(module.Offset(0x23924A + 1), CTeam_ModifiedSize); - - // CTeam::CTeam - increase memset length to clean newly allocated data - ChangeOffset(module.Offset(0x2395AE + 2), 256 + CTeam_AddedSize); - - *module.Offset(0xC945A0).As() = 0; - auto DT_Team_Construct = module.Offset(0x238F50).As<__int64(__fastcall*)()>(); - DT_Team_Construct(); -} - -// 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(recvProp, elements, flags, name, proxyFn); -} - -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(module.Offset(0x164C41 + 1), C_PlayerResource_ModifiedSize); - - // C_PlayerResource::C_PlayerResource - change loop end value - ChangeOffset(module.Offset(0x1640C4 + 2), NEW_MAX_PLAYERS - 32); - - // C_PlayerResource::C_PlayerResource - change m_szName address - ChangeOffset( - 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( - 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(module.Offset(0x1640D0 + 3), 2244 + C_PlayerResource_AddedSize); - - // C_PlayerResource::UpdatePlayerName - change m_szName address - ChangeOffset(module.Offset(0x16431F + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); - - // C_PlayerResource::GetPlayerName - change m_szName address 1 - ChangeOffset(module.Offset(0x1645B1 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); - - // C_PlayerResource::GetPlayerName - change m_szName address 2 - ChangeOffset(module.Offset(0x1645C0 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); - - // C_PlayerResource::GetPlayerName - change m_szName address 3 - ChangeOffset(module.Offset(0x1645DD + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); - - // C_PlayerResource::GetPlayerName internal func - change m_szName address 1 - ChangeOffset(module.Offset(0x164B71 + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); - - // C_PlayerResource::GetPlayerName internal func - change m_szName address 2 - ChangeOffset(module.Offset(0x164B9B + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); - - // C_PlayerResource::GetPlayerName2 (?) - change m_szName address 1 - ChangeOffset(module.Offset(0x164641 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); - - // C_PlayerResource::GetPlayerName2 (?) - change m_szName address 2 - ChangeOffset(module.Offset(0x164650 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); - - // C_PlayerResource::GetPlayerName2 (?) - change m_szName address 3 - ChangeOffset(module.Offset(0x16466D + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); - - // C_PlayerResource::GetPlayerName internal func - change m_szName2 (?) address 1 - ChangeOffset(module.Offset(0x164BA3 + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); - - // C_PlayerResource::GetPlayerName internal func - change m_szName2 (?) address 2 - ChangeOffset(module.Offset(0x164BCE + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); - - // C_PlayerResource::GetPlayerName internal func - change m_szName2 (?) address 3 - ChangeOffset(module.Offset(0x164BE7 + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); - - // C_PlayerResource::m_szName - ChangeOffset(module.Offset(0xc350f8), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); - ChangeOffset(module.Offset(0xc350f8 + 4), NEW_MAX_PLAYERS + 1); - - // DT_PlayerResource size - ChangeOffset(module.Offset(0x163415 + 6), C_PlayerResource_ModifiedSize); - - // DT_PlayerResource::m_iPing RecvProp - ChangeOffset(module.Offset(0x163492 + 2), C_PlayerResource_OriginalSize + PlayerResource_Ping_Start); - ChangeOffset(module.Offset(0x1634D6 + 2), C_PlayerResource_OriginalSize + PlayerResource_Ping_Start); - ChangeOffset(module.Offset(0x163515 + 5), NEW_MAX_PLAYERS + 1); - - // C_PlayerResource::m_iPing - ChangeOffset(module.Offset(0xc35170), C_PlayerResource_OriginalSize + PlayerResource_Ping_Start); - ChangeOffset(module.Offset(0xc35170 + 4), NEW_MAX_PLAYERS + 1); - - // DT_PlayerResource::m_iTeam RecvProp - ChangeOffset(module.Offset(0x163549 + 2), C_PlayerResource_OriginalSize + PlayerResource_Team_Start); - ChangeOffset(module.Offset(0x1635C8 + 2), C_PlayerResource_OriginalSize + PlayerResource_Team_Start); - ChangeOffset(module.Offset(0x1635AD + 5), NEW_MAX_PLAYERS + 1); - - // C_PlayerResource::m_iTeam - ChangeOffset(module.Offset(0xc351e8), C_PlayerResource_OriginalSize + PlayerResource_Team_Start); - ChangeOffset(module.Offset(0xc351e8 + 4), NEW_MAX_PLAYERS + 1); - - // DT_PlayerResource::m_iPRHealth RecvProp - ChangeOffset(module.Offset(0x1635F9 + 2), C_PlayerResource_OriginalSize + PlayerResource_PRHealth_Start); - ChangeOffset(module.Offset(0x163625 + 2), C_PlayerResource_OriginalSize + PlayerResource_PRHealth_Start); - ChangeOffset(module.Offset(0x163675 + 5), NEW_MAX_PLAYERS + 1); - - // C_PlayerResource::m_iPRHealth - ChangeOffset(module.Offset(0xc35260), C_PlayerResource_OriginalSize + PlayerResource_PRHealth_Start); - ChangeOffset(module.Offset(0xc35260 + 4), NEW_MAX_PLAYERS + 1); - - // DT_PlayerResource::m_bConnected RecvProp - ChangeOffset(module.Offset(0x1636A9 + 2), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); - ChangeOffset(module.Offset(0x1636D5 + 2), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); - ChangeOffset(module.Offset(0x163725 + 5), NEW_MAX_PLAYERS + 1); - - // C_PlayerResource::m_bConnected - ChangeOffset(module.Offset(0xc352d8), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); - ChangeOffset(module.Offset(0xc352d8 + 4), NEW_MAX_PLAYERS + 1); - - // DT_PlayerResource::m_bAlive RecvProp - ChangeOffset(module.Offset(0x163759 + 2), C_PlayerResource_OriginalSize + PlayerResource_Alive_Start); - ChangeOffset(module.Offset(0x163785 + 2), C_PlayerResource_OriginalSize + PlayerResource_Alive_Start); - ChangeOffset(module.Offset(0x1637D5 + 5), NEW_MAX_PLAYERS + 1); - - // C_PlayerResource::m_bAlive - ChangeOffset(module.Offset(0xc35350), C_PlayerResource_OriginalSize + PlayerResource_Alive_Start); - ChangeOffset(module.Offset(0xc35350 + 4), NEW_MAX_PLAYERS + 1); - - // DT_PlayerResource::m_boolStats RecvProp - ChangeOffset(module.Offset(0x163809 + 2), C_PlayerResource_OriginalSize + PlayerResource_BoolStats_Start); - ChangeOffset(module.Offset(0x163835 + 2), C_PlayerResource_OriginalSize + PlayerResource_BoolStats_Start); - ChangeOffset(module.Offset(0x163885 + 5), NEW_MAX_PLAYERS + 1); - - // C_PlayerResource::m_boolStats - ChangeOffset(module.Offset(0xc353c8), C_PlayerResource_OriginalSize + PlayerResource_BoolStats_Start); - ChangeOffset(module.Offset(0xc353c8 + 4), NEW_MAX_PLAYERS + 1); - - // DT_PlayerResource::m_killStats RecvProp - ChangeOffset(module.Offset(0x1638B3 + 2), C_PlayerResource_OriginalSize + PlayerResource_KillStats_Start); - ChangeOffset(module.Offset(0x1638E5 + 2), C_PlayerResource_OriginalSize + PlayerResource_KillStats_Start); - ChangeOffset(module.Offset(0x163935 + 5), PlayerResource_KillStats_Length); - - // C_PlayerResource::m_killStats - ChangeOffset(module.Offset(0xc35440), C_PlayerResource_OriginalSize + PlayerResource_KillStats_Start); - ChangeOffset(module.Offset(0xc35440 + 4), PlayerResource_KillStats_Length); - - // DT_PlayerResource::m_scoreStats RecvProp - ChangeOffset(module.Offset(0x163969 + 2), C_PlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); - ChangeOffset(module.Offset(0x163995 + 2), C_PlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); - ChangeOffset(module.Offset(0x1639E5 + 5), PlayerResource_ScoreStats_Length); - - // C_PlayerResource::m_scoreStats - ChangeOffset(module.Offset(0xc354b8), C_PlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); - ChangeOffset(module.Offset(0xc354b8 + 4), PlayerResource_ScoreStats_Length); - - // C_PlayerResource::GetPlayerName - change m_bConnected address - ChangeOffset(module.Offset(0x164599 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); - - // C_PlayerResource::GetPlayerName2 (?) - change m_bConnected address - ChangeOffset(module.Offset(0x164629 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); - - // C_PlayerResource::GetPlayerName internal func - change m_bConnected address - ChangeOffset(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(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(module.Offset(0x164834 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); - - *module.Offset(0xC35068).As() = 0; - auto DT_PlayerResource_Construct = module.Offset(0x163400).As<__int64(__fastcall*)()>(); - DT_PlayerResource_Construct(); - - constexpr int C_Team_OriginalSize = 3200; - constexpr int C_Team_AddedSize = Team_AddedSize; - 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(module.Offset(0x182321 + 1), C_Team_ModifiedSize); - - // C_Team::C_Team - increase memset length to clean newly allocated data - ChangeOffset(module.Offset(0x1804A2 + 2), 256 + C_Team_AddedSize); - - // DT_Team size - ChangeOffset(module.Offset(0xC3AA0C), C_Team_ModifiedSize); - - *module.Offset(0xC3AFF8).As() = 0; - auto DT_Team_Construct = module.Offset(0x17F950).As<__int64(__fastcall*)()>(); - DT_Team_Construct(); -} diff --git a/NorthstarDLL/maxplayers.h b/NorthstarDLL/maxplayers.h deleted file mode 100644 index b251f6a6..00000000 --- a/NorthstarDLL/maxplayers.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -// should we use R2 for this? not sure -namespace R2 // use R2 namespace for game funcs -{ - int GetMaxPlayers(); -} // namespace R2 diff --git a/NorthstarDLL/memalloc.cpp b/NorthstarDLL/memalloc.cpp deleted file mode 100644 index 8c0fafc8..00000000 --- a/NorthstarDLL/memalloc.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include "pch.h" -#include "memalloc.h" -#include "tier0.h" - -using namespace Tier0; - -// TODO: rename to malloc and free after removing statically compiled .libs - -extern "C" void* _malloc_base(size_t n) -{ - // allocate into static buffer if g_pMemAllocSingleton isn't initialised - if (!g_pMemAllocSingleton) - TryCreateGlobalMemAlloc(); - - return g_pMemAllocSingleton->m_vtable->Alloc(g_pMemAllocSingleton, n); -} - -/*extern "C" void* malloc(size_t n) -{ - return _malloc_base(n); -}*/ - -extern "C" void _free_base(void* p) -{ - if (!g_pMemAllocSingleton) - TryCreateGlobalMemAlloc(); - - g_pMemAllocSingleton->m_vtable->Free(g_pMemAllocSingleton, p); -} - -extern "C" void* _realloc_base(void* oldPtr, size_t size) -{ - if (!g_pMemAllocSingleton) - TryCreateGlobalMemAlloc(); - - return g_pMemAllocSingleton->m_vtable->Realloc(g_pMemAllocSingleton, oldPtr, size); -} - -extern "C" void* _calloc_base(size_t n, size_t size) -{ - size_t bytes = n * size; - void* memory = _malloc_base(bytes); - if (memory) - { - memset(memory, 0, bytes); - } - return memory; -} - -extern "C" char* _strdup_base(const char* src) -{ - char* str; - char* p; - int len = 0; - - while (src[len]) - len++; - str = reinterpret_cast(_malloc_base(len + 1)); - p = str; - while (*src) - *p++ = *src++; - *p = '\0'; - return str; -} - -void* operator new(size_t n) -{ - return _malloc_base(n); -} - -void operator delete(void* p) noexcept -{ - _free_base(p); -} // /FORCE:MULTIPLE diff --git a/NorthstarDLL/memalloc.h b/NorthstarDLL/memalloc.h deleted file mode 100644 index 7df68429..00000000 --- a/NorthstarDLL/memalloc.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include "rapidjson/document.h" -//#include "include/rapidjson/allocators.h" - -extern "C" void* _malloc_base(size_t size); -extern "C" void* _calloc_base(size_t const count, size_t const size); -extern "C" void* _realloc_base(void* block, size_t size); -extern "C" void* _recalloc_base(void* const block, size_t const count, size_t const size); -extern "C" void _free_base(void* const block); -extern "C" char* _strdup_base(const char* src); - -void* operator new(size_t n); -void operator delete(void* p) noexcept; - -// void* malloc(size_t n); - -class SourceAllocator -{ - public: - static const bool kNeedFree = true; - void* Malloc(size_t size) - { - if (size) // behavior of malloc(0) is implementation defined. - return _malloc_base(size); - else - return NULL; // standardize to returning NULL. - } - void* Realloc(void* originalPtr, size_t originalSize, size_t newSize) - { - (void)originalSize; - if (newSize == 0) - { - _free_base(originalPtr); - return NULL; - } - return _realloc_base(originalPtr, newSize); - } - static void Free(void* ptr) - { - _free_base(ptr); - } -}; - -typedef rapidjson::GenericDocument, rapidjson::MemoryPoolAllocator, SourceAllocator> rapidjson_document; -// typedef rapidjson::GenericDocument, SourceAllocator, SourceAllocator> rapidjson_document; -// typedef rapidjson::Document rapidjson_document; -// using MyDocument = rapidjson::GenericDocument, MemoryAllocator>; -// using rapidjson_document = rapidjson::GenericDocument, SourceAllocator, SourceAllocator>; diff --git a/NorthstarDLL/memory.cpp b/NorthstarDLL/memory.cpp deleted file mode 100644 index 2e994fb2..00000000 --- a/NorthstarDLL/memory.cpp +++ /dev/null @@ -1,348 +0,0 @@ -#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(pAddress)) {} - -// operators -MemoryAddress::operator uintptr_t() const -{ - return m_nAddress; -} - -MemoryAddress::operator void*() const -{ - return reinterpret_cast(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(ret); - - return MemoryAddress(ret); -} - -// patching -void MemoryAddress::Patch(const uint8_t* pBytes, const size_t nSize) -{ - if (nSize) - WriteProcessMemory(GetCurrentProcess(), reinterpret_cast(m_nAddress), pBytes, nSize, NULL); -} - -void MemoryAddress::Patch(const std::initializer_list 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 HexBytesToString(const char* pHexString) -{ - std::vector 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 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(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(mInfo.SizeOfImage); - m_pModuleBase = reinterpret_cast(mInfo.lpBaseOfDll); - m_nAddress = m_pModuleBase; - - if (!m_nModuleSize || !m_pModuleBase) - return; - - m_pDOSHeader = reinterpret_cast(m_pModuleBase); - m_pNTHeaders = reinterpret_cast(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(hCurrentSection.Name)), - static_cast(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(GetProcAddress(reinterpret_cast(m_nAddress), pExportName))); -} - -MemoryAddress CModule::FindPattern(const uint8_t* pPattern, const char* pMask) -{ - if (!m_ExecutableCode.IsSectionValid()) - return MemoryAddress(); - - uint64_t nBase = static_cast(m_ExecutableCode.m_pSectionBase); - uint64_t nSize = static_cast(m_ExecutableCode.m_nSectionSize); - - const uint8_t* pData = reinterpret_cast(nBase); - const uint8_t* pEnd = pData + static_cast(nSize) - strlen(pMask); - - int nMasks[64]; // 64*16 = enough masks for 1024 bytes. - int iNumMasks = static_cast(ceil(static_cast(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(&nMasks[i]), j); - } - } - } - __m128i xmm1 = _mm_loadu_si128(reinterpret_cast(pPattern)); - __m128i xmm2, xmm3, msks; - for (; pData != pEnd; _mm_prefetch(reinterpret_cast(++pData + 64), _MM_HINT_NTA)) - { - if (pPattern[0] == pData[0]) - { - xmm2 = _mm_loadu_si128(reinterpret_cast(pData)); - msks = _mm_cmpeq_epi8(xmm1, xmm2); - if ((_mm_movemask_epi8(msks) & nMasks[0]) == nMasks[0]) - { - for (uintptr_t i = 1; i < static_cast(iNumMasks); ++i) - { - xmm2 = _mm_loadu_si128(reinterpret_cast((pData + i * 16))); - xmm3 = _mm_loadu_si128(reinterpret_cast((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(pData)); - } - } - else - goto CONTINUE; - } - - return MemoryAddress((&*(const_cast(pData)))); - } - } - - CONTINUE:; - } - - return MemoryAddress(); -} - -inline std::pair, std::string> MaskedBytesFromPattern(const char* pPatternString) -{ - std::vector 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()); -} diff --git a/NorthstarDLL/memory.h b/NorthstarDLL/memory.h deleted file mode 100644 index 38c76cb3..00000000 --- a/NorthstarDLL/memory.h +++ /dev/null @@ -1,90 +0,0 @@ -#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 T As() - { - return reinterpret_cast(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 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 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); -}; diff --git a/NorthstarDLL/misccommands.cpp b/NorthstarDLL/misccommands.cpp deleted file mode 100644 index f877dcc0..00000000 --- a/NorthstarDLL/misccommands.cpp +++ /dev/null @@ -1,315 +0,0 @@ -#include "pch.h" -#include "misccommands.h" -#include "concommand.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->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() -{ - RegisterConCommand( - "force_newgame", - ConCommand_force_newgame, - "forces a map load through directly setting g_pHostState->m_iNextState to HS_NEW_GAME", - FCVAR_NONE); - - 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); - - // this is a concommand because we make a deferred call to it from another thread - 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(); - - 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> 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}, - - // fcvars that should be cheats - {"net_ignoreAllSnapshots", FCVAR_CHEAT}, - {"highlight_draw", FCVAR_CHEAT}, - // these should potentially be replicated rather than cheat, like sv_footsteps is - // however they're defined on client, so can't make replicated atm sadly - {"cl_footstep_event_max_dist", FCVAR_CHEAT}, - {"cl_footstep_event_max_dist_titan", FCVAR_CHEAT}, - }; - - // array of cvars and the flags we want to remove from them - const std::vector> 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}, - - // these are devonly by default but should be modifyable - // NOTE: not all of these may actually do anything or work properly in practice - // network settings - {"cl_updaterate_mp", FCVAR_DEVELOPMENTONLY}, - {"cl_updaterate_sp", FCVAR_DEVELOPMENTONLY}, - {"clock_bias_sp", FCVAR_DEVELOPMENTONLY}, - {"clock_bias_mp", FCVAR_DEVELOPMENTONLY}, - {"cl_interpolate", FCVAR_DEVELOPMENTONLY}, // super duper ultra fucks anims if changed - {"cl_interpolateSoAllAnimsLoop", FCVAR_DEVELOPMENTONLY}, - {"cl_cmdrate", FCVAR_DEVELOPMENTONLY}, - {"cl_cmdbackup", FCVAR_DEVELOPMENTONLY}, - {"rate", FCVAR_DEVELOPMENTONLY}, - {"net_minroutable", FCVAR_DEVELOPMENTONLY}, - {"net_maxroutable", FCVAR_DEVELOPMENTONLY}, - {"net_lerpFields", FCVAR_DEVELOPMENTONLY}, - {"net_ignoreAllSnapshots", FCVAR_DEVELOPMENTONLY}, - {"net_chokeloop", FCVAR_DEVELOPMENTONLY}, - {"sv_unlag", FCVAR_DEVELOPMENTONLY}, - {"sv_maxunlag", FCVAR_DEVELOPMENTONLY}, - {"sv_lagpushticks", FCVAR_DEVELOPMENTONLY}, - {"sv_instancebaselines", FCVAR_DEVELOPMENTONLY}, - {"sv_voiceEcho", FCVAR_DEVELOPMENTONLY}, - {"net_compresspackets", FCVAR_DEVELOPMENTONLY}, - {"net_compresspackets_minsize", FCVAR_DEVELOPMENTONLY}, - {"net_verifyEncryption", FCVAR_DEVELOPMENTONLY}, // unsure if functional in retail - - // gameplay settings - {"vel_samples", FCVAR_DEVELOPMENTONLY}, - {"vel_sampleFrequency", FCVAR_DEVELOPMENTONLY}, - {"sv_friction", FCVAR_DEVELOPMENTONLY}, - {"sv_stopspeed", FCVAR_DEVELOPMENTONLY}, - {"sv_airaccelerate", FCVAR_DEVELOPMENTONLY}, - {"sv_forceGrapplesToFail", FCVAR_DEVELOPMENTONLY}, - {"sv_maxvelocity", FCVAR_DEVELOPMENTONLY}, - {"sv_footsteps", FCVAR_DEVELOPMENTONLY}, - // these 2 are flagged as CHEAT above, could be made REPLICATED later potentially - {"cl_footstep_event_max_dist", FCVAR_DEVELOPMENTONLY}, - {"cl_footstep_event_max_dist_titan", FCVAR_DEVELOPMENTONLY}, - {"sv_balanceTeams", FCVAR_DEVELOPMENTONLY}, - {"rodeo_enable", FCVAR_DEVELOPMENTONLY}, - {"sv_forceRodeoToFail", FCVAR_DEVELOPMENTONLY}, - {"player_find_rodeo_target_per_cmd", FCVAR_DEVELOPMENTONLY}, // todo test before merge - {"hud_takesshots", FCVAR_DEVELOPMENTONLY}, // very likely does not work but would be cool if it did - - {"cam_collision", FCVAR_DEVELOPMENTONLY}, - {"cam_idealdelta", FCVAR_DEVELOPMENTONLY}, - {"cam_ideallag", FCVAR_DEVELOPMENTONLY}, - {"r_drawviewmodel", FCVAR_CHEAT}, // this is a cheat command, but shouldn't be as you can achieve similar effects through mods - - // graphics/visual settings - {"mat_colorcorrection", FCVAR_DEVELOPMENTONLY}, - {"r_hbaoRadius", FCVAR_DEVELOPMENTONLY}, - {"r_hbaoDepthMax", FCVAR_DEVELOPMENTONLY}, - {"r_hbaoBlurSharpness", FCVAR_DEVELOPMENTONLY}, - {"r_hbaoIntensity", FCVAR_DEVELOPMENTONLY}, - {"r_hbaoBias", FCVAR_DEVELOPMENTONLY}, - {"r_hbaoDistanceLerp", FCVAR_DEVELOPMENTONLY}, - {"r_hbaoBlurRadius", FCVAR_DEVELOPMENTONLY}, - {"r_hbaoExponent", FCVAR_DEVELOPMENTONLY}, - {"r_hbaoDepthFadePctDefault", FCVAR_DEVELOPMENTONLY}, - {"r_drawscreenspaceparticles", FCVAR_DEVELOPMENTONLY}, - {"ui_loadingscreen_fadeout_time", FCVAR_DEVELOPMENTONLY}, - {"ui_loadingscreen_fadein_time", FCVAR_DEVELOPMENTONLY}, - {"ui_loadingscreen_transition_time", FCVAR_DEVELOPMENTONLY}, - {"ui_loadingscreen_mintransition_time", FCVAR_DEVELOPMENTONLY}, - // these 2 could be FCVAR_CHEAT, i guess? - {"cl_draw_player_model", FCVAR_DEVELOPMENTONLY}, - {"cl_always_draw_3p_player", FCVAR_DEVELOPMENTONLY}, - {"idcolor_neutral", FCVAR_DEVELOPMENTONLY}, - {"idcolor_ally", FCVAR_DEVELOPMENTONLY}, - {"idcolor_ally_cb1", FCVAR_DEVELOPMENTONLY}, - {"idcolor_ally_cb2", FCVAR_DEVELOPMENTONLY}, - {"idcolor_ally_cb3", FCVAR_DEVELOPMENTONLY}, - {"idcolor_enemy", FCVAR_DEVELOPMENTONLY}, - {"idcolor_enemy_cb1", FCVAR_DEVELOPMENTONLY}, - {"idcolor_enemy_cb2", FCVAR_DEVELOPMENTONLY}, - {"idcolor_enemy_cb3", FCVAR_DEVELOPMENTONLY}, - {"playerListPartyColorR", FCVAR_DEVELOPMENTONLY}, - {"playerListPartyColorG", FCVAR_DEVELOPMENTONLY}, - {"playerListPartyColorB", FCVAR_DEVELOPMENTONLY}, - {"playerListUseFriendColor", FCVAR_DEVELOPMENTONLY}, - {"fx_impact_neutral", FCVAR_DEVELOPMENTONLY}, - {"fx_impact_ally", FCVAR_DEVELOPMENTONLY}, - {"fx_impact_enemy", FCVAR_DEVELOPMENTONLY}, - {"hitch_alert_color", FCVAR_DEVELOPMENTONLY}, - {"particles_cull_all", FCVAR_DEVELOPMENTONLY}, - {"particles_cull_dlights", FCVAR_DEVELOPMENTONLY}, - {"map_settings_override", FCVAR_DEVELOPMENTONLY}, - {"highlight_draw", FCVAR_DEVELOPMENTONLY}, - - // sys/engine settings - {"sleep_when_meeting_framerate", FCVAR_DEVELOPMENTONLY}, - {"sleep_when_meeting_framerate_headroom_ms", FCVAR_DEVELOPMENTONLY}, - {"not_focus_sleep", FCVAR_DEVELOPMENTONLY}, - {"sp_not_focus_pause", FCVAR_DEVELOPMENTONLY}, - {"joy_requireFocus", FCVAR_DEVELOPMENTONLY}, - - {"host_thread_mode", FCVAR_DEVELOPMENTONLY}, - {"phys_enable_simd_optimizations", FCVAR_DEVELOPMENTONLY}, - {"phys_enable_experimental_optimizations", FCVAR_DEVELOPMENTONLY}, - - {"community_frame_run", FCVAR_DEVELOPMENTONLY}, - {"sv_single_core_dedi", FCVAR_DEVELOPMENTONLY}, - {"sv_stressbots", FCVAR_DEVELOPMENTONLY}, - - {"fatal_script_errors", FCVAR_DEVELOPMENTONLY}, - {"fatal_script_errors_client", FCVAR_DEVELOPMENTONLY}, - {"fatal_script_errors_server", FCVAR_DEVELOPMENTONLY}, - {"script_error_on_midgame_load", FCVAR_DEVELOPMENTONLY}, // idk what this is - - {"ai_ainRebuildOnMapStart", FCVAR_DEVELOPMENTONLY}, - - {"save_enable", FCVAR_DEVELOPMENTONLY}, - - // cheat commands - {"switchclass", FCVAR_DEVELOPMENTONLY}, - {"set", FCVAR_DEVELOPMENTONLY}, - {"_setClassVarServer", FCVAR_DEVELOPMENTONLY}, - - // reparse commands - {"aisettings_reparse", FCVAR_DEVELOPMENTONLY}, - {"aisettings_reparse_client", FCVAR_DEVELOPMENTONLY}, - {"damagedefs_reparse", FCVAR_DEVELOPMENTONLY}, - {"damagedefs_reparse_client", FCVAR_DEVELOPMENTONLY}, - {"playerSettings_reparse", FCVAR_DEVELOPMENTONLY}, - {"_playerSettings_reparse_Server", FCVAR_DEVELOPMENTONLY}, - - }; - - const std::vector> CVAR_FIXUP_DEFAULT_VALUES = { - {"sv_stressbots", "0"}, // not currently used but this is probably a bad default if we get bots working - {"cl_pred_optimize", "0"} // fixes issues with animation prediction in thirdperson - }; - - 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); - } - - for (auto& fixup : CVAR_FIXUP_DEFAULT_VALUES) - { - ConVar* cvar = R2::g_pCVar->FindVar(std::get<0>(fixup)); - if (cvar && !strcmp(cvar->GetString(), cvar->m_pszDefaultValue)) - { - cvar->SetValue(std::get<1>(fixup)); - cvar->m_pszDefaultValue = std::get<1>(fixup); - } - } -} diff --git a/NorthstarDLL/misccommands.h b/NorthstarDLL/misccommands.h deleted file mode 100644 index 07a07fb3..00000000 --- a/NorthstarDLL/misccommands.h +++ /dev/null @@ -1,3 +0,0 @@ -#pragma once -void AddMiscConCommands(); -void FixupCvarFlags(); diff --git a/NorthstarDLL/miscserverfixes.cpp b/NorthstarDLL/miscserverfixes.cpp deleted file mode 100644 index 4feca505..00000000 --- a/NorthstarDLL/miscserverfixes.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include "pch.h" - -ON_DLL_LOAD("server.dll", MiscServerFixes, (CModule module)) -{ - // nop out call to VGUI shutdown since it crashes the game when quitting from the console - module.Offset(0x154A96).NOP(5); -} diff --git a/NorthstarDLL/miscserverscript.cpp b/NorthstarDLL/miscserverscript.cpp deleted file mode 100644 index a8e7264b..00000000 --- a/NorthstarDLL/miscserverscript.cpp +++ /dev/null @@ -1,60 +0,0 @@ -#include "pch.h" -#include "squirrel.h" -#include "masterserver.h" -#include "serverauthentication.h" -#include "dedicated.h" -#include "r2client.h" -#include "r2server.h" - -#include - -ADD_SQFUNC("void", NSEarlyWritePlayerPersistenceForLeave, "entity player", "", ScriptContext::SERVER) -{ - const R2::CBasePlayer* pPlayer = g_pSquirrel->getentity(sqvm, 1); - if (!pPlayer) - { - spdlog::warn("NSEarlyWritePlayerPersistenceForLeave got null player"); - - g_pSquirrel->pushbool(sqvm, false); - return SQRESULT_NOTNULL; - } - - R2::CBaseClient* pClient = &R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1]; - if (g_pServerAuthentication->m_PlayerAuthenticationData.find(pClient) == g_pServerAuthentication->m_PlayerAuthenticationData.end()) - { - g_pSquirrel->pushbool(sqvm, false); - return SQRESULT_NOTNULL; - } - - g_pServerAuthentication->m_PlayerAuthenticationData[pClient].needPersistenceWriteOnLeave = false; - g_pServerAuthentication->WritePersistentData(pClient); - return SQRESULT_NULL; -} - -ADD_SQFUNC("bool", NSIsWritingPlayerPersistence, "", "", ScriptContext::SERVER) -{ - g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_bSavingPersistentData); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("bool", NSIsPlayerLocalPlayer, "entity player", "", ScriptContext::SERVER) -{ - const R2::CBasePlayer* pPlayer = g_pSquirrel->getentity(sqvm, 1); - if (!pPlayer) - { - spdlog::warn("NSIsPlayerLocalPlayer got null player"); - - g_pSquirrel->pushbool(sqvm, false); - return SQRESULT_NOTNULL; - } - - R2::CBaseClient* pClient = &R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1]; - g_pSquirrel->pushbool(sqvm, !strcmp(R2::g_pLocalPlayerUserID, pClient->m_UID)); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("bool", NSIsDedicated, "", "", ScriptContext::SERVER) -{ - g_pSquirrel->pushbool(sqvm, IsDedicatedServer()); - return SQRESULT_NOTNULL; -} diff --git a/NorthstarDLL/modkeyvalues.cpp b/NorthstarDLL/modkeyvalues.cpp deleted file mode 100644 index 75188329..00000000 --- a/NorthstarDLL/modkeyvalues.cpp +++ /dev/null @@ -1,107 +0,0 @@ -#include "pch.h" -#include "modmanager.h" -#include "filesystem.h" - -#include - -AUTOHOOK_INIT() - -void ModManager::TryBuildKeyValues(const char* filename) -{ - spdlog::info("Building KeyValues for file {}", filename); - - std::string normalisedPath = g_pModManager->NormaliseModFilePath(fs::path(filename)); - fs::path compiledPath = GetCompiledAssetsPath() / filename; - fs::path compiledDir = compiledPath.parent_path(); - fs::create_directories(compiledDir); - - fs::path kvPath(filename); - std::string ogFilePath = "mod_original_"; - ogFilePath += kvPath.filename().string(); - - std::string newKvs = "// AUTOGENERATED: MOD PATCH KV\n"; - - int patchNum = 0; - - // 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--) - { - 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()) - { - // should result in smth along the lines of #include "mod_patch_5_mp_weapon_car.txt" - - std::string patchFilePath = "mod_patch_"; - patchFilePath += std::to_string(patchNum++); - patchFilePath += "_"; - patchFilePath += kvPath.filename().string(); - - newKvs += "#base \""; - newKvs += patchFilePath; - newKvs += "\"\n"; - - fs::remove(compiledDir / patchFilePath); - - fs::copy_file(m_LoadedMods[i].m_ModDirectory / "keyvalues" / filename, compiledDir / patchFilePath); - } - } - - // add original #base last, #bases don't override preexisting keys, including the ones we've just done - newKvs += "#base \""; - newKvs += ogFilePath; - newKvs += "\"\n"; - - // load original file, so we can parse out the name of the root obj (e.g. WeaponData for weapons) - std::string originalFile = R2::ReadVPKOriginalFile(filename); - - if (!originalFile.length()) - { - spdlog::warn("Tried to patch kv {} but no base kv was found!", ogFilePath); - return; - } - - char rootName[64]; - memset(rootName, 0, sizeof(rootName)); - - // iterate until we hit an ascii char that isn't in a # command or comment to get root obj name - int i = 0; - while (!(originalFile[i] >= 65 && originalFile[i] <= 122)) - { - // if we hit a comment or # thing, iterate until end of line - if (originalFile[i] == '/' || originalFile[i] == '#') - while (originalFile[i] != '\n') - i++; - - i++; - } - - int j = 0; - for (int j = 0; originalFile[i] >= 65 && originalFile[i] <= 122; j++) - rootName[j] = originalFile[i++]; - - // empty kv, all the other stuff gets #base'd - newKvs += rootName; - newKvs += "\n{\n}\n"; - - std::ofstream originalFileWriteStream(compiledDir / ogFilePath, std::ios::binary); - originalFileWriteStream << originalFile; - originalFileWriteStream.close(); - - std::ofstream writeStream(compiledPath, std::ios::binary); - writeStream << newKvs; - writeStream.close(); - - ModOverrideFile overrideFile; - overrideFile.m_pOwningMod = nullptr; - overrideFile.m_Path = normalisedPath; - - if (m_ModFiles.find(normalisedPath) == m_ModFiles.end()) - m_ModFiles.insert(std::make_pair(normalisedPath, overrideFile)); - else - m_ModFiles[normalisedPath] = overrideFile; -} diff --git a/NorthstarDLL/modlocalisation.cpp b/NorthstarDLL/modlocalisation.cpp deleted file mode 100644 index b12f0a40..00000000 --- a/NorthstarDLL/modlocalisation.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "pch.h" -#include "modmanager.h" - -AUTOHOOK_INIT() - -// clang-format off -AUTOHOOK(AddLocalisationFile, localize.dll + 0x6D80, -bool, __fastcall, (void* pVguiLocalize, const char* path, const char* pathId, char unknown)) -// clang-format on -{ - static bool bLoadModLocalisationFiles = true; - bool ret = AddLocalisationFile(pVguiLocalize, path, pathId, unknown); - - if (ret) - spdlog::info("Loaded localisation file {} successfully", path); - - if (!bLoadModLocalisationFiles) - return ret; - - bLoadModLocalisationFiles = false; - - for (Mod mod : g_pModManager->m_LoadedMods) - if (mod.m_bEnabled) - for (std::string& localisationFile : mod.LocalisationFiles) - AddLocalisationFile(pVguiLocalize, localisationFile.c_str(), pathId, unknown); - - bLoadModLocalisationFiles = true; - - return ret; -} - -ON_DLL_LOAD_CLIENT("localize.dll", Localize, (CModule module)) -{ - AUTOHOOK_DISPATCH() -} diff --git a/NorthstarDLL/modmanager.cpp b/NorthstarDLL/modmanager.cpp deleted file mode 100644 index d95bcbce..00000000 --- a/NorthstarDLL/modmanager.cpp +++ /dev/null @@ -1,727 +0,0 @@ -#include "pch.h" -#include "modmanager.h" -#include "convar.h" -#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" -#include "rapidjson/writer.h" -#include -#include -#include -#include -#include - -ModManager* g_pModManager; - -Mod::Mod(fs::path modDir, char* jsonBuf) -{ - m_bWasReadSuccessfully = false; - - m_ModDirectory = modDir; - - rapidjson_document modJson; - modJson.Parse(jsonBuf); - - // fail if parse error - if (modJson.HasParseError()) - { - spdlog::error( - "Failed reading mod file {}: encountered parse error \"{}\" at offset {}", - (modDir / "mod.json").string(), - GetParseError_En(modJson.GetParseError()), - modJson.GetErrorOffset()); - return; - } - - // fail if it's not a json obj (could be an array, string, etc) - if (!modJson.IsObject()) - { - spdlog::error("Failed reading mod file {}: file is not a JSON object", (modDir / "mod.json").string()); - return; - } - - // basic mod info - // name is required - if (!modJson.HasMember("Name")) - { - spdlog::error("Failed reading mod file {}: missing required member \"Name\"", (modDir / "mod.json").string()); - return; - } - - Name = modJson["Name"].GetString(); - - if (modJson.HasMember("Description")) - Description = modJson["Description"].GetString(); - else - Description = ""; - - if (modJson.HasMember("Version")) - Version = modJson["Version"].GetString(); - else - { - Version = "0.0.0"; - spdlog::warn("Mod file {} is missing a version, consider adding a version", (modDir / "mod.json").string()); - } - - if (modJson.HasMember("DownloadLink")) - DownloadLink = modJson["DownloadLink"].GetString(); - else - DownloadLink = ""; - - if (modJson.HasMember("RequiredOnClient")) - RequiredOnClient = modJson["RequiredOnClient"].GetBool(); - else - RequiredOnClient = false; - - if (modJson.HasMember("LoadPriority")) - LoadPriority = modJson["LoadPriority"].GetInt(); - else - { - spdlog::info("Mod file {} is missing a LoadPriority, consider adding one", (modDir / "mod.json").string()); - LoadPriority = 0; - } - - // mod convars - if (modJson.HasMember("ConVars") && modJson["ConVars"].IsArray()) - { - for (auto& convarObj : modJson["ConVars"].GetArray()) - { - if (!convarObj.IsObject() || !convarObj.HasMember("Name") || !convarObj.HasMember("DefaultValue")) - continue; - - // have to allocate this manually, otherwise convar registration will break - // unfortunately this causes us to leak memory on reload, unsure of a way around this rn - ModConVar* convar = new ModConVar; - convar->Name = convarObj["Name"].GetString(); - convar->DefaultValue = convarObj["DefaultValue"].GetString(); - - if (convarObj.HasMember("HelpString")) - convar->HelpString = convarObj["HelpString"].GetString(); - else - convar->HelpString = ""; - - convar->Flags = FCVAR_NONE; - - if (convarObj.HasMember("Flags")) - { - // 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); - } - } - - // mod scripts - if (modJson.HasMember("Scripts") && modJson["Scripts"].IsArray()) - { - for (auto& scriptObj : modJson["Scripts"].GetArray()) - { - if (!scriptObj.IsObject() || !scriptObj.HasMember("Path") || !scriptObj.HasMember("RunOn")) - continue; - - ModScript script; - - script.Path = scriptObj["Path"].GetString(); - script.RunOn = scriptObj["RunOn"].GetString(); - - if (scriptObj.HasMember("ServerCallback") && scriptObj["ServerCallback"].IsObject()) - { - ModScriptCallback callback; - callback.Context = ScriptContext::SERVER; - - if (scriptObj["ServerCallback"].HasMember("Before") && scriptObj["ServerCallback"]["Before"].IsString()) - callback.BeforeCallback = scriptObj["ServerCallback"]["Before"].GetString(); - - if (scriptObj["ServerCallback"].HasMember("After") && scriptObj["ServerCallback"]["After"].IsString()) - callback.AfterCallback = scriptObj["ServerCallback"]["After"].GetString(); - - script.Callbacks.push_back(callback); - } - - if (scriptObj.HasMember("ClientCallback") && scriptObj["ClientCallback"].IsObject()) - { - ModScriptCallback callback; - callback.Context = ScriptContext::CLIENT; - - if (scriptObj["ClientCallback"].HasMember("Before") && scriptObj["ClientCallback"]["Before"].IsString()) - callback.BeforeCallback = scriptObj["ClientCallback"]["Before"].GetString(); - - if (scriptObj["ClientCallback"].HasMember("After") && scriptObj["ClientCallback"]["After"].IsString()) - callback.AfterCallback = scriptObj["ClientCallback"]["After"].GetString(); - - script.Callbacks.push_back(callback); - } - - if (scriptObj.HasMember("UICallback") && scriptObj["UICallback"].IsObject()) - { - ModScriptCallback callback; - callback.Context = ScriptContext::UI; - - if (scriptObj["UICallback"].HasMember("Before") && scriptObj["UICallback"]["Before"].IsString()) - callback.BeforeCallback = scriptObj["UICallback"]["Before"].GetString(); - - if (scriptObj["UICallback"].HasMember("After") && scriptObj["UICallback"]["After"].IsString()) - callback.AfterCallback = scriptObj["UICallback"]["After"].GetString(); - - script.Callbacks.push_back(callback); - } - - Scripts.push_back(script); - } - } - - if (modJson.HasMember("Localisation") && modJson["Localisation"].IsArray()) - { - for (auto& localisationStr : modJson["Localisation"].GetArray()) - { - if (!localisationStr.IsString()) - continue; - - LocalisationFiles.push_back(localisationStr.GetString()); - } - } - - if (modJson.HasMember("Dependencies") && modJson["Dependencies"].IsObject()) - { - for (auto v = modJson["Dependencies"].MemberBegin(); v != modJson["Dependencies"].MemberEnd(); v++) - { - if (!v->name.IsString() || !v->value.IsString()) - continue; - - spdlog::info("Constant {} defined by {} for mod {}", v->name.GetString(), Name, v->value.GetString()); - if (DependencyConstants.find(v->name.GetString()) != DependencyConstants.end() && - v->value.GetString() != DependencyConstants[v->name.GetString()]) - { - spdlog::error("A dependency constant with the same name already exists for another mod. Change the constant name."); - return; - } - - if (DependencyConstants.find(v->name.GetString()) == DependencyConstants.end()) - DependencyConstants.emplace(v->name.GetString(), v->value.GetString()); - } - } - - m_bWasReadSuccessfully = true; -} - -ModManager::ModManager() -{ - // precaculated string hashes - // note: use backslashes for these, since we use lexically_normal for file paths which uses them - m_hScriptsRsonHash = STR_HASH("scripts\\vscripts\\scripts.rson"); - m_hPdefHash = STR_HASH( - "cfg\\server\\persistent_player_data_version_231.pdef" // this can have multiple versions, but we use 231 so that's what we hash - ); - m_hKBActHash = STR_HASH("scripts\\kb_act.lst"); - - LoadMods(); -} - -void ModManager::LoadMods() -{ - if (m_bHasLoadedMods) - UnloadMods(); - - std::vector modDirs; - - // ensure dirs exist - fs::remove_all(GetCompiledAssetsPath()); - fs::create_directories(GetModFolderPath()); - - m_DependencyConstants.clear(); - - // read enabled mods cfg - std::ifstream enabledModsStream(GetNorthstarPrefix() + "/enabledmods.json"); - std::stringstream enabledModsStringStream; - - if (!enabledModsStream.fail()) - { - while (enabledModsStream.peek() != EOF) - enabledModsStringStream << (char)enabledModsStream.get(); - - enabledModsStream.close(); - m_EnabledModsCfg.Parse( - enabledModsStringStream.str().c_str()); - - m_bHasEnabledModsCfg = m_EnabledModsCfg.IsObject(); - } - - // get mod directories - for (fs::directory_entry dir : fs::directory_iterator(GetModFolderPath())) - if (fs::exists(dir.path() / "mod.json")) - modDirs.push_back(dir.path()); - - for (fs::path modDir : modDirs) - { - // read mod json file - std::ifstream jsonStream(modDir / "mod.json"); - std::stringstream jsonStringStream; - - // fail if no mod json - if (jsonStream.fail()) - { - spdlog::warn("Mod {} has a directory but no mod.json", modDir.string()); - continue; - } - - while (jsonStream.peek() != EOF) - jsonStringStream << (char)jsonStream.get(); - - jsonStream.close(); - - Mod mod(modDir, (char*)jsonStringStream.str().c_str()); - - for (auto& pair : mod.DependencyConstants) - { - 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.m_bWasReadSuccessfully = false; - break; - } - if (m_DependencyConstants.find(pair.first) == m_DependencyConstants.end()) - m_DependencyConstants.emplace(pair); - } - - if (m_bHasEnabledModsCfg && m_EnabledModsCfg.HasMember(mod.Name.c_str())) - mod.m_bEnabled = m_EnabledModsCfg[mod.Name.c_str()].IsTrue(); - else - mod.m_bEnabled = true; - - if (mod.m_bWasReadSuccessfully) - { - spdlog::info("Loaded mod {} successfully", mod.Name); - if (mod.m_bEnabled) - spdlog::info("Mod {} is enabled", mod.Name); - else - spdlog::info("Mod {} is disabled", mod.Name); - - 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; }); - - for (Mod& mod : m_LoadedMods) - { - if (!mod.m_bEnabled) - continue; - - // register convars - // for reloads, this is sorta barebones, when we have a good findconvar method, we could probably reset flags and stuff on - // 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 (!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.m_ModDirectory / "vpk")) - { - // read vpk cfg - std::ifstream vpkJsonStream(mod.m_ModDirectory / "vpk/vpk.json"); - std::stringstream vpkJsonStringStream; - - bool bUseVPKJson = false; - rapidjson::Document dVpkJson; - - if (!vpkJsonStream.fail()) - { - while (vpkJsonStream.peek() != EOF) - vpkJsonStringStream << (char)vpkJsonStream.get(); - - vpkJsonStream.close(); - dVpkJson.Parse( - vpkJsonStringStream.str().c_str()); - - bUseVPKJson = !dVpkJson.HasParseError() && dVpkJson.IsObject(); - } - - 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 - if (fs::is_regular_file(file) && file.path().extension() == ".vpk" && - file.path().string().find("english") != std::string::npos && - file.path().string().find(".bsp.pak000_dir") != std::string::npos) - { - std::string formattedPath = file.path().filename().string(); - - // this really fucking sucks but it'll work - 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 = (file.path().parent_path() / vpkName).string(); - - 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.m_ModDirectory / "paks")) - { - // read rpak cfg - std::ifstream rpakJsonStream(mod.m_ModDirectory / "paks/rpak.json"); - std::stringstream rpakJsonStringStream; - - bool bUseRpakJson = false; - rapidjson::Document dRpakJson; - - if (!rpakJsonStream.fail()) - { - while (rpakJsonStream.peek() != EOF) - rpakJsonStringStream << (char)rpakJsonStream.get(); - - rpakJsonStream.close(); - dRpakJson.Parse( - rpakJsonStringStream.str().c_str()); - - bUseRpakJson = !dRpakJson.HasParseError() && dRpakJson.IsObject(); - } - - // read pak aliases - if (bUseRpakJson && dRpakJson.HasMember("Aliases") && dRpakJson["Aliases"].IsObject()) - { - for (rapidjson::Value::ConstMemberIterator iterator = dRpakJson["Aliases"].MemberBegin(); - iterator != dRpakJson["Aliases"].MemberEnd(); - iterator++) - { - if (!iterator->name.IsString() || !iterator->value.IsString()) - continue; - - mod.RpakAliases.insert(std::make_pair(iterator->name.GetString(), iterator->value.GetString())); - } - } - - 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") - { - std::string pakName(file.path().filename().string()); - - ModRpakEntry& modPak = mod.Rpaks.emplace_back(); - 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; - - // read header of file and get the starpak paths - // this is done here as opposed to on starpak load because multiple rpaks can load a starpak - // and there is seemingly no good way to tell which rpak is causing the load of a starpak :/ - - std::ifstream rpakStream(file.path(), std::ios::binary); - - // seek to the point in the header where the starpak reference size is - rpakStream.seekg(0x38, std::ios::beg); - int starpaksSize = 0; - rpakStream.read((char*)&starpaksSize, 2); - - // seek to just after the header - rpakStream.seekg(0x58, std::ios::beg); - // read the starpak reference(s) - std::vector buf(starpaksSize); - rpakStream.read(buf.data(), starpaksSize); - - rpakStream.close(); - - // split the starpak reference(s) into strings to hash - std::string str = ""; - for (int i = 0; i < starpaksSize; i++) - { - // if the current char is null, that signals the end of the current starpak path - if (buf[i] != 0x00) - { - str += buf[i]; - } - else - { - // only add the string we are making if it isnt empty - if (!str.empty()) - { - mod.StarpakPaths.push_back(STR_HASH(str)); - spdlog::info("Mod {} registered starpak '{}'", mod.Name, str); - str = ""; - } - } - } - - // not using atm because we need to resolve path to rpak - // if (m_hasLoadedMods && modPak.m_bAutoLoad) - // g_pPakLoadManager->LoadPakAsync(pakName.c_str()); - } - } - } - - // read keyvalues paths - if (fs::exists(mod.m_ModDirectory / "keyvalues")) - { - for (fs::directory_entry file : fs::recursive_directory_iterator(mod.m_ModDirectory / "keyvalues")) - { - if (fs::is_regular_file(file)) - { - 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.m_ModDirectory / "mod.pdiff")) - { - std::ifstream pdiffStream(mod.m_ModDirectory / "mod.pdiff"); - - if (!pdiffStream.fail()) - { - std::stringstream pdiffStringStream; - while (pdiffStream.peek() != EOF) - pdiffStringStream << (char)pdiffStream.get(); - - pdiffStream.close(); - - mod.Pdiff = pdiffStringStream.str(); - } - } - - // read bink video paths - if (fs::exists(mod.m_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.m_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") - { - if (!g_CustomAudioManager.TryLoadAudioOverride(file.path())) - { - spdlog::warn("Mod {} has an invalid audio def {}", mod.Name, file.path().filename().string()); - continue; - } - } - } - } - } - - // 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--) - { - if (!m_LoadedMods[i].m_bEnabled) - continue; - - if (fs::exists(m_LoadedMods[i].m_ModDirectory / MOD_OVERRIDE_DIR)) - { - for (fs::directory_entry file : fs::recursive_directory_iterator(m_LoadedMods[i].m_ModDirectory / MOD_OVERRIDE_DIR)) - { - 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.m_pOwningMod = &m_LoadedMods[i]; - modFile.m_Path = path; - m_ModFiles.insert(std::make_pair(path, modFile)); - } - } - } - } - - // build modinfo obj for masterserver - rapidjson_document modinfoDoc; - modinfoDoc.SetObject(); - modinfoDoc.AddMember("Mods", rapidjson_document::GenericValue(rapidjson::kArrayType), modinfoDoc.GetAllocator()); - - int currentModIndex = 0; - for (Mod& mod : m_LoadedMods) - { - if (!mod.m_bEnabled || (!mod.RequiredOnClient && !mod.Pdiff.size())) - continue; - - modinfoDoc["Mods"].PushBack(rapidjson_document::GenericValue(rapidjson::kObjectType), modinfoDoc.GetAllocator()); - modinfoDoc["Mods"][currentModIndex].AddMember("Name", rapidjson::StringRef(&mod.Name[0]), modinfoDoc.GetAllocator()); - modinfoDoc["Mods"][currentModIndex].AddMember("Version", rapidjson::StringRef(&mod.Version[0]), modinfoDoc.GetAllocator()); - modinfoDoc["Mods"][currentModIndex].AddMember("RequiredOnClient", mod.RequiredOnClient, modinfoDoc.GetAllocator()); - modinfoDoc["Mods"][currentModIndex].AddMember("Pdiff", rapidjson::StringRef(&mod.Pdiff[0]), modinfoDoc.GetAllocator()); - - currentModIndex++; - } - - rapidjson::StringBuffer buffer; - buffer.Clear(); - rapidjson::Writer writer(buffer); - modinfoDoc.Accept(writer); - g_pMasterServerManager->m_sOwnModInfoJson = std::string(buffer.GetString()); - - m_bHasLoadedMods = true; -} - -void ModManager::UnloadMods() -{ - // clean up stuff from mods before we unload - m_ModFiles.clear(); - fs::remove_all(GetCompiledAssetsPath()); - - g_CustomAudioManager.ClearAudioOverrides(); - - if (!m_bHasEnabledModsCfg) - m_EnabledModsCfg.SetObject(); - - for (Mod& mod : m_LoadedMods) - { - // remove all built kvs - for (std::pair kvPaths : mod.KeyValues) - fs::remove(GetCompiledAssetsPath() / fs::path(kvPaths.second).lexically_relative(mod.m_ModDirectory)); - - mod.KeyValues.clear(); - - // write to m_enabledModsCfg - // 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( - rapidjson_document::StringRefType(mod.Name.c_str()), - rapidjson_document::GenericValue(false), - m_EnabledModsCfg.GetAllocator()); - - m_EnabledModsCfg[mod.Name.c_str()].SetBool(mod.m_bEnabled); - } - - std::ofstream writeStream(GetNorthstarPrefix() + "/enabledmods.json"); - rapidjson::OStreamWrapper writeStreamWrapper(writeStream); - rapidjson::Writer writer(writeStreamWrapper); - m_EnabledModsCfg.Accept(writer); - - // do we need to dealloc individual entries in m_loadedMods? idk, rework - 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(NormaliseModFilePath(fs::path(filename))); - - if (fileHash == m_hScriptsRsonHash) - BuildScriptsRson(); - else if (fileHash == m_hPdefHash) - BuildPdef(); - else if (fileHash == m_hKBActHash) - BuildKBActionsList(); - 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) - { - if (!mod.m_bEnabled) - continue; - - if (mod.KeyValues.find(fileHash) != mod.KeyValues.end()) - { - TryBuildKeyValues(filename); - return; - } - } - } -} - -void ConCommand_reload_mods(const CCommand& args) -{ - g_pModManager->LoadMods(); -} - -fs::path GetModFolderPath() -{ - return fs::path(GetNorthstarPrefix() + MOD_FOLDER_SUFFIX); -} -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); -} diff --git a/NorthstarDLL/modmanager.h b/NorthstarDLL/modmanager.h deleted file mode 100644 index 6540a3de..00000000 --- a/NorthstarDLL/modmanager.h +++ /dev/null @@ -1,154 +0,0 @@ -#pragma once -#include "convar.h" -#include "memalloc.h" -#include "squirrel.h" - -#include "rapidjson/document.h" -#include -#include -#include - -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"; - -struct ModConVar -{ - public: - std::string Name; - std::string DefaultValue; - std::string HelpString; - int Flags; -}; - -struct ModScriptCallback -{ - public: - ScriptContext Context; - - // called before the codecallback is executed - std::string BeforeCallback; - // called after the codecallback has finished executing - std::string AfterCallback; -}; - -struct ModScript -{ - public: - std::string Path; - std::string RunOn; - - std::vector Callbacks; -}; - -// these are pretty much identical, could refactor to use the same stuff? -struct ModVPKEntry -{ - public: - bool m_bAutoLoad; - std::string m_sVpkPath; -}; - -struct ModRpakEntry -{ - public: - bool m_bAutoLoad; - std::string m_sPakName; - std::string m_sLoadAfterPak; -}; - -class Mod -{ - public: - // runtime stuff - bool m_bEnabled = true; - bool m_bWasReadSuccessfully = false; - fs::path m_ModDirectory; - // bool m_bIsRemote; - - // mod.json stuff: - - // the mod's name - std::string Name; - // the mod's description - std::string Description; - // the mod's version, should be in semver - std::string Version; - // a download link to the mod, for clients that try to join without the mod - std::string DownloadLink; - - // whether clients need the mod to join servers running this mod - bool RequiredOnClient; - // the priority for this mod's files, mods with prio 0 are loaded first, then 1, then 2, etc - int LoadPriority; - - // custom scripts used by the mod - std::vector Scripts; - // convars created by the mod - std::vector ConVars; - // custom localisation files created by the mod - std::vector LocalisationFiles; - - // other files: - - std::vector Vpks; - std::unordered_map KeyValues; - std::vector BinkVideos; - std::string Pdiff; // only need one per mod - - std::vector Rpaks; - std::unordered_map - RpakAliases; // paks we alias to other rpaks, e.g. to load sp_crashsite paks on the map mp_crashsite - std::vector StarpakPaths; // starpaks that this mod contains - // there seems to be no nice way to get the rpak that is causing the load of a starpak? - // hashed with STR_HASH - - std::unordered_map DependencyConstants; - - public: - Mod(fs::path modPath, char* jsonBuf); -}; - -struct ModOverrideFile -{ - public: - Mod* m_pOwningMod; - fs::path m_Path; -}; - -class ModManager -{ - private: - bool m_bHasLoadedMods = false; - bool m_bHasEnabledModsCfg; - rapidjson_document m_EnabledModsCfg; - - // precalculated hashes - size_t m_hScriptsRsonHash; - size_t m_hPdefHash; - size_t m_hKBActHash; - - public: - std::vector m_LoadedMods; - std::unordered_map m_ModFiles; - std::unordered_map 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 runtime/compiled/ - void BuildScriptsRson(); - void TryBuildKeyValues(const char* filename); - void BuildPdef(); - void BuildKBActionsList(); -}; - -fs::path GetModFolderPath(); -fs::path GetCompiledAssetsPath(); - -extern ModManager* g_pModManager; diff --git a/NorthstarDLL/modpdef.cpp b/NorthstarDLL/modpdef.cpp deleted file mode 100644 index d33ba8a6..00000000 --- a/NorthstarDLL/modpdef.cpp +++ /dev/null @@ -1,119 +0,0 @@ -#include "pch.h" -#include "modmanager.h" -#include "filesystem.h" - -#include -#include -#include - -const fs::path MOD_PDEF_SUFFIX = "cfg/server/persistent_player_data_version_231.pdef"; -const char* VPK_PDEF_PATH = "cfg/server/persistent_player_data_version_231.pdef"; - -void ModManager::BuildPdef() -{ - spdlog::info("Building persistent_player_data_version_231.pdef..."); - - fs::path MOD_PDEF_PATH = fs::path(GetCompiledAssetsPath() / MOD_PDEF_SUFFIX); - - fs::remove(MOD_PDEF_PATH); - std::string pdef = R2::ReadVPKOriginalFile(VPK_PDEF_PATH); - - for (Mod& mod : m_LoadedMods) - { - if (!mod.m_bEnabled || !mod.Pdiff.size()) - continue; - - // this code probably isn't going to be pretty lol - // refer to shared/pjson.js for an actual okish parser of the pdiff format - // but pretty much, $ENUM_ADD blocks define members added to preexisting enums - // $PROP_START ends the custom stuff, and from there it's just normal props we append to the pdef - - std::map> enumAdds; - - // read pdiff - bool inEnum = false; - bool inProp = false; - std::string currentEnum; - std::string currentLine; - std::istringstream pdiffStream(mod.Pdiff); - - while (std::getline(pdiffStream, currentLine)) - { - if (inProp) - { - // just append to pdef here - pdef += currentLine; - pdef += '\n'; - continue; - } - - // trim leading whitespace - size_t start = currentLine.find_first_not_of(" \n\r\t\f\v"); - size_t end = currentLine.find("//"); - if (end == std::string::npos) - end = currentLine.size() - 1; // last char - - if (!currentLine.size() || !currentLine.compare(start, 2, "//")) - continue; - - if (inEnum) - { - if (!currentLine.compare(start, 9, "$ENUM_END")) - inEnum = false; - else - enumAdds[currentEnum].push_back(currentLine); // only need to push_back current line, if there's syntax errors then game - // pdef parser will handle them - } - else if (!currentLine.compare(start, 9, "$ENUM_ADD")) - { - inEnum = true; - currentEnum = currentLine.substr(start + 10 /*$ENUM_ADD + 1*/, currentLine.size() - end - (start + 10)); - enumAdds.insert(std::make_pair(currentEnum, std::vector())); - } - else if (!currentLine.compare(start, 11, "$PROP_START")) - { - inProp = true; - pdef += "\n// $PROP_START "; - pdef += mod.Name; - pdef += "\n"; - } - } - - // add new members to preexisting enums - // note: this code could 100% be messed up if people put //$ENUM_START comments and the like - // could make it protect against this, but honestly not worth atm - for (auto enumAdd : enumAdds) - { - std::string addStr; - for (std::string enumMember : enumAdd.second) - { - addStr += enumMember; - addStr += '\n'; - } - - // start of enum we're adding to - std::string startStr = "$ENUM_START "; - startStr += enumAdd.first; - - // insert enum values into enum - size_t insertIdx = pdef.find("$ENUM_END", pdef.find(startStr)); - pdef.reserve(addStr.size()); - pdef.insert(insertIdx, addStr); - } - } - - fs::create_directories(MOD_PDEF_PATH.parent_path()); - - std::ofstream writeStream(MOD_PDEF_PATH, std::ios::binary); - writeStream << pdef; - writeStream.close(); - - ModOverrideFile overrideFile; - 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)); - else - m_ModFiles[VPK_PDEF_PATH] = overrideFile; -} diff --git a/NorthstarDLL/mods/compiled/kb_act.cpp b/NorthstarDLL/mods/compiled/kb_act.cpp new file mode 100644 index 00000000..4a011dc7 --- /dev/null +++ b/NorthstarDLL/mods/compiled/kb_act.cpp @@ -0,0 +1,45 @@ +#include "pch.h" +#include "mods/modmanager.h" +#include "core/filesystem/filesystem.h" + +#include + +const char* KB_ACT_PATH = "scripts\\kb_act.lst"; + +// compiles the file kb_act.lst, that defines entries for keybindings in the options menu +void ModManager::BuildKBActionsList() +{ + spdlog::info("Building kb_act.lst"); + + fs::create_directories(GetCompiledAssetsPath() / "scripts"); + std::ofstream soCompiledKeys(GetCompiledAssetsPath() / KB_ACT_PATH, std::ios::binary); + + // write vanilla file's content to compiled file + soCompiledKeys << R2::ReadVPKOriginalFile(KB_ACT_PATH); + + for (Mod& mod : m_LoadedMods) + { + if (!mod.m_bEnabled) + continue; + + // write content of each modded file to compiled file + std::ifstream siModKeys(mod.m_ModDirectory / "kb_act.lst"); + + if (siModKeys.good()) + soCompiledKeys << siModKeys.rdbuf() << std::endl; + + siModKeys.close(); + } + + soCompiledKeys.close(); + + // push to overrides + ModOverrideFile overrideFile; + overrideFile.m_pOwningMod = nullptr; + overrideFile.m_Path = KB_ACT_PATH; + + if (m_ModFiles.find(KB_ACT_PATH) == m_ModFiles.end()) + m_ModFiles.insert(std::make_pair(KB_ACT_PATH, overrideFile)); + else + m_ModFiles[KB_ACT_PATH] = overrideFile; +} diff --git a/NorthstarDLL/mods/compiled/modkeyvalues.cpp b/NorthstarDLL/mods/compiled/modkeyvalues.cpp new file mode 100644 index 00000000..774be0eb --- /dev/null +++ b/NorthstarDLL/mods/compiled/modkeyvalues.cpp @@ -0,0 +1,107 @@ +#include "pch.h" +#include "mods/modmanager.h" +#include "core/filesystem/filesystem.h" + +#include + +AUTOHOOK_INIT() + +void ModManager::TryBuildKeyValues(const char* filename) +{ + spdlog::info("Building KeyValues for file {}", filename); + + std::string normalisedPath = g_pModManager->NormaliseModFilePath(fs::path(filename)); + fs::path compiledPath = GetCompiledAssetsPath() / filename; + fs::path compiledDir = compiledPath.parent_path(); + fs::create_directories(compiledDir); + + fs::path kvPath(filename); + std::string ogFilePath = "mod_original_"; + ogFilePath += kvPath.filename().string(); + + std::string newKvs = "// AUTOGENERATED: MOD PATCH KV\n"; + + int patchNum = 0; + + // 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--) + { + 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()) + { + // should result in smth along the lines of #include "mod_patch_5_mp_weapon_car.txt" + + std::string patchFilePath = "mod_patch_"; + patchFilePath += std::to_string(patchNum++); + patchFilePath += "_"; + patchFilePath += kvPath.filename().string(); + + newKvs += "#base \""; + newKvs += patchFilePath; + newKvs += "\"\n"; + + fs::remove(compiledDir / patchFilePath); + + fs::copy_file(m_LoadedMods[i].m_ModDirectory / "keyvalues" / filename, compiledDir / patchFilePath); + } + } + + // add original #base last, #bases don't override preexisting keys, including the ones we've just done + newKvs += "#base \""; + newKvs += ogFilePath; + newKvs += "\"\n"; + + // load original file, so we can parse out the name of the root obj (e.g. WeaponData for weapons) + std::string originalFile = R2::ReadVPKOriginalFile(filename); + + if (!originalFile.length()) + { + spdlog::warn("Tried to patch kv {} but no base kv was found!", ogFilePath); + return; + } + + char rootName[64]; + memset(rootName, 0, sizeof(rootName)); + + // iterate until we hit an ascii char that isn't in a # command or comment to get root obj name + int i = 0; + while (!(originalFile[i] >= 65 && originalFile[i] <= 122)) + { + // if we hit a comment or # thing, iterate until end of line + if (originalFile[i] == '/' || originalFile[i] == '#') + while (originalFile[i] != '\n') + i++; + + i++; + } + + int j = 0; + for (int j = 0; originalFile[i] >= 65 && originalFile[i] <= 122; j++) + rootName[j] = originalFile[i++]; + + // empty kv, all the other stuff gets #base'd + newKvs += rootName; + newKvs += "\n{\n}\n"; + + std::ofstream originalFileWriteStream(compiledDir / ogFilePath, std::ios::binary); + originalFileWriteStream << originalFile; + originalFileWriteStream.close(); + + std::ofstream writeStream(compiledPath, std::ios::binary); + writeStream << newKvs; + writeStream.close(); + + ModOverrideFile overrideFile; + overrideFile.m_pOwningMod = nullptr; + overrideFile.m_Path = normalisedPath; + + if (m_ModFiles.find(normalisedPath) == m_ModFiles.end()) + m_ModFiles.insert(std::make_pair(normalisedPath, overrideFile)); + else + m_ModFiles[normalisedPath] = overrideFile; +} diff --git a/NorthstarDLL/mods/compiled/modpdef.cpp b/NorthstarDLL/mods/compiled/modpdef.cpp new file mode 100644 index 00000000..219c744b --- /dev/null +++ b/NorthstarDLL/mods/compiled/modpdef.cpp @@ -0,0 +1,119 @@ +#include "pch.h" +#include "mods/modmanager.h" +#include "core/filesystem/filesystem.h" + +#include +#include +#include + +const fs::path MOD_PDEF_SUFFIX = "cfg/server/persistent_player_data_version_231.pdef"; +const char* VPK_PDEF_PATH = "cfg/server/persistent_player_data_version_231.pdef"; + +void ModManager::BuildPdef() +{ + spdlog::info("Building persistent_player_data_version_231.pdef..."); + + fs::path MOD_PDEF_PATH = fs::path(GetCompiledAssetsPath() / MOD_PDEF_SUFFIX); + + fs::remove(MOD_PDEF_PATH); + std::string pdef = R2::ReadVPKOriginalFile(VPK_PDEF_PATH); + + for (Mod& mod : m_LoadedMods) + { + if (!mod.m_bEnabled || !mod.Pdiff.size()) + continue; + + // this code probably isn't going to be pretty lol + // refer to shared/pjson.js for an actual okish parser of the pdiff format + // but pretty much, $ENUM_ADD blocks define members added to preexisting enums + // $PROP_START ends the custom stuff, and from there it's just normal props we append to the pdef + + std::map> enumAdds; + + // read pdiff + bool inEnum = false; + bool inProp = false; + std::string currentEnum; + std::string currentLine; + std::istringstream pdiffStream(mod.Pdiff); + + while (std::getline(pdiffStream, currentLine)) + { + if (inProp) + { + // just append to pdef here + pdef += currentLine; + pdef += '\n'; + continue; + } + + // trim leading whitespace + size_t start = currentLine.find_first_not_of(" \n\r\t\f\v"); + size_t end = currentLine.find("//"); + if (end == std::string::npos) + end = currentLine.size() - 1; // last char + + if (!currentLine.size() || !currentLine.compare(start, 2, "//")) + continue; + + if (inEnum) + { + if (!currentLine.compare(start, 9, "$ENUM_END")) + inEnum = false; + else + enumAdds[currentEnum].push_back(currentLine); // only need to push_back current line, if there's syntax errors then game + // pdef parser will handle them + } + else if (!currentLine.compare(start, 9, "$ENUM_ADD")) + { + inEnum = true; + currentEnum = currentLine.substr(start + 10 /*$ENUM_ADD + 1*/, currentLine.size() - end - (start + 10)); + enumAdds.insert(std::make_pair(currentEnum, std::vector())); + } + else if (!currentLine.compare(start, 11, "$PROP_START")) + { + inProp = true; + pdef += "\n// $PROP_START "; + pdef += mod.Name; + pdef += "\n"; + } + } + + // add new members to preexisting enums + // note: this code could 100% be messed up if people put //$ENUM_START comments and the like + // could make it protect against this, but honestly not worth atm + for (auto enumAdd : enumAdds) + { + std::string addStr; + for (std::string enumMember : enumAdd.second) + { + addStr += enumMember; + addStr += '\n'; + } + + // start of enum we're adding to + std::string startStr = "$ENUM_START "; + startStr += enumAdd.first; + + // insert enum values into enum + size_t insertIdx = pdef.find("$ENUM_END", pdef.find(startStr)); + pdef.reserve(addStr.size()); + pdef.insert(insertIdx, addStr); + } + } + + fs::create_directories(MOD_PDEF_PATH.parent_path()); + + std::ofstream writeStream(MOD_PDEF_PATH, std::ios::binary); + writeStream << pdef; + writeStream.close(); + + ModOverrideFile overrideFile; + 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)); + else + m_ModFiles[VPK_PDEF_PATH] = overrideFile; +} diff --git a/NorthstarDLL/mods/compiled/modscriptsrson.cpp b/NorthstarDLL/mods/compiled/modscriptsrson.cpp new file mode 100644 index 00000000..15fcdd13 --- /dev/null +++ b/NorthstarDLL/mods/compiled/modscriptsrson.cpp @@ -0,0 +1,66 @@ +#include "pch.h" +#include "mods/modmanager.h" +#include "core/filesystem/filesystem.h" +#include "squirrel/squirrel.h" + +#include + +const std::string MOD_SCRIPTS_RSON_SUFFIX = "scripts/vscripts/scripts.rson"; +const char* VPK_SCRIPTS_RSON_PATH = "scripts\\vscripts\\scripts.rson"; + +void ModManager::BuildScriptsRson() +{ + spdlog::info("Building custom scripts.rson"); + fs::path MOD_SCRIPTS_RSON_PATH = fs::path(GetCompiledAssetsPath() / MOD_SCRIPTS_RSON_SUFFIX); + fs::remove(MOD_SCRIPTS_RSON_PATH); + + std::string scriptsRson = R2::ReadVPKOriginalFile(VPK_SCRIPTS_RSON_PATH); + scriptsRson += "\n\n// START MODDED SCRIPT CONTENT\n\n"; // newline before we start custom stuff + + for (Mod& mod : m_LoadedMods) + { + if (!mod.m_bEnabled) + continue; + + // this isn't needed at all, just nice to have imo + scriptsRson += "// MOD: "; + scriptsRson += mod.Name; + scriptsRson += ":\n\n"; + + for (ModScript& script : mod.Scripts) + { + /* should create something with this format for each script + When: "CONTEXT" + Scripts: + [ + _coolscript.gnut + ]*/ + + scriptsRson += "When: \""; + scriptsRson += script.RunOn; + scriptsRson += "\"\n"; + + scriptsRson += "Scripts:\n[\n\t"; + scriptsRson += script.Path; + scriptsRson += "\n]\n\n"; + } + } + + fs::create_directories(MOD_SCRIPTS_RSON_PATH.parent_path()); + + std::ofstream writeStream(MOD_SCRIPTS_RSON_PATH, std::ios::binary); + writeStream << scriptsRson; + writeStream.close(); + + ModOverrideFile overrideFile; + overrideFile.m_pOwningMod = nullptr; + overrideFile.m_Path = VPK_SCRIPTS_RSON_PATH; + + if (m_ModFiles.find(VPK_SCRIPTS_RSON_PATH) == m_ModFiles.end()) + m_ModFiles.insert(std::make_pair(VPK_SCRIPTS_RSON_PATH, overrideFile)); + else + m_ModFiles[VPK_SCRIPTS_RSON_PATH] = overrideFile; + + // todo: for preventing dupe scripts in scripts.rson, we could actually parse when conditions with the squirrel vm, just need a way to + // get a result out of squirrelmanager.ExecuteCode this would probably be the best way to do this, imo +} diff --git a/NorthstarDLL/mods/modmanager.cpp b/NorthstarDLL/mods/modmanager.cpp new file mode 100644 index 00000000..2196b118 --- /dev/null +++ b/NorthstarDLL/mods/modmanager.cpp @@ -0,0 +1,725 @@ +#include "pch.h" +#include "modmanager.h" +#include "core/convar/convar.h" +#include "core/convar/concommand.h" +#include "client/audio.h" +#include "masterserver/masterserver.h" +#include "core/filesystem/filesystem.h" +#include "core/filesystem/rpakfilesystem.h" +#include "config/profile.h" + +#include "rapidjson/error/en.h" +#include "rapidjson/document.h" +#include "rapidjson/ostreamwrapper.h" +#include "rapidjson/writer.h" +#include +#include +#include +#include +#include + +ModManager* g_pModManager; + +Mod::Mod(fs::path modDir, char* jsonBuf) +{ + m_bWasReadSuccessfully = false; + + m_ModDirectory = modDir; + + rapidjson_document modJson; + modJson.Parse(jsonBuf); + + // fail if parse error + if (modJson.HasParseError()) + { + spdlog::error( + "Failed reading mod file {}: encountered parse error \"{}\" at offset {}", + (modDir / "mod.json").string(), + GetParseError_En(modJson.GetParseError()), + modJson.GetErrorOffset()); + return; + } + + // fail if it's not a json obj (could be an array, string, etc) + if (!modJson.IsObject()) + { + spdlog::error("Failed reading mod file {}: file is not a JSON object", (modDir / "mod.json").string()); + return; + } + + // basic mod info + // name is required + if (!modJson.HasMember("Name")) + { + spdlog::error("Failed reading mod file {}: missing required member \"Name\"", (modDir / "mod.json").string()); + return; + } + + Name = modJson["Name"].GetString(); + + if (modJson.HasMember("Description")) + Description = modJson["Description"].GetString(); + else + Description = ""; + + if (modJson.HasMember("Version")) + Version = modJson["Version"].GetString(); + else + { + Version = "0.0.0"; + spdlog::warn("Mod file {} is missing a version, consider adding a version", (modDir / "mod.json").string()); + } + + if (modJson.HasMember("DownloadLink")) + DownloadLink = modJson["DownloadLink"].GetString(); + else + DownloadLink = ""; + + if (modJson.HasMember("RequiredOnClient")) + RequiredOnClient = modJson["RequiredOnClient"].GetBool(); + else + RequiredOnClient = false; + + if (modJson.HasMember("LoadPriority")) + LoadPriority = modJson["LoadPriority"].GetInt(); + else + { + spdlog::info("Mod file {} is missing a LoadPriority, consider adding one", (modDir / "mod.json").string()); + LoadPriority = 0; + } + + // mod convars + if (modJson.HasMember("ConVars") && modJson["ConVars"].IsArray()) + { + for (auto& convarObj : modJson["ConVars"].GetArray()) + { + if (!convarObj.IsObject() || !convarObj.HasMember("Name") || !convarObj.HasMember("DefaultValue")) + continue; + + // have to allocate this manually, otherwise convar registration will break + // unfortunately this causes us to leak memory on reload, unsure of a way around this rn + ModConVar* convar = new ModConVar; + convar->Name = convarObj["Name"].GetString(); + convar->DefaultValue = convarObj["DefaultValue"].GetString(); + + if (convarObj.HasMember("HelpString")) + convar->HelpString = convarObj["HelpString"].GetString(); + else + convar->HelpString = ""; + + convar->Flags = FCVAR_NONE; + + if (convarObj.HasMember("Flags")) + { + // 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); + } + } + + // mod scripts + if (modJson.HasMember("Scripts") && modJson["Scripts"].IsArray()) + { + for (auto& scriptObj : modJson["Scripts"].GetArray()) + { + if (!scriptObj.IsObject() || !scriptObj.HasMember("Path") || !scriptObj.HasMember("RunOn")) + continue; + + ModScript script; + + script.Path = scriptObj["Path"].GetString(); + script.RunOn = scriptObj["RunOn"].GetString(); + + if (scriptObj.HasMember("ServerCallback") && scriptObj["ServerCallback"].IsObject()) + { + ModScriptCallback callback; + callback.Context = ScriptContext::SERVER; + + if (scriptObj["ServerCallback"].HasMember("Before") && scriptObj["ServerCallback"]["Before"].IsString()) + callback.BeforeCallback = scriptObj["ServerCallback"]["Before"].GetString(); + + if (scriptObj["ServerCallback"].HasMember("After") && scriptObj["ServerCallback"]["After"].IsString()) + callback.AfterCallback = scriptObj["ServerCallback"]["After"].GetString(); + + script.Callbacks.push_back(callback); + } + + if (scriptObj.HasMember("ClientCallback") && scriptObj["ClientCallback"].IsObject()) + { + ModScriptCallback callback; + callback.Context = ScriptContext::CLIENT; + + if (scriptObj["ClientCallback"].HasMember("Before") && scriptObj["ClientCallback"]["Before"].IsString()) + callback.BeforeCallback = scriptObj["ClientCallback"]["Before"].GetString(); + + if (scriptObj["ClientCallback"].HasMember("After") && scriptObj["ClientCallback"]["After"].IsString()) + callback.AfterCallback = scriptObj["ClientCallback"]["After"].GetString(); + + script.Callbacks.push_back(callback); + } + + if (scriptObj.HasMember("UICallback") && scriptObj["UICallback"].IsObject()) + { + ModScriptCallback callback; + callback.Context = ScriptContext::UI; + + if (scriptObj["UICallback"].HasMember("Before") && scriptObj["UICallback"]["Before"].IsString()) + callback.BeforeCallback = scriptObj["UICallback"]["Before"].GetString(); + + if (scriptObj["UICallback"].HasMember("After") && scriptObj["UICallback"]["After"].IsString()) + callback.AfterCallback = scriptObj["UICallback"]["After"].GetString(); + + script.Callbacks.push_back(callback); + } + + Scripts.push_back(script); + } + } + + if (modJson.HasMember("Localisation") && modJson["Localisation"].IsArray()) + { + for (auto& localisationStr : modJson["Localisation"].GetArray()) + { + if (!localisationStr.IsString()) + continue; + + LocalisationFiles.push_back(localisationStr.GetString()); + } + } + + if (modJson.HasMember("Dependencies") && modJson["Dependencies"].IsObject()) + { + for (auto v = modJson["Dependencies"].MemberBegin(); v != modJson["Dependencies"].MemberEnd(); v++) + { + if (!v->name.IsString() || !v->value.IsString()) + continue; + + spdlog::info("Constant {} defined by {} for mod {}", v->name.GetString(), Name, v->value.GetString()); + if (DependencyConstants.find(v->name.GetString()) != DependencyConstants.end() && + v->value.GetString() != DependencyConstants[v->name.GetString()]) + { + spdlog::error("A dependency constant with the same name already exists for another mod. Change the constant name."); + return; + } + + if (DependencyConstants.find(v->name.GetString()) == DependencyConstants.end()) + DependencyConstants.emplace(v->name.GetString(), v->value.GetString()); + } + } + + m_bWasReadSuccessfully = true; +} + +ModManager::ModManager() +{ + // precaculated string hashes + // note: use backslashes for these, since we use lexically_normal for file paths which uses them + m_hScriptsRsonHash = STR_HASH("scripts\\vscripts\\scripts.rson"); + m_hPdefHash = STR_HASH( + "cfg\\server\\persistent_player_data_version_231.pdef" // this can have multiple versions, but we use 231 so that's what we hash + ); + m_hKBActHash = STR_HASH("scripts\\kb_act.lst"); + + LoadMods(); +} + +void ModManager::LoadMods() +{ + if (m_bHasLoadedMods) + UnloadMods(); + + std::vector modDirs; + + // ensure dirs exist + fs::remove_all(GetCompiledAssetsPath()); + fs::create_directories(GetModFolderPath()); + + m_DependencyConstants.clear(); + + // read enabled mods cfg + std::ifstream enabledModsStream(GetNorthstarPrefix() + "/enabledmods.json"); + std::stringstream enabledModsStringStream; + + if (!enabledModsStream.fail()) + { + while (enabledModsStream.peek() != EOF) + enabledModsStringStream << (char)enabledModsStream.get(); + + enabledModsStream.close(); + m_EnabledModsCfg.Parse( + enabledModsStringStream.str().c_str()); + + m_bHasEnabledModsCfg = m_EnabledModsCfg.IsObject(); + } + + // get mod directories + for (fs::directory_entry dir : fs::directory_iterator(GetModFolderPath())) + if (fs::exists(dir.path() / "mod.json")) + modDirs.push_back(dir.path()); + + for (fs::path modDir : modDirs) + { + // read mod json file + std::ifstream jsonStream(modDir / "mod.json"); + std::stringstream jsonStringStream; + + // fail if no mod json + if (jsonStream.fail()) + { + spdlog::warn("Mod {} has a directory but no mod.json", modDir.string()); + continue; + } + + while (jsonStream.peek() != EOF) + jsonStringStream << (char)jsonStream.get(); + + jsonStream.close(); + + Mod mod(modDir, (char*)jsonStringStream.str().c_str()); + + for (auto& pair : mod.DependencyConstants) + { + 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.m_bWasReadSuccessfully = false; + break; + } + if (m_DependencyConstants.find(pair.first) == m_DependencyConstants.end()) + m_DependencyConstants.emplace(pair); + } + + if (m_bHasEnabledModsCfg && m_EnabledModsCfg.HasMember(mod.Name.c_str())) + mod.m_bEnabled = m_EnabledModsCfg[mod.Name.c_str()].IsTrue(); + else + mod.m_bEnabled = true; + + if (mod.m_bWasReadSuccessfully) + { + spdlog::info("Loaded mod {} successfully", mod.Name); + if (mod.m_bEnabled) + spdlog::info("Mod {} is enabled", mod.Name); + else + spdlog::info("Mod {} is disabled", mod.Name); + + 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; }); + + for (Mod& mod : m_LoadedMods) + { + if (!mod.m_bEnabled) + continue; + + // register convars + // for reloads, this is sorta barebones, when we have a good findconvar method, we could probably reset flags and stuff on + // 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 (!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.m_ModDirectory / "vpk")) + { + // read vpk cfg + std::ifstream vpkJsonStream(mod.m_ModDirectory / "vpk/vpk.json"); + std::stringstream vpkJsonStringStream; + + bool bUseVPKJson = false; + rapidjson::Document dVpkJson; + + if (!vpkJsonStream.fail()) + { + while (vpkJsonStream.peek() != EOF) + vpkJsonStringStream << (char)vpkJsonStream.get(); + + vpkJsonStream.close(); + dVpkJson.Parse( + vpkJsonStringStream.str().c_str()); + + bUseVPKJson = !dVpkJson.HasParseError() && dVpkJson.IsObject(); + } + + 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 + if (fs::is_regular_file(file) && file.path().extension() == ".vpk" && + file.path().string().find("english") != std::string::npos && + file.path().string().find(".bsp.pak000_dir") != std::string::npos) + { + std::string formattedPath = file.path().filename().string(); + + // this really fucking sucks but it'll work + 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 = (file.path().parent_path() / vpkName).string(); + + 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.m_ModDirectory / "paks")) + { + // read rpak cfg + std::ifstream rpakJsonStream(mod.m_ModDirectory / "paks/rpak.json"); + std::stringstream rpakJsonStringStream; + + bool bUseRpakJson = false; + rapidjson::Document dRpakJson; + + if (!rpakJsonStream.fail()) + { + while (rpakJsonStream.peek() != EOF) + rpakJsonStringStream << (char)rpakJsonStream.get(); + + rpakJsonStream.close(); + dRpakJson.Parse( + rpakJsonStringStream.str().c_str()); + + bUseRpakJson = !dRpakJson.HasParseError() && dRpakJson.IsObject(); + } + + // read pak aliases + if (bUseRpakJson && dRpakJson.HasMember("Aliases") && dRpakJson["Aliases"].IsObject()) + { + for (rapidjson::Value::ConstMemberIterator iterator = dRpakJson["Aliases"].MemberBegin(); + iterator != dRpakJson["Aliases"].MemberEnd(); + iterator++) + { + if (!iterator->name.IsString() || !iterator->value.IsString()) + continue; + + mod.RpakAliases.insert(std::make_pair(iterator->name.GetString(), iterator->value.GetString())); + } + } + + 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") + { + std::string pakName(file.path().filename().string()); + + ModRpakEntry& modPak = mod.Rpaks.emplace_back(); + 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; + + // read header of file and get the starpak paths + // this is done here as opposed to on starpak load because multiple rpaks can load a starpak + // and there is seemingly no good way to tell which rpak is causing the load of a starpak :/ + + std::ifstream rpakStream(file.path(), std::ios::binary); + + // seek to the point in the header where the starpak reference size is + rpakStream.seekg(0x38, std::ios::beg); + int starpaksSize = 0; + rpakStream.read((char*)&starpaksSize, 2); + + // seek to just after the header + rpakStream.seekg(0x58, std::ios::beg); + // read the starpak reference(s) + std::vector buf(starpaksSize); + rpakStream.read(buf.data(), starpaksSize); + + rpakStream.close(); + + // split the starpak reference(s) into strings to hash + std::string str = ""; + for (int i = 0; i < starpaksSize; i++) + { + // if the current char is null, that signals the end of the current starpak path + if (buf[i] != 0x00) + { + str += buf[i]; + } + else + { + // only add the string we are making if it isnt empty + if (!str.empty()) + { + mod.StarpakPaths.push_back(STR_HASH(str)); + spdlog::info("Mod {} registered starpak '{}'", mod.Name, str); + str = ""; + } + } + } + + // not using atm because we need to resolve path to rpak + // if (m_hasLoadedMods && modPak.m_bAutoLoad) + // g_pPakLoadManager->LoadPakAsync(pakName.c_str()); + } + } + } + + // read keyvalues paths + if (fs::exists(mod.m_ModDirectory / "keyvalues")) + { + for (fs::directory_entry file : fs::recursive_directory_iterator(mod.m_ModDirectory / "keyvalues")) + { + if (fs::is_regular_file(file)) + { + 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.m_ModDirectory / "mod.pdiff")) + { + std::ifstream pdiffStream(mod.m_ModDirectory / "mod.pdiff"); + + if (!pdiffStream.fail()) + { + std::stringstream pdiffStringStream; + while (pdiffStream.peek() != EOF) + pdiffStringStream << (char)pdiffStream.get(); + + pdiffStream.close(); + + mod.Pdiff = pdiffStringStream.str(); + } + } + + // read bink video paths + if (fs::exists(mod.m_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.m_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") + { + if (!g_CustomAudioManager.TryLoadAudioOverride(file.path())) + { + spdlog::warn("Mod {} has an invalid audio def {}", mod.Name, file.path().filename().string()); + continue; + } + } + } + } + } + + // 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--) + { + if (!m_LoadedMods[i].m_bEnabled) + continue; + + if (fs::exists(m_LoadedMods[i].m_ModDirectory / MOD_OVERRIDE_DIR)) + { + for (fs::directory_entry file : fs::recursive_directory_iterator(m_LoadedMods[i].m_ModDirectory / MOD_OVERRIDE_DIR)) + { + 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.m_pOwningMod = &m_LoadedMods[i]; + modFile.m_Path = path; + m_ModFiles.insert(std::make_pair(path, modFile)); + } + } + } + } + + // build modinfo obj for masterserver + rapidjson_document modinfoDoc; + auto& alloc = modinfoDoc.GetAllocator(); + modinfoDoc.SetObject(); + modinfoDoc.AddMember("Mods", rapidjson::kArrayType, alloc); + + int currentModIndex = 0; + for (Mod& mod : m_LoadedMods) + { + if (!mod.m_bEnabled || (!mod.RequiredOnClient && !mod.Pdiff.size())) + continue; + + modinfoDoc["Mods"].PushBack(rapidjson::kObjectType, modinfoDoc.GetAllocator()); + modinfoDoc["Mods"][currentModIndex].AddMember("Name", rapidjson::StringRef(&mod.Name[0]), modinfoDoc.GetAllocator()); + modinfoDoc["Mods"][currentModIndex].AddMember("Version", rapidjson::StringRef(&mod.Version[0]), modinfoDoc.GetAllocator()); + modinfoDoc["Mods"][currentModIndex].AddMember("RequiredOnClient", mod.RequiredOnClient, modinfoDoc.GetAllocator()); + modinfoDoc["Mods"][currentModIndex].AddMember("Pdiff", rapidjson::StringRef(&mod.Pdiff[0]), modinfoDoc.GetAllocator()); + + currentModIndex++; + } + + rapidjson::StringBuffer buffer; + buffer.Clear(); + rapidjson::Writer writer(buffer); + modinfoDoc.Accept(writer); + g_pMasterServerManager->m_sOwnModInfoJson = std::string(buffer.GetString()); + + m_bHasLoadedMods = true; +} + +void ModManager::UnloadMods() +{ + // clean up stuff from mods before we unload + m_ModFiles.clear(); + fs::remove_all(GetCompiledAssetsPath()); + + g_CustomAudioManager.ClearAudioOverrides(); + + if (!m_bHasEnabledModsCfg) + m_EnabledModsCfg.SetObject(); + + for (Mod& mod : m_LoadedMods) + { + // remove all built kvs + for (std::pair kvPaths : mod.KeyValues) + fs::remove(GetCompiledAssetsPath() / fs::path(kvPaths.second).lexically_relative(mod.m_ModDirectory)); + + mod.KeyValues.clear(); + + // write to m_enabledModsCfg + // 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(rapidjson_document::StringRefType(mod.Name.c_str()), false, m_EnabledModsCfg.GetAllocator()); + + m_EnabledModsCfg[mod.Name.c_str()].SetBool(mod.m_bEnabled); + } + + std::ofstream writeStream(GetNorthstarPrefix() + "/enabledmods.json"); + rapidjson::OStreamWrapper writeStreamWrapper(writeStream); + rapidjson::Writer writer(writeStreamWrapper); + m_EnabledModsCfg.Accept(writer); + + // do we need to dealloc individual entries in m_loadedMods? idk, rework + 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(NormaliseModFilePath(fs::path(filename))); + + if (fileHash == m_hScriptsRsonHash) + BuildScriptsRson(); + else if (fileHash == m_hPdefHash) + BuildPdef(); + else if (fileHash == m_hKBActHash) + BuildKBActionsList(); + 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) + { + if (!mod.m_bEnabled) + continue; + + if (mod.KeyValues.find(fileHash) != mod.KeyValues.end()) + { + TryBuildKeyValues(filename); + return; + } + } + } +} + +void ConCommand_reload_mods(const CCommand& args) +{ + g_pModManager->LoadMods(); +} + +fs::path GetModFolderPath() +{ + return fs::path(GetNorthstarPrefix() + MOD_FOLDER_SUFFIX); +} +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); +} diff --git a/NorthstarDLL/mods/modmanager.h b/NorthstarDLL/mods/modmanager.h new file mode 100644 index 00000000..a77d85bd --- /dev/null +++ b/NorthstarDLL/mods/modmanager.h @@ -0,0 +1,154 @@ +#pragma once +#include "core/convar/convar.h" +#include "core/memalloc.h" +#include "squirrel/squirrel.h" + +#include "rapidjson/document.h" +#include +#include +#include + +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"; + +struct ModConVar +{ + public: + std::string Name; + std::string DefaultValue; + std::string HelpString; + int Flags; +}; + +struct ModScriptCallback +{ + public: + ScriptContext Context; + + // called before the codecallback is executed + std::string BeforeCallback; + // called after the codecallback has finished executing + std::string AfterCallback; +}; + +struct ModScript +{ + public: + std::string Path; + std::string RunOn; + + std::vector Callbacks; +}; + +// these are pretty much identical, could refactor to use the same stuff? +struct ModVPKEntry +{ + public: + bool m_bAutoLoad; + std::string m_sVpkPath; +}; + +struct ModRpakEntry +{ + public: + bool m_bAutoLoad; + std::string m_sPakName; + std::string m_sLoadAfterPak; +}; + +class Mod +{ + public: + // runtime stuff + bool m_bEnabled = true; + bool m_bWasReadSuccessfully = false; + fs::path m_ModDirectory; + // bool m_bIsRemote; + + // mod.json stuff: + + // the mod's name + std::string Name; + // the mod's description + std::string Description; + // the mod's version, should be in semver + std::string Version; + // a download link to the mod, for clients that try to join without the mod + std::string DownloadLink; + + // whether clients need the mod to join servers running this mod + bool RequiredOnClient; + // the priority for this mod's files, mods with prio 0 are loaded first, then 1, then 2, etc + int LoadPriority; + + // custom scripts used by the mod + std::vector Scripts; + // convars created by the mod + std::vector ConVars; + // custom localisation files created by the mod + std::vector LocalisationFiles; + + // other files: + + std::vector Vpks; + std::unordered_map KeyValues; + std::vector BinkVideos; + std::string Pdiff; // only need one per mod + + std::vector Rpaks; + std::unordered_map + RpakAliases; // paks we alias to other rpaks, e.g. to load sp_crashsite paks on the map mp_crashsite + std::vector StarpakPaths; // starpaks that this mod contains + // there seems to be no nice way to get the rpak that is causing the load of a starpak? + // hashed with STR_HASH + + std::unordered_map DependencyConstants; + + public: + Mod(fs::path modPath, char* jsonBuf); +}; + +struct ModOverrideFile +{ + public: + Mod* m_pOwningMod; + fs::path m_Path; +}; + +class ModManager +{ + private: + bool m_bHasLoadedMods = false; + bool m_bHasEnabledModsCfg; + rapidjson_document m_EnabledModsCfg; + + // precalculated hashes + size_t m_hScriptsRsonHash; + size_t m_hPdefHash; + size_t m_hKBActHash; + + public: + std::vector m_LoadedMods; + std::unordered_map m_ModFiles; + std::unordered_map 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 runtime/compiled/ + void BuildScriptsRson(); + void TryBuildKeyValues(const char* filename); + void BuildPdef(); + void BuildKBActionsList(); +}; + +fs::path GetModFolderPath(); +fs::path GetCompiledAssetsPath(); + +extern ModManager* g_pModManager; diff --git a/NorthstarDLL/modscriptsrson.cpp b/NorthstarDLL/modscriptsrson.cpp deleted file mode 100644 index 3615dd80..00000000 --- a/NorthstarDLL/modscriptsrson.cpp +++ /dev/null @@ -1,66 +0,0 @@ -#include "pch.h" -#include "modmanager.h" -#include "filesystem.h" -#include "squirrel.h" - -#include - -const std::string MOD_SCRIPTS_RSON_SUFFIX = "scripts/vscripts/scripts.rson"; -const char* VPK_SCRIPTS_RSON_PATH = "scripts\\vscripts\\scripts.rson"; - -void ModManager::BuildScriptsRson() -{ - spdlog::info("Building custom scripts.rson"); - fs::path MOD_SCRIPTS_RSON_PATH = fs::path(GetCompiledAssetsPath() / MOD_SCRIPTS_RSON_SUFFIX); - fs::remove(MOD_SCRIPTS_RSON_PATH); - - std::string scriptsRson = R2::ReadVPKOriginalFile(VPK_SCRIPTS_RSON_PATH); - scriptsRson += "\n\n// START MODDED SCRIPT CONTENT\n\n"; // newline before we start custom stuff - - for (Mod& mod : m_LoadedMods) - { - if (!mod.m_bEnabled) - continue; - - // this isn't needed at all, just nice to have imo - scriptsRson += "// MOD: "; - scriptsRson += mod.Name; - scriptsRson += ":\n\n"; - - for (ModScript& script : mod.Scripts) - { - /* should create something with this format for each script - When: "CONTEXT" - Scripts: - [ - _coolscript.gnut - ]*/ - - scriptsRson += "When: \""; - scriptsRson += script.RunOn; - scriptsRson += "\"\n"; - - scriptsRson += "Scripts:\n[\n\t"; - scriptsRson += script.Path; - scriptsRson += "\n]\n\n"; - } - } - - fs::create_directories(MOD_SCRIPTS_RSON_PATH.parent_path()); - - std::ofstream writeStream(MOD_SCRIPTS_RSON_PATH, std::ios::binary); - writeStream << scriptsRson; - writeStream.close(); - - ModOverrideFile overrideFile; - overrideFile.m_pOwningMod = nullptr; - overrideFile.m_Path = VPK_SCRIPTS_RSON_PATH; - - if (m_ModFiles.find(VPK_SCRIPTS_RSON_PATH) == m_ModFiles.end()) - m_ModFiles.insert(std::make_pair(VPK_SCRIPTS_RSON_PATH, overrideFile)); - else - m_ModFiles[VPK_SCRIPTS_RSON_PATH] = overrideFile; - - // todo: for preventing dupe scripts in scripts.rson, we could actually parse when conditions with the squirrel vm, just need a way to - // get a result out of squirrelmanager.ExecuteCode this would probably be the best way to do this, imo -} diff --git a/NorthstarDLL/nsprefix.cpp b/NorthstarDLL/nsprefix.cpp deleted file mode 100644 index 8057865e..00000000 --- a/NorthstarDLL/nsprefix.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "pch.h" -#include "nsprefix.h" -#include "dedicated.h" -#include - -std::string GetNorthstarPrefix() -{ - return NORTHSTAR_FOLDER_PREFIX; -} - -void InitialiseNorthstarPrefix() -{ - char* clachar = strstr(GetCommandLineA(), "-profile="); - if (clachar) - { - std::string cla = std::string(clachar); - if (strncmp(cla.substr(9, 1).c_str(), "\"", 1)) - { - int space = cla.find(" "); - std::string dirname = cla.substr(9, space - 9); - spdlog::info("Found profile in command line arguments: " + dirname); - NORTHSTAR_FOLDER_PREFIX = dirname; - } - else - { - std::string quote = "\""; - int quote1 = cla.find(quote); - int quote2 = (cla.substr(quote1 + 1)).find(quote); - std::string dirname = cla.substr(quote1 + 1, quote2); - spdlog::info("Found profile in command line arguments: " + dirname); - NORTHSTAR_FOLDER_PREFIX = dirname; - } - } - else - { - spdlog::info("Profile was not found in command line arguments. Using default: R2Northstar"); - NORTHSTAR_FOLDER_PREFIX = "R2Northstar"; - } - - // set the console title to show the current profile - // dont do this on dedi as title contains useful information on dedi and setting title breaks it as well - if (!IsDedicatedServer()) - SetConsoleTitleA((std::string("NorthstarLauncher | ") + NORTHSTAR_FOLDER_PREFIX).c_str()); -} diff --git a/NorthstarDLL/nsprefix.h b/NorthstarDLL/nsprefix.h deleted file mode 100644 index 9f3087fb..00000000 --- a/NorthstarDLL/nsprefix.h +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once -#include - -static std::string NORTHSTAR_FOLDER_PREFIX; - -void InitialiseNorthstarPrefix(); -std::string GetNorthstarPrefix(); diff --git a/NorthstarDLL/pch.h b/NorthstarDLL/pch.h index 824aaee8..a6a3f06d 100644 --- a/NorthstarDLL/pch.h +++ b/NorthstarDLL/pch.h @@ -9,7 +9,7 @@ #define RAPIDJSON_HAS_STDSTRING 1 // add headers that you want to pre-compile here -#include "memalloc.h" +#include "core/memalloc.h" #include #include @@ -20,13 +20,17 @@ namespace fs = std::filesystem; -#include "structs.h" -#include "color.h" +// clang-format off +#define assert_msg(exp, msg) assert((exp, msg)) +//clang-format on + +#include "core/structs.h" +#include "core/math/color.h" #include "spdlog/spdlog.h" -#include "logging.h" +#include "logging/logging.h" #include "MinHook.h" #include "libcurl/include/curl/curl.h" -#include "hooks.h" +#include "core/hooks.h" #include "memory.h" template ReturnType CallVFunc(int index, void* thisPtr, Args... args) diff --git a/NorthstarDLL/playlist.cpp b/NorthstarDLL/playlist.cpp deleted file mode 100644 index 4f34dbb1..00000000 --- a/NorthstarDLL/playlist.cpp +++ /dev/null @@ -1,130 +0,0 @@ -#include "pch.h" -#include "playlist.h" -#include "concommand.h" -#include "convar.h" -#include "squirrel.h" -#include "hoststate.h" -#include "serverpresence.h" - -AUTOHOOK_INIT() - -// 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; - - R2::SetCurrentPlaylist(args.Arg(1)); -} - -void ConCommand_setplaylistvaroverride(const CCommand& args) -{ - if (args.ArgC() < 3) - return; - - for (int i = 1; i < args.ArgC(); i += 2) - R2::SetPlaylistVarOverride(args.Arg(i), args.Arg(i + 1)); -} - -ON_DLL_LOAD_RELIESON("engine.dll", PlaylistHooks, (ConCommand, ConVar), (CModule module)) -{ - AUTOHOOK_DISPATCH() - - R2::GetCurrentPlaylistName = module.Offset(0x18C640).As(); - R2::SetCurrentPlaylist = module.Offset(0x18EB20).As(); - R2::SetPlaylistVarOverride = module.Offset(0x18ED00).As(); - R2::GetCurrentPlaylistVar = module.Offset(0x18C680).As(); - - // 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 - // 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"); - - // 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 - module.Offset(0x18ED8D).Patch("C3"); - - // patch to allow setplaylistvaroverride to be called before map init on dedicated and private match launched through the game - module.Offset(0x18ED17).NOP(6); -} diff --git a/NorthstarDLL/playlist.h b/NorthstarDLL/playlist.h deleted file mode 100644 index c77b37d9..00000000 --- a/NorthstarDLL/playlist.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -// 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 diff --git a/NorthstarDLL/plugin_abi.h b/NorthstarDLL/plugin_abi.h deleted file mode 100644 index 4b176a32..00000000 --- a/NorthstarDLL/plugin_abi.h +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once -#include - -#define ABI_VERSION 1 -/// -/// This enum is used for referencing the different types of objects we can pass to a plugin -/// Anything exposed to a plugin must not a be C++ type, as they could break when compiling with a different compiler. -/// Any ABI incompatible change must increment the version number. -/// Nothing must be removed from this enum, only appended. When it absolutely necessary to deprecate an object, it should return UNSUPPORTED -/// when retrieved -/// -enum PluginObject -{ - UNSUPPORTED = 0, - GAMESTATE = 1, - SERVERINFO = 2, - PLAYERINFO = 3, - DUMMY = 0xFFFF -}; - -enum GameStateInfoType -{ - ourScore = 0, - secondHighestScore = 1, - highestScore = 2, - connected = 3, - loading = 4, - map = 5, - mapDisplayName = 6, - playlist = 7, - playlistDisplayName = 8, - players = 9 -}; -struct GameState -{ - int (*getGameStateChar)(char* out_buf, size_t out_buf_len, GameStateInfoType var); - int (*getGameStateInt)(int* out_ptr, GameStateInfoType var); - int (*getGameStateBool)(bool* out_ptr, GameStateInfoType var); -}; - -enum ServerInfoType -{ - id = 0, - name = 1, - description = 2, - password = 3, - maxPlayers = 4, - roundBased = 5, - scoreLimit = 6, - endTime = 7 -}; -struct ServerInfo -{ - int (*getServerInfoChar)(char* out_buf, size_t out_buf_len, ServerInfoType var); - int (*getServerInfoInt)(int* out_ptr, ServerInfoType var); - int (*getServerInfoBool)(bool* out_ptr, ServerInfoType var); -}; - -enum PlayerInfoType -{ - uid = 0 -}; -struct PlayerInfo -{ - int (*getPlayerInfoChar)(char* out_buf, size_t out_buf_len, PlayerInfoType var); - int (*getPlayerInfoInt)(int* out_ptr, PlayerInfoType var); - int (*getPlayerInfoBool)(bool* out_ptr, PlayerInfoType var); -}; diff --git a/NorthstarDLL/plugins.cpp b/NorthstarDLL/plugins.cpp deleted file mode 100644 index 790439d1..00000000 --- a/NorthstarDLL/plugins.cpp +++ /dev/null @@ -1,422 +0,0 @@ -#include "pch.h" -#include "squirrel.h" -#include "plugins.h" -#include "masterserver.h" -#include "convar.h" -#include "serverpresence.h" - -#include -#include - -/// -/// The data is split into two different representations: one for internal, and one for plugins, for thread safety reasons -/// The struct exposed to plugins contains getter functions for the various data types. -/// We can safely use C++ types like std::string here since these are only ever handled by Northstar internally -/// -struct InternalGameState -{ - int ourScore; - int secondHighestScore; - int highestScore; - - bool connected; - bool loading; - std::string map; - std::string mapDisplayName; - std::string playlist; - std::string playlistDisplayName; - int players; -}; -struct InternalServerInfo -{ - std::string id; - std::string name; - std::string description; - std::string password; - int maxPlayers; - bool roundBased; - int scoreLimit; - int endTime; -}; -// TODO: need to extend this to include current player data like loadouts -struct InternalPlayerInfo -{ - int uid; -}; - -InternalGameState gameState; -InternalServerInfo serverInfo; -InternalPlayerInfo playerInfo; - -GameState gameStateExport; -ServerInfo serverInfoExport; -PlayerInfo playerInfoExport; - -/// -/// We use SRW Locks because plugins will often be running their own thread -/// To ensure thread safety, and to make it difficult to fuck up, we force them to use *our* functions to get data -/// -static SRWLOCK gameStateLock; -static SRWLOCK serverInfoLock; -static SRWLOCK playerInfoLock; - -void* getPluginObject(PluginObject var) -{ - switch (var) - { - case PluginObject::GAMESTATE: - return &gameStateExport; - case PluginObject::SERVERINFO: - return &serverInfoExport; - case PluginObject::PLAYERINFO: - return &playerInfoExport; - default: - return (void*)-1; - } -} - -void initGameState() -{ - // Initalize the Slim Reader / Writer locks - InitializeSRWLock(&gameStateLock); - InitializeSRWLock(&serverInfoLock); - InitializeSRWLock(&playerInfoLock); - - gameStateExport.getGameStateChar = &getGameStateChar; - gameStateExport.getGameStateInt = &getGameStateInt; - gameStateExport.getGameStateBool = &getGameStateBool; - - serverInfoExport.getServerInfoChar = &getServerInfoChar; - serverInfoExport.getServerInfoInt = &getServerInfoInt; - serverInfoExport.getServerInfoBool = &getServerInfoBool; - - playerInfoExport.getPlayerInfoChar = &getPlayerInfoChar; - playerInfoExport.getPlayerInfoInt = &getPlayerInfoInt; - playerInfoExport.getPlayerInfoBool = &getPlayerInfoBool; - - serverInfo.id = ""; - serverInfo.name = ""; - serverInfo.description = ""; - serverInfo.password = ""; - serverInfo.maxPlayers = 0; - gameState.connected = false; - gameState.loading = false; - gameState.map = ""; - gameState.mapDisplayName = ""; - gameState.playlist = ""; - gameState.playlistDisplayName = ""; - gameState.players = 0; - - playerInfo.uid = 123; -} - -// string gamemode, string gamemodeName, string map, string mapName, bool connected, bool loading -SQRESULT SQ_UpdateGameStateUI(HSquirrelVM* sqvm) -{ - AcquireSRWLockExclusive(&gameStateLock); - gameState.map = g_pSquirrel->getstring(sqvm, 1); - gameState.mapDisplayName = g_pSquirrel->getstring(sqvm, 2); - gameState.playlist = g_pSquirrel->getstring(sqvm, 3); - gameState.playlistDisplayName = g_pSquirrel->getstring(sqvm, 4); - gameState.connected = g_pSquirrel->getbool(sqvm, 5); - gameState.loading = g_pSquirrel->getbool(sqvm, 6); - ReleaseSRWLockExclusive(&gameStateLock); - return SQRESULT_NOTNULL; -} - -// int playerCount, int outScore, int secondHighestScore, int highestScore, bool roundBased, int scoreLimit -SQRESULT SQ_UpdateGameStateClient(HSquirrelVM* sqvm) -{ - AcquireSRWLockExclusive(&gameStateLock); - AcquireSRWLockExclusive(&serverInfoLock); - gameState.players = g_pSquirrel->getinteger(sqvm, 1); - serverInfo.maxPlayers = g_pSquirrel->getinteger(sqvm, 2); - gameState.ourScore = g_pSquirrel->getinteger(sqvm, 3); - gameState.secondHighestScore = g_pSquirrel->getinteger(sqvm, 4); - gameState.highestScore = g_pSquirrel->getinteger(sqvm, 5); - serverInfo.roundBased = g_pSquirrel->getbool(sqvm, 6); - serverInfo.scoreLimit = g_pSquirrel->getbool(sqvm, 7); - ReleaseSRWLockExclusive(&gameStateLock); - ReleaseSRWLockExclusive(&serverInfoLock); - return SQRESULT_NOTNULL; -} - -// string id, string name, string password, int players, int maxPlayers, string map, string mapDisplayName, string playlist, string -// playlistDisplayName -SQRESULT SQ_UpdateServerInfo(HSquirrelVM* sqvm) -{ - AcquireSRWLockExclusive(&gameStateLock); - AcquireSRWLockExclusive(&serverInfoLock); - serverInfo.id = g_pSquirrel->getstring(sqvm, 1); - serverInfo.name = g_pSquirrel->getstring(sqvm, 2); - serverInfo.password = g_pSquirrel->getstring(sqvm, 3); - gameState.players = g_pSquirrel->getinteger(sqvm, 4); - serverInfo.maxPlayers = g_pSquirrel->getinteger(sqvm, 5); - gameState.map = g_pSquirrel->getstring(sqvm, 6); - gameState.mapDisplayName = g_pSquirrel->getstring(sqvm, 7); - gameState.playlist = g_pSquirrel->getstring(sqvm, 8); - gameState.playlistDisplayName = g_pSquirrel->getstring(sqvm, 9); - ReleaseSRWLockExclusive(&gameStateLock); - ReleaseSRWLockExclusive(&serverInfoLock); - return SQRESULT_NOTNULL; -} - -// int maxPlayers -SQRESULT SQ_UpdateServerInfoBetweenRounds(HSquirrelVM* sqvm) -{ - AcquireSRWLockExclusive(&serverInfoLock); - serverInfo.id = g_pSquirrel->getstring(sqvm, 1); - serverInfo.name = g_pSquirrel->getstring(sqvm, 2); - serverInfo.password = g_pSquirrel->getstring(sqvm, 3); - serverInfo.maxPlayers = g_pSquirrel->getinteger(sqvm, 4); - ReleaseSRWLockExclusive(&serverInfoLock); - return SQRESULT_NOTNULL; -} - -// float timeInFuture -SQRESULT SQ_UpdateTimeInfo(HSquirrelVM* sqvm) -{ - AcquireSRWLockExclusive(&serverInfoLock); - serverInfo.endTime = ceil(g_pSquirrel->getfloat(sqvm, 1)); - ReleaseSRWLockExclusive(&serverInfoLock); - return SQRESULT_NOTNULL; -} - -// bool loading -SQRESULT SQ_SetConnected(HSquirrelVM* sqvm) -{ - AcquireSRWLockExclusive(&gameStateLock); - gameState.loading = g_pSquirrel->getbool(sqvm, 1); - ReleaseSRWLockExclusive(&gameStateLock); - return SQRESULT_NOTNULL; -} - -SQRESULT SQ_UpdateListenServer(HSquirrelVM* sqvm) -{ - AcquireSRWLockExclusive(&serverInfoLock); - serverInfo.id = g_pMasterServerManager->m_sOwnServerId; - serverInfo.password = ""; // g_pServerPresence->Cvar_ns_server_password->GetString(); todo this fr - ReleaseSRWLockExclusive(&serverInfoLock); - return SQRESULT_NOTNULL; -} - -int getServerInfoChar(char* out_buf, size_t out_buf_len, ServerInfoType var) -{ - AcquireSRWLockShared(&serverInfoLock); - int n = 0; - switch (var) - { - case ServerInfoType::id: - strncpy(out_buf, serverInfo.id.c_str(), out_buf_len); - break; - case ServerInfoType::name: - strncpy(out_buf, serverInfo.name.c_str(), out_buf_len); - break; - case ServerInfoType::description: - strncpy(out_buf, serverInfo.id.c_str(), out_buf_len); - break; - case ServerInfoType::password: - strncpy(out_buf, serverInfo.password.c_str(), out_buf_len); - break; - default: - n = -1; - } - - ReleaseSRWLockShared(&serverInfoLock); - - return n; -} -int getServerInfoInt(int* out_ptr, ServerInfoType var) -{ - AcquireSRWLockShared(&serverInfoLock); - int n = 0; - switch (var) - { - case ServerInfoType::maxPlayers: - *out_ptr = serverInfo.maxPlayers; - break; - case ServerInfoType::scoreLimit: - *out_ptr = serverInfo.scoreLimit; - break; - case ServerInfoType::endTime: - *out_ptr = serverInfo.endTime; - break; - default: - n = -1; - } - - ReleaseSRWLockShared(&serverInfoLock); - - return n; -} -int getServerInfoBool(bool* out_ptr, ServerInfoType var) -{ - AcquireSRWLockShared(&serverInfoLock); - int n = 0; - switch (var) - { - case ServerInfoType::roundBased: - *out_ptr = serverInfo.roundBased; - break; - default: - n = -1; - } - - ReleaseSRWLockShared(&serverInfoLock); - - return n; -} - -int getGameStateChar(char* out_buf, size_t out_buf_len, GameStateInfoType var) -{ - AcquireSRWLockShared(&gameStateLock); - int n = 0; - switch (var) - { - case GameStateInfoType::map: - strncpy(out_buf, gameState.map.c_str(), out_buf_len); - break; - case GameStateInfoType::mapDisplayName: - strncpy(out_buf, gameState.mapDisplayName.c_str(), out_buf_len); - break; - case GameStateInfoType::playlist: - strncpy(out_buf, gameState.playlist.c_str(), out_buf_len); - break; - case GameStateInfoType::playlistDisplayName: - strncpy(out_buf, gameState.playlistDisplayName.c_str(), out_buf_len); - break; - default: - n = -1; - } - - ReleaseSRWLockShared(&gameStateLock); - - return n; -} -int getGameStateInt(int* out_ptr, GameStateInfoType var) -{ - AcquireSRWLockShared(&gameStateLock); - int n = 0; - switch (var) - { - case GameStateInfoType::ourScore: - *out_ptr = gameState.ourScore; - break; - case GameStateInfoType::secondHighestScore: - *out_ptr = gameState.secondHighestScore; - break; - case GameStateInfoType::highestScore: - *out_ptr = gameState.highestScore; - break; - case GameStateInfoType::players: - *out_ptr = gameState.players; - break; - default: - n = -1; - } - - ReleaseSRWLockShared(&gameStateLock); - - return n; -} -int getGameStateBool(bool* out_ptr, GameStateInfoType var) -{ - AcquireSRWLockShared(&gameStateLock); - int n = 0; - switch (var) - { - case GameStateInfoType::connected: - *out_ptr = gameState.connected; - break; - case GameStateInfoType::loading: - *out_ptr = gameState.loading; - break; - default: - n = -1; - } - - ReleaseSRWLockShared(&gameStateLock); - - return n; -} - -int getPlayerInfoChar(char* out_buf, size_t out_buf_len, PlayerInfoType var) -{ - AcquireSRWLockShared(&playerInfoLock); - int n = 0; - switch (var) - { - default: - n = -1; - } - - ReleaseSRWLockShared(&playerInfoLock); - - return n; -} -int getPlayerInfoInt(int* out_ptr, PlayerInfoType var) -{ - AcquireSRWLockShared(&playerInfoLock); - int n = 0; - switch (var) - { - case PlayerInfoType::uid: - *out_ptr = playerInfo.uid; - break; - default: - n = -1; - } - - ReleaseSRWLockShared(&playerInfoLock); - - return n; -} -int getPlayerInfoBool(bool* out_ptr, PlayerInfoType var) -{ - AcquireSRWLockShared(&playerInfoLock); - int n = 0; - switch (var) - { - default: - n = -1; - } - - ReleaseSRWLockShared(&playerInfoLock); - - return n; -} - -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_pSquirrel && g_pSquirrel) - { - g_pSquirrel->AddFuncRegistration( - "void", - "NSUpdateGameStateUI", - "string gamemode, string gamemodeName, string map, string mapName, bool connected, bool loading", - "", - SQ_UpdateGameStateUI); - g_pSquirrel->AddFuncRegistration( - "void", - "NSUpdateGameStateClient", - "int playerCount, int maxPlayers, int outScore, int secondHighestScore, int highestScore, bool roundBased, int scoreLimit", - "", - SQ_UpdateGameStateClient); - g_pSquirrel->AddFuncRegistration( - "void", - "NSUpdateServerInfo", - "string id, string name, string password, int players, int maxPlayers, string map, string mapDisplayName, string playlist, " - "string " - "playlistDisplayName", - "", - SQ_UpdateServerInfo); - g_pSquirrel->AddFuncRegistration( - "void", "NSUpdateServerInfoReload", "int maxPlayers", "", SQ_UpdateServerInfoBetweenRounds); - g_pSquirrel->AddFuncRegistration("void", "NSUpdateTimeInfo", "float timeInFuture", "", SQ_UpdateTimeInfo); - g_pSquirrel->AddFuncRegistration("void", "NSSetLoading", "bool loading", "", SQ_SetConnected); - g_pSquirrel->AddFuncRegistration("void", "NSUpdateListenServer", "", "", SQ_UpdateListenServer); - } -} diff --git a/NorthstarDLL/plugins.h b/NorthstarDLL/plugins.h deleted file mode 100644 index 953801a2..00000000 --- a/NorthstarDLL/plugins.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once -#include "plugin_abi.h" - -int getServerInfoChar(char* out_buf, size_t out_buf_len, ServerInfoType var); -int getServerInfoInt(int* out_ptr, ServerInfoType var); -int getServerInfoBool(bool* out_ptr, ServerInfoType var); - -int getGameStateChar(char* out_buf, size_t out_buf_len, GameStateInfoType var); -int getGameStateInt(int* out_ptr, GameStateInfoType var); -int getGameStateBool(bool* out_ptr, GameStateInfoType var); - -int getPlayerInfoChar(char* out_buf, size_t out_buf_len, PlayerInfoType var); -int getPlayerInfoInt(int* out_ptr, PlayerInfoType var); -int getPlayerInfoBool(bool* out_ptr, PlayerInfoType var); - -void initGameState(); -void* getPluginObject(PluginObject var); diff --git a/NorthstarDLL/plugins/plugin_abi.h b/NorthstarDLL/plugins/plugin_abi.h new file mode 100644 index 00000000..4b176a32 --- /dev/null +++ b/NorthstarDLL/plugins/plugin_abi.h @@ -0,0 +1,68 @@ +#pragma once +#include + +#define ABI_VERSION 1 +/// +/// This enum is used for referencing the different types of objects we can pass to a plugin +/// Anything exposed to a plugin must not a be C++ type, as they could break when compiling with a different compiler. +/// Any ABI incompatible change must increment the version number. +/// Nothing must be removed from this enum, only appended. When it absolutely necessary to deprecate an object, it should return UNSUPPORTED +/// when retrieved +/// +enum PluginObject +{ + UNSUPPORTED = 0, + GAMESTATE = 1, + SERVERINFO = 2, + PLAYERINFO = 3, + DUMMY = 0xFFFF +}; + +enum GameStateInfoType +{ + ourScore = 0, + secondHighestScore = 1, + highestScore = 2, + connected = 3, + loading = 4, + map = 5, + mapDisplayName = 6, + playlist = 7, + playlistDisplayName = 8, + players = 9 +}; +struct GameState +{ + int (*getGameStateChar)(char* out_buf, size_t out_buf_len, GameStateInfoType var); + int (*getGameStateInt)(int* out_ptr, GameStateInfoType var); + int (*getGameStateBool)(bool* out_ptr, GameStateInfoType var); +}; + +enum ServerInfoType +{ + id = 0, + name = 1, + description = 2, + password = 3, + maxPlayers = 4, + roundBased = 5, + scoreLimit = 6, + endTime = 7 +}; +struct ServerInfo +{ + int (*getServerInfoChar)(char* out_buf, size_t out_buf_len, ServerInfoType var); + int (*getServerInfoInt)(int* out_ptr, ServerInfoType var); + int (*getServerInfoBool)(bool* out_ptr, ServerInfoType var); +}; + +enum PlayerInfoType +{ + uid = 0 +}; +struct PlayerInfo +{ + int (*getPlayerInfoChar)(char* out_buf, size_t out_buf_len, PlayerInfoType var); + int (*getPlayerInfoInt)(int* out_ptr, PlayerInfoType var); + int (*getPlayerInfoBool)(bool* out_ptr, PlayerInfoType var); +}; diff --git a/NorthstarDLL/plugins/plugins.cpp b/NorthstarDLL/plugins/plugins.cpp new file mode 100644 index 00000000..97ac0b9f --- /dev/null +++ b/NorthstarDLL/plugins/plugins.cpp @@ -0,0 +1,422 @@ +#include "pch.h" +#include "squirrel/squirrel.h" +#include "plugins.h" +#include "masterserver/masterserver.h" +#include "core/convar/convar.h" +#include "server/serverpresence.h" + +#include +#include + +/// +/// The data is split into two different representations: one for internal, and one for plugins, for thread safety reasons +/// The struct exposed to plugins contains getter functions for the various data types. +/// We can safely use C++ types like std::string here since these are only ever handled by Northstar internally +/// +struct InternalGameState +{ + int ourScore; + int secondHighestScore; + int highestScore; + + bool connected; + bool loading; + std::string map; + std::string mapDisplayName; + std::string playlist; + std::string playlistDisplayName; + int players; +}; +struct InternalServerInfo +{ + std::string id; + std::string name; + std::string description; + std::string password; + int maxPlayers; + bool roundBased; + int scoreLimit; + int endTime; +}; +// TODO: need to extend this to include current player data like loadouts +struct InternalPlayerInfo +{ + int uid; +}; + +InternalGameState gameState; +InternalServerInfo serverInfo; +InternalPlayerInfo playerInfo; + +GameState gameStateExport; +ServerInfo serverInfoExport; +PlayerInfo playerInfoExport; + +/// +/// We use SRW Locks because plugins will often be running their own thread +/// To ensure thread safety, and to make it difficult to fuck up, we force them to use *our* functions to get data +/// +static SRWLOCK gameStateLock; +static SRWLOCK serverInfoLock; +static SRWLOCK playerInfoLock; + +void* getPluginObject(PluginObject var) +{ + switch (var) + { + case PluginObject::GAMESTATE: + return &gameStateExport; + case PluginObject::SERVERINFO: + return &serverInfoExport; + case PluginObject::PLAYERINFO: + return &playerInfoExport; + default: + return (void*)-1; + } +} + +void initGameState() +{ + // Initalize the Slim Reader / Writer locks + InitializeSRWLock(&gameStateLock); + InitializeSRWLock(&serverInfoLock); + InitializeSRWLock(&playerInfoLock); + + gameStateExport.getGameStateChar = &getGameStateChar; + gameStateExport.getGameStateInt = &getGameStateInt; + gameStateExport.getGameStateBool = &getGameStateBool; + + serverInfoExport.getServerInfoChar = &getServerInfoChar; + serverInfoExport.getServerInfoInt = &getServerInfoInt; + serverInfoExport.getServerInfoBool = &getServerInfoBool; + + playerInfoExport.getPlayerInfoChar = &getPlayerInfoChar; + playerInfoExport.getPlayerInfoInt = &getPlayerInfoInt; + playerInfoExport.getPlayerInfoBool = &getPlayerInfoBool; + + serverInfo.id = ""; + serverInfo.name = ""; + serverInfo.description = ""; + serverInfo.password = ""; + serverInfo.maxPlayers = 0; + gameState.connected = false; + gameState.loading = false; + gameState.map = ""; + gameState.mapDisplayName = ""; + gameState.playlist = ""; + gameState.playlistDisplayName = ""; + gameState.players = 0; + + playerInfo.uid = 123; +} + +// string gamemode, string gamemodeName, string map, string mapName, bool connected, bool loading +SQRESULT SQ_UpdateGameStateUI(HSquirrelVM* sqvm) +{ + AcquireSRWLockExclusive(&gameStateLock); + gameState.map = g_pSquirrel->getstring(sqvm, 1); + gameState.mapDisplayName = g_pSquirrel->getstring(sqvm, 2); + gameState.playlist = g_pSquirrel->getstring(sqvm, 3); + gameState.playlistDisplayName = g_pSquirrel->getstring(sqvm, 4); + gameState.connected = g_pSquirrel->getbool(sqvm, 5); + gameState.loading = g_pSquirrel->getbool(sqvm, 6); + ReleaseSRWLockExclusive(&gameStateLock); + return SQRESULT_NOTNULL; +} + +// int playerCount, int outScore, int secondHighestScore, int highestScore, bool roundBased, int scoreLimit +SQRESULT SQ_UpdateGameStateClient(HSquirrelVM* sqvm) +{ + AcquireSRWLockExclusive(&gameStateLock); + AcquireSRWLockExclusive(&serverInfoLock); + gameState.players = g_pSquirrel->getinteger(sqvm, 1); + serverInfo.maxPlayers = g_pSquirrel->getinteger(sqvm, 2); + gameState.ourScore = g_pSquirrel->getinteger(sqvm, 3); + gameState.secondHighestScore = g_pSquirrel->getinteger(sqvm, 4); + gameState.highestScore = g_pSquirrel->getinteger(sqvm, 5); + serverInfo.roundBased = g_pSquirrel->getbool(sqvm, 6); + serverInfo.scoreLimit = g_pSquirrel->getbool(sqvm, 7); + ReleaseSRWLockExclusive(&gameStateLock); + ReleaseSRWLockExclusive(&serverInfoLock); + return SQRESULT_NOTNULL; +} + +// string id, string name, string password, int players, int maxPlayers, string map, string mapDisplayName, string playlist, string +// playlistDisplayName +SQRESULT SQ_UpdateServerInfo(HSquirrelVM* sqvm) +{ + AcquireSRWLockExclusive(&gameStateLock); + AcquireSRWLockExclusive(&serverInfoLock); + serverInfo.id = g_pSquirrel->getstring(sqvm, 1); + serverInfo.name = g_pSquirrel->getstring(sqvm, 2); + serverInfo.password = g_pSquirrel->getstring(sqvm, 3); + gameState.players = g_pSquirrel->getinteger(sqvm, 4); + serverInfo.maxPlayers = g_pSquirrel->getinteger(sqvm, 5); + gameState.map = g_pSquirrel->getstring(sqvm, 6); + gameState.mapDisplayName = g_pSquirrel->getstring(sqvm, 7); + gameState.playlist = g_pSquirrel->getstring(sqvm, 8); + gameState.playlistDisplayName = g_pSquirrel->getstring(sqvm, 9); + ReleaseSRWLockExclusive(&gameStateLock); + ReleaseSRWLockExclusive(&serverInfoLock); + return SQRESULT_NOTNULL; +} + +// int maxPlayers +SQRESULT SQ_UpdateServerInfoBetweenRounds(HSquirrelVM* sqvm) +{ + AcquireSRWLockExclusive(&serverInfoLock); + serverInfo.id = g_pSquirrel->getstring(sqvm, 1); + serverInfo.name = g_pSquirrel->getstring(sqvm, 2); + serverInfo.password = g_pSquirrel->getstring(sqvm, 3); + serverInfo.maxPlayers = g_pSquirrel->getinteger(sqvm, 4); + ReleaseSRWLockExclusive(&serverInfoLock); + return SQRESULT_NOTNULL; +} + +// float timeInFuture +SQRESULT SQ_UpdateTimeInfo(HSquirrelVM* sqvm) +{ + AcquireSRWLockExclusive(&serverInfoLock); + serverInfo.endTime = ceil(g_pSquirrel->getfloat(sqvm, 1)); + ReleaseSRWLockExclusive(&serverInfoLock); + return SQRESULT_NOTNULL; +} + +// bool loading +SQRESULT SQ_SetConnected(HSquirrelVM* sqvm) +{ + AcquireSRWLockExclusive(&gameStateLock); + gameState.loading = g_pSquirrel->getbool(sqvm, 1); + ReleaseSRWLockExclusive(&gameStateLock); + return SQRESULT_NOTNULL; +} + +SQRESULT SQ_UpdateListenServer(HSquirrelVM* sqvm) +{ + AcquireSRWLockExclusive(&serverInfoLock); + serverInfo.id = g_pMasterServerManager->m_sOwnServerId; + serverInfo.password = ""; // g_pServerPresence->Cvar_ns_server_password->GetString(); todo this fr + ReleaseSRWLockExclusive(&serverInfoLock); + return SQRESULT_NOTNULL; +} + +int getServerInfoChar(char* out_buf, size_t out_buf_len, ServerInfoType var) +{ + AcquireSRWLockShared(&serverInfoLock); + int n = 0; + switch (var) + { + case ServerInfoType::id: + strncpy(out_buf, serverInfo.id.c_str(), out_buf_len); + break; + case ServerInfoType::name: + strncpy(out_buf, serverInfo.name.c_str(), out_buf_len); + break; + case ServerInfoType::description: + strncpy(out_buf, serverInfo.id.c_str(), out_buf_len); + break; + case ServerInfoType::password: + strncpy(out_buf, serverInfo.password.c_str(), out_buf_len); + break; + default: + n = -1; + } + + ReleaseSRWLockShared(&serverInfoLock); + + return n; +} +int getServerInfoInt(int* out_ptr, ServerInfoType var) +{ + AcquireSRWLockShared(&serverInfoLock); + int n = 0; + switch (var) + { + case ServerInfoType::maxPlayers: + *out_ptr = serverInfo.maxPlayers; + break; + case ServerInfoType::scoreLimit: + *out_ptr = serverInfo.scoreLimit; + break; + case ServerInfoType::endTime: + *out_ptr = serverInfo.endTime; + break; + default: + n = -1; + } + + ReleaseSRWLockShared(&serverInfoLock); + + return n; +} +int getServerInfoBool(bool* out_ptr, ServerInfoType var) +{ + AcquireSRWLockShared(&serverInfoLock); + int n = 0; + switch (var) + { + case ServerInfoType::roundBased: + *out_ptr = serverInfo.roundBased; + break; + default: + n = -1; + } + + ReleaseSRWLockShared(&serverInfoLock); + + return n; +} + +int getGameStateChar(char* out_buf, size_t out_buf_len, GameStateInfoType var) +{ + AcquireSRWLockShared(&gameStateLock); + int n = 0; + switch (var) + { + case GameStateInfoType::map: + strncpy(out_buf, gameState.map.c_str(), out_buf_len); + break; + case GameStateInfoType::mapDisplayName: + strncpy(out_buf, gameState.mapDisplayName.c_str(), out_buf_len); + break; + case GameStateInfoType::playlist: + strncpy(out_buf, gameState.playlist.c_str(), out_buf_len); + break; + case GameStateInfoType::playlistDisplayName: + strncpy(out_buf, gameState.playlistDisplayName.c_str(), out_buf_len); + break; + default: + n = -1; + } + + ReleaseSRWLockShared(&gameStateLock); + + return n; +} +int getGameStateInt(int* out_ptr, GameStateInfoType var) +{ + AcquireSRWLockShared(&gameStateLock); + int n = 0; + switch (var) + { + case GameStateInfoType::ourScore: + *out_ptr = gameState.ourScore; + break; + case GameStateInfoType::secondHighestScore: + *out_ptr = gameState.secondHighestScore; + break; + case GameStateInfoType::highestScore: + *out_ptr = gameState.highestScore; + break; + case GameStateInfoType::players: + *out_ptr = gameState.players; + break; + default: + n = -1; + } + + ReleaseSRWLockShared(&gameStateLock); + + return n; +} +int getGameStateBool(bool* out_ptr, GameStateInfoType var) +{ + AcquireSRWLockShared(&gameStateLock); + int n = 0; + switch (var) + { + case GameStateInfoType::connected: + *out_ptr = gameState.connected; + break; + case GameStateInfoType::loading: + *out_ptr = gameState.loading; + break; + default: + n = -1; + } + + ReleaseSRWLockShared(&gameStateLock); + + return n; +} + +int getPlayerInfoChar(char* out_buf, size_t out_buf_len, PlayerInfoType var) +{ + AcquireSRWLockShared(&playerInfoLock); + int n = 0; + switch (var) + { + default: + n = -1; + } + + ReleaseSRWLockShared(&playerInfoLock); + + return n; +} +int getPlayerInfoInt(int* out_ptr, PlayerInfoType var) +{ + AcquireSRWLockShared(&playerInfoLock); + int n = 0; + switch (var) + { + case PlayerInfoType::uid: + *out_ptr = playerInfo.uid; + break; + default: + n = -1; + } + + ReleaseSRWLockShared(&playerInfoLock); + + return n; +} +int getPlayerInfoBool(bool* out_ptr, PlayerInfoType var) +{ + AcquireSRWLockShared(&playerInfoLock); + int n = 0; + switch (var) + { + default: + n = -1; + } + + ReleaseSRWLockShared(&playerInfoLock); + + return n; +} + +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_pSquirrel && g_pSquirrel) + { + g_pSquirrel->AddFuncRegistration( + "void", + "NSUpdateGameStateUI", + "string gamemode, string gamemodeName, string map, string mapName, bool connected, bool loading", + "", + SQ_UpdateGameStateUI); + g_pSquirrel->AddFuncRegistration( + "void", + "NSUpdateGameStateClient", + "int playerCount, int maxPlayers, int outScore, int secondHighestScore, int highestScore, bool roundBased, int scoreLimit", + "", + SQ_UpdateGameStateClient); + g_pSquirrel->AddFuncRegistration( + "void", + "NSUpdateServerInfo", + "string id, string name, string password, int players, int maxPlayers, string map, string mapDisplayName, string playlist, " + "string " + "playlistDisplayName", + "", + SQ_UpdateServerInfo); + g_pSquirrel->AddFuncRegistration( + "void", "NSUpdateServerInfoReload", "int maxPlayers", "", SQ_UpdateServerInfoBetweenRounds); + g_pSquirrel->AddFuncRegistration("void", "NSUpdateTimeInfo", "float timeInFuture", "", SQ_UpdateTimeInfo); + g_pSquirrel->AddFuncRegistration("void", "NSSetLoading", "bool loading", "", SQ_SetConnected); + g_pSquirrel->AddFuncRegistration("void", "NSUpdateListenServer", "", "", SQ_UpdateListenServer); + } +} diff --git a/NorthstarDLL/plugins/plugins.h b/NorthstarDLL/plugins/plugins.h new file mode 100644 index 00000000..953801a2 --- /dev/null +++ b/NorthstarDLL/plugins/plugins.h @@ -0,0 +1,17 @@ +#pragma once +#include "plugin_abi.h" + +int getServerInfoChar(char* out_buf, size_t out_buf_len, ServerInfoType var); +int getServerInfoInt(int* out_ptr, ServerInfoType var); +int getServerInfoBool(bool* out_ptr, ServerInfoType var); + +int getGameStateChar(char* out_buf, size_t out_buf_len, GameStateInfoType var); +int getGameStateInt(int* out_ptr, GameStateInfoType var); +int getGameStateBool(bool* out_ptr, GameStateInfoType var); + +int getPlayerInfoChar(char* out_buf, size_t out_buf_len, PlayerInfoType var); +int getPlayerInfoInt(int* out_ptr, PlayerInfoType var); +int getPlayerInfoBool(bool* out_ptr, PlayerInfoType var); + +void initGameState(); +void* getPluginObject(PluginObject var); diff --git a/NorthstarDLL/printcommand.h b/NorthstarDLL/printcommand.h deleted file mode 100644 index 6c3ef850..00000000 --- a/NorthstarDLL/printcommand.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once -#include "concommand.h" - -void PrintCommandHelpDialogue(const ConCommandBase* command, const char* name); -void TryPrintCvarHelpForCommand(const char* pCommand); -void InitialiseCommandPrint(); diff --git a/NorthstarDLL/printcommands.cpp b/NorthstarDLL/printcommands.cpp deleted file mode 100644 index 90af1575..00000000 --- a/NorthstarDLL/printcommands.cpp +++ /dev/null @@ -1,174 +0,0 @@ -#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 "); - 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 [...]"); - 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 "); - 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; -} diff --git a/NorthstarDLL/printmaps.cpp b/NorthstarDLL/printmaps.cpp deleted file mode 100644 index c378daad..00000000 --- a/NorthstarDLL/printmaps.cpp +++ /dev/null @@ -1,168 +0,0 @@ -#include "pch.h" -#include "printmaps.h" -#include "convar.h" -#include "concommand.h" -#include "modmanager.h" -#include "tier0.h" -#include "r2engine.h" - -#include -#include - -AUTOHOOK_INIT() - -enum class MapSource_t -{ - VPK, - GAMEDIR, - MOD -}; - -const std::unordered_map 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 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 "); - 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; -} diff --git a/NorthstarDLL/printmaps.h b/NorthstarDLL/printmaps.h deleted file mode 100644 index b01761c0..00000000 --- a/NorthstarDLL/printmaps.h +++ /dev/null @@ -1,2 +0,0 @@ -#pragma once -void InitialiseMapsPrint(); diff --git a/NorthstarDLL/r2client.cpp b/NorthstarDLL/r2client.cpp deleted file mode 100644 index 2e7bd564..00000000 --- a/NorthstarDLL/r2client.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "pch.h" -#include "r2client.h" - -using namespace R2; - -// use the R2 namespace for game funcs -namespace R2 -{ - char* g_pLocalPlayerUserID; - char* g_pLocalPlayerOriginToken; - GetBaseLocalClientType GetBaseLocalClient; -} // namespace R2 - -ON_DLL_LOAD("engine.dll", R2EngineClient, (CModule module)) -{ - g_pLocalPlayerUserID = module.Offset(0x13F8E688).As(); - g_pLocalPlayerOriginToken = module.Offset(0x13979C80).As(); - - GetBaseLocalClient = module.Offset(0x78200).As(); -} diff --git a/NorthstarDLL/r2client.h b/NorthstarDLL/r2client.h deleted file mode 100644 index 64ed6c61..00000000 --- a/NorthstarDLL/r2client.h +++ /dev/null @@ -1,11 +0,0 @@ -#pragma once - -// use the R2 namespace for game funcs -namespace R2 -{ - extern char* g_pLocalPlayerUserID; - extern char* g_pLocalPlayerOriginToken; - - typedef void* (*GetBaseLocalClientType)(); - extern GetBaseLocalClientType GetBaseLocalClient; -} // namespace R2 diff --git a/NorthstarDLL/r2engine.cpp b/NorthstarDLL/r2engine.cpp deleted file mode 100644 index 11233a2d..00000000 --- a/NorthstarDLL/r2engine.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "pch.h" -#include "r2engine.h" - -using namespace R2; - -// use the R2 namespace for game funcs -namespace R2 -{ - Cbuf_GetCurrentPlayerType Cbuf_GetCurrentPlayer; - Cbuf_AddTextType Cbuf_AddText; - Cbuf_ExecuteType Cbuf_Execute; - - CEngine* g_pEngine; - - void (*CBaseClient__Disconnect)(void* self, uint32_t unknownButAlways1, const char* reason, ...); - CBaseClient* g_pClientArray; - - server_state_t* g_pServerState; - - char* g_pModName = - nullptr; // we cant set this up here atm since we dont have an offset to it in engine, instead we store it in IsRespawnMod -} // namespace R2 - -ON_DLL_LOAD("engine.dll", R2Engine, (CModule module)) -{ - Cbuf_GetCurrentPlayer = module.Offset(0x120630).As(); - Cbuf_AddText = module.Offset(0x1203B0).As(); - Cbuf_Execute = module.Offset(0x1204B0).As(); - - g_pEngine = module.Offset(0x7D70C8).Deref().As(); - - CBaseClient__Disconnect = module.Offset(0x1012C0).As(); - g_pClientArray = module.Offset(0x12A53F90).As(); - - g_pServerState = module.Offset(0x12A53D48).As(); -} diff --git a/NorthstarDLL/r2engine.h b/NorthstarDLL/r2engine.h deleted file mode 100644 index 2614b4cc..00000000 --- a/NorthstarDLL/r2engine.h +++ /dev/null @@ -1,185 +0,0 @@ -#pragma once -#include "keyvalues.h" - -// use the R2 namespace for game funcs -namespace R2 -{ - // Cbuf - 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_GAMEDLL_FOR_REMOTE_CLIENTS. - 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; - - 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; - - // CEngine - - enum EngineQuitState - { - QUIT_NOTQUITTING = 0, - QUIT_TODESKTOP, - QUIT_RESTART - }; - - enum class 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; - - extern void (*CBaseClient__Disconnect)(void* self, uint32_t unknownButAlways1, const char* reason, ...); - -#pragma once - typedef enum - { - NA_NULL = 0, - NA_LOOPBACK, - NA_IP, - } netadrtype_t; - -#pragma pack(push, 1) - typedef struct netadr_s - { - netadrtype_t type; - unsigned char ip[16]; // IPv6 - // IPv4's 127.0.0.1 is [::ffff:127.0.0.1], that is: - // 00 00 00 00 00 00 00 00 00 00 FF FF 7F 00 00 01 - unsigned short port; - } netadr_t; -#pragma pack(pop) - -#pragma pack(push, 1) - typedef struct netpacket_s - { - netadr_t adr; // sender address - // int source; // received source - char unk[10]; - double received_time; - unsigned char* data; // pointer to raw packet data - void* message; // easy bitbuf data access // 'inpacket.message' etc etc (pointer) - char unk2[16]; - int size; - - // bf_read message; // easy bitbuf data access // 'inpacket.message' etc etc (pointer) - // int size; // size in bytes - // int wiresize; // size in bytes before decompression - // bool stream; // was send as stream - // struct netpacket_s* pNext; // for internal use, should be NULL in public - } netpacket_t; -#pragma pack(pop) - - // #56169 $DB69 PData size - // #512 $200 Trailing data - // #100 $64 Safety buffer - const int PERSISTENCE_MAX_SIZE = 0xDDCD; - - // note: NOT_READY and READY are the only entries we have here that are defined by the vanilla game - // entries after this are custom and used to determine the source of persistence, e.g. whether it is local or remote - enum class ePersistenceReady : char - { - NOT_READY, - READY = 3, - READY_INSECURE = 3, - READY_REMOTE - }; - - // clang-format off - OFFSET_STRUCT(CBaseClient) - { - STRUCT_SIZE(0x2D728) - FIELD(0x16, char m_Name[64]) - FIELD(0x258, KeyValues* m_ConVars) - FIELD(0x4A0, ePersistenceReady m_iPersistenceReady) - FIELD(0x4FA, char m_PersistenceBuffer[PERSISTENCE_MAX_SIZE]) - FIELD(0xF500, char m_UID[32]) - }; - // clang-format on - - extern CBaseClient* g_pClientArray; - - enum server_state_t - { - ss_dead = 0, // Dead - ss_loading, // Spawning - ss_active, // Running - ss_paused, // Running, but paused - }; - - extern server_state_t* g_pServerState; - - extern char* g_pModName; -} // namespace R2 diff --git a/NorthstarDLL/r2server.cpp b/NorthstarDLL/r2server.cpp deleted file mode 100644 index 50cfa239..00000000 --- a/NorthstarDLL/r2server.cpp +++ /dev/null @@ -1,17 +0,0 @@ -#include "pch.h" -#include "r2server.h" - -using namespace R2; - -// use the R2 namespace for game funcs -namespace R2 -{ - CBaseEntity* (*Server_GetEntityByIndex)(int index); - CBasePlayer*(__fastcall* UTIL_PlayerByIndex)(int playerIndex); -} // namespace R2 - -ON_DLL_LOAD("server.dll", R2GameServer, (CModule module)) -{ - Server_GetEntityByIndex = module.Offset(0xFB820).As(); - UTIL_PlayerByIndex = module.Offset(0x26AA10).As(); -} diff --git a/NorthstarDLL/r2server.h b/NorthstarDLL/r2server.h deleted file mode 100644 index aadfdefe..00000000 --- a/NorthstarDLL/r2server.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include "vector.h" - -// use the R2 namespace for game funcs -namespace R2 -{ - // server entity stuff - class CBaseEntity; - extern CBaseEntity* (*Server_GetEntityByIndex)(int index); - - // clang-format off - OFFSET_STRUCT(CBasePlayer) - { - STRUCT_SIZE(0x1D02); - FIELD(0x58, uint32_t m_nPlayerIndex) - - FIELD(0x1C90, bool m_hasBadReputation) - FIELD(0x1C91, char m_communityName[64]) - FIELD(0x1CD1, char m_communityClanTag[16]) - FIELD(0x1CE1, char m_factionName[16]) - FIELD(0x1CF1, char m_hardwareIcon[16]) - FIELD(0x1D01, bool m_happyHourActive) - }; - // clang-format on - - extern CBasePlayer*(__fastcall* UTIL_PlayerByIndex)(int playerIndex); -} // namespace R2 diff --git a/NorthstarDLL/rpakfilesystem.cpp b/NorthstarDLL/rpakfilesystem.cpp deleted file mode 100644 index f6de514b..00000000 --- a/NorthstarDLL/rpakfilesystem.cpp +++ /dev/null @@ -1,342 +0,0 @@ -#include "pch.h" -#include "rpakfilesystem.h" -#include "modmanager.h" -#include "dedicated.h" -#include "tier0.h" - -AUTOHOOK_INIT() - -// there are more i'm just too lazy to add -struct PakLoadFuncs -{ - void* unk0[3]; - int (*LoadPakAsync)(const char* pPath, void* unknownSingleton, int flags, void* callback0, void* callback1); - void* unk1[2]; - void* (*UnloadPak)(int iPakHandle, void* callback); - void* unk2[6]; - void* (*LoadFile)(const char* path); // unsure - void* unk3[10]; - void* (*ReadFileAsync)(const char* pPath, void* a2); -}; - -PakLoadFuncs* g_pakLoadApi; - -PakLoadManager* g_pPakLoadManager; -void** pUnknownPakLoadSingleton; - -int PakLoadManager::LoadPakAsync(const char* pPath, const ePakLoadSource nLoadSource) -{ - int nHandle = g_pakLoadApi->LoadPakAsync(pPath, *pUnknownPakLoadSingleton, 2, nullptr, nullptr); - - // set the load source of the pak we just loaded - if (nHandle != -1) - GetPakInfo(nHandle)->m_nLoadSource = nLoadSource; - - return nHandle; -} - -void PakLoadManager::UnloadPak(const int nPakHandle) -{ - g_pakLoadApi->UnloadPak(nPakHandle, nullptr); -} - -void PakLoadManager::UnloadMapPaks() -{ - for (auto& pair : m_vLoadedPaks) - if (pair.second.m_nLoadSource == ePakLoadSource::MAP) - UnloadPak(pair.first); -} - -LoadedPak* PakLoadManager::TrackLoadedPak(ePakLoadSource nLoadSource, int nPakHandle, size_t nPakNameHash) -{ - LoadedPak pak; - pak.m_nLoadSource = nLoadSource; - pak.m_nPakHandle = nPakHandle; - pak.m_nPakNameHash = nPakNameHash; - - m_vLoadedPaks.insert(std::make_pair(nPakHandle, pak)); - return &m_vLoadedPaks.at(nPakHandle); -} - -void PakLoadManager::RemoveLoadedPak(int nPakHandle) -{ - m_vLoadedPaks.erase(nPakHandle); -} - -LoadedPak* PakLoadManager::GetPakInfo(const int nPakHandle) -{ - return &m_vLoadedPaks.at(nPakHandle); -} - -int PakLoadManager::GetPakHandle(const size_t nPakNameHash) -{ - for (auto& pair : m_vLoadedPaks) - if (pair.second.m_nPakNameHash == nPakNameHash) - return pair.first; - - return -1; -} - -int PakLoadManager::GetPakHandle(const char* pPath) -{ - return GetPakHandle(STR_HASH(pPath)); -} - -void* PakLoadManager::LoadFile(const char* path) -{ - return g_pakLoadApi->LoadFile(path); -} - -void HandlePakAliases(char** map) -{ - // convert the pak being loaded to it's aliased one, e.g. aliasing mp_hub_timeshift => sp_hub_timeshift - for (int64_t i = g_pModManager->m_LoadedMods.size() - 1; i > -1; i--) - { - Mod* mod = &g_pModManager->m_LoadedMods[i]; - if (!mod->m_bEnabled) - continue; - - if (mod->RpakAliases.find(*map) != mod->RpakAliases.end()) - { - *map = &mod->RpakAliases[*map][0]; - return; - } - } -} - -void LoadPreloadPaks() -{ - // note, loading from ./ is necessary otherwise paks will load from gamedir/r2/paks - for (Mod& mod : g_pModManager->m_LoadedMods) - { - if (!mod.m_bEnabled) - continue; - - // need to get a relative path of mod to mod folder - fs::path modPakPath("./" / mod.m_ModDirectory / "paks"); - - for (ModRpakEntry& pak : mod.Rpaks) - if (pak.m_bAutoLoad) - g_pPakLoadManager->LoadPakAsync((modPakPath / pak.m_sPakName).string().c_str(), ePakLoadSource::CONSTANT); - } -} - -void LoadPostloadPaks(const char* pPath) -{ - // note, loading from ./ is necessary otherwise paks will load from gamedir/r2/paks - for (Mod& mod : g_pModManager->m_LoadedMods) - { - if (!mod.m_bEnabled) - continue; - - // need to get a relative path of mod to mod folder - fs::path modPakPath("./" / mod.m_ModDirectory / "paks"); - - for (ModRpakEntry& pak : mod.Rpaks) - if (pak.m_sLoadAfterPak == pPath) - g_pPakLoadManager->LoadPakAsync((modPakPath / pak.m_sPakName).string().c_str(), ePakLoadSource::CONSTANT); - } -} - -void LoadCustomMapPaks(char** pakName, bool* bNeedToFreePakName) -{ - // whether the vanilla game has this rpak - bool bHasOriginalPak = fs::exists(fs::path("r2/paks/Win64/") / *pakName); - - // note, loading from ./ is necessary otherwise paks will load from gamedir/r2/paks - for (Mod& mod : g_pModManager->m_LoadedMods) - { - if (!mod.m_bEnabled) - continue; - - // need to get a relative path of mod to mod folder - fs::path modPakPath("./" / mod.m_ModDirectory / "paks"); - - for (ModRpakEntry& pak : mod.Rpaks) - { - if (!pak.m_bAutoLoad && !pak.m_sPakName.compare(*pakName)) - { - // if the game doesn't have the original pak, let it handle loading this one as if it was the one it was loading originally - if (!bHasOriginalPak) - { - std::string path = (modPakPath / pak.m_sPakName).string(); - *pakName = new char[path.size() + 1]; - strcpy(*pakName, &path[0]); - (*pakName)[path.size()] = '\0'; - - bHasOriginalPak = true; - *bNeedToFreePakName = - true; // we can't free this memory until we're done with the pak, so let whatever's calling this deal with it - } - else - g_pPakLoadManager->LoadPakAsync((modPakPath / pak.m_sPakName).string().c_str(), ePakLoadSource::MAP); - } - } - } -} - -// clang-format off -HOOK(LoadPakAsyncHook, LoadPakAsync, -int, __fastcall, (char* pPath, void* unknownSingleton, int flags, void* pCallback0, void* pCallback1)) -// clang-format on -{ - HandlePakAliases(&pPath); - - // dont load the pak if it's currently loaded already - size_t nPathHash = STR_HASH(pPath); - if (g_pPakLoadManager->GetPakHandle(nPathHash) != -1) - return -1; - - bool bNeedToFreePakName = false; - - static bool bShouldLoadPaks = true; - if (bShouldLoadPaks) - { - // make a copy of the path for comparing to determine whether we should load this pak on dedi, before it could get overwritten by - // LoadCustomMapPaks - std::string originalPath(pPath); - - // disable preloading while we're doing this - bShouldLoadPaks = false; - - LoadPreloadPaks(); - LoadCustomMapPaks(&pPath, &bNeedToFreePakName); - - bShouldLoadPaks = true; - - // do this after custom paks load and in bShouldLoadPaks so we only ever call this on the root pakload call - // todo: could probably add some way to flag custom paks to not be loaded on dedicated servers in rpak.json - - // dedicated only needs common, common_mp, common_sp, and sp_ rpaks - // sp_ rpaks contain tutorial ghost data - // sucks to have to load the entire rpak for that but sp was never meant to be done on dedi - if (IsDedicatedServer() && (Tier0::CommandLine()->CheckParm("-nopakdedi") || - strncmp(&originalPath[0], "common", 6) && strncmp(&originalPath[0], "sp_", 3))) - { - if (bNeedToFreePakName) - delete[] pPath; - - NS::log::rpak->info("Not loading pak {} for dedicated server", originalPath); - return -1; - } - } - - int iPakHandle = LoadPakAsync(pPath, unknownSingleton, flags, pCallback0, pCallback1); - NS::log::rpak->info("LoadPakAsync {} {}", pPath, iPakHandle); - - // trak the pak - g_pPakLoadManager->TrackLoadedPak(ePakLoadSource::UNTRACKED, iPakHandle, nPathHash); - LoadPostloadPaks(pPath); - - if (bNeedToFreePakName) - delete[] pPath; - - return iPakHandle; -} - -// clang-format off -HOOK(UnloadPakHook, UnloadPak, -void*, __fastcall, (int nPakHandle, void* pCallback)) -// clang-format on -{ - // stop tracking the pak - g_pPakLoadManager->RemoveLoadedPak(nPakHandle); - - static bool bShouldUnloadPaks = true; - if (bShouldUnloadPaks) - { - bShouldUnloadPaks = false; - g_pPakLoadManager->UnloadMapPaks(); - bShouldUnloadPaks = true; - } - - NS::log::rpak->info("UnloadPak {}", nPakHandle); - return UnloadPak(nPakHandle, pCallback); -} - -// we hook this exclusively for resolving stbsp paths, but seemingly it's also used for other stuff like vpk, rpak, mprj and starpak loads -// tbh this actually might be for memory mapped files or something, would make sense i think -// clang-format off -HOOK(ReadFileAsyncHook, ReadFileAsync, -void*, __fastcall, (const char* pPath, void* pCallback)) -// clang-format on -{ - fs::path path(pPath); - std::string newPath = ""; - fs::path filename = path.filename(); - - if (path.extension() == ".stbsp") - { - NS::log::rpak->info("LoadStreamBsp: {}", filename.string()); - - // resolve modded stbsp path so we can load mod stbsps - auto modFile = g_pModManager->m_ModFiles.find(g_pModManager->NormaliseModFilePath(fs::path("maps" / filename))); - if (modFile != g_pModManager->m_ModFiles.end()) - { - newPath = (modFile->second.m_pOwningMod->m_ModDirectory / "mod" / modFile->second.m_Path).string(); - pPath = newPath.c_str(); - } - } - else if (path.extension() == ".starpak") - { - // code for this is mostly stolen from above - - // unfortunately I can't find a way to get the rpak that is causing this function call, so I have to - // store them on mod init and then compare the current path with the stored paths - - // game adds r2\ to every path, so assume that a starpak path that begins with r2\paks\ is a vanilla one - // modded starpaks will be in the mod's paks folder (but can be in folders within the paks folder) - - // this might look a bit confusing, but its just an iterator over the various directories in a path. - // path.begin() being the first directory, r2 in this case, which is guaranteed anyway, - // so increment the iterator with ++ to get the first actual directory, * just gets the actual value - // then we compare to "paks" to determine if it's a vanilla rpak or not - if (*++path.begin() != "paks") - { - // remove the r2\ from the start used for path lookups - std::string starpakPath = path.string().substr(3); - // hash the starpakPath to compare with stored entries - size_t hashed = STR_HASH(starpakPath); - - // loop through all loaded mods - for (Mod& mod : g_pModManager->m_LoadedMods) - { - // ignore non-loaded mods - if (!mod.m_bEnabled) - continue; - - // loop through the stored starpak paths - for (size_t hash : mod.StarpakPaths) - { - if (hash == hashed) - { - // construct new path - newPath = (mod.m_ModDirectory / "paks" / starpakPath).string(); - // set path to the new path - pPath = newPath.c_str(); - goto LOG_STARPAK; - } - } - } - } - - LOG_STARPAK: - NS::log::rpak->info("LoadStreamPak: {}", filename.string()); - } - - return ReadFileAsync(pPath, pCallback); -} - -ON_DLL_LOAD("engine.dll", RpakFilesystem, (CModule module)) -{ - AUTOHOOK_DISPATCH(); - - g_pPakLoadManager = new PakLoadManager; - - g_pakLoadApi = module.Offset(0x5BED78).Deref().As(); - pUnknownPakLoadSingleton = module.Offset(0x7C5E20).As(); - - LoadPakAsyncHook.Dispatch(g_pakLoadApi->LoadPakAsync); - UnloadPakHook.Dispatch(g_pakLoadApi->UnloadPak); - ReadFileAsyncHook.Dispatch(g_pakLoadApi->ReadFileAsync); -} diff --git a/NorthstarDLL/rpakfilesystem.h b/NorthstarDLL/rpakfilesystem.h deleted file mode 100644 index 3f608dba..00000000 --- a/NorthstarDLL/rpakfilesystem.h +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once - -enum class ePakLoadSource -{ - UNTRACKED = -1, // not a pak we loaded, we shouldn't touch this one - - CONSTANT, // should be loaded at all times - MAP // loaded from a map, should be unloaded when the map is unloaded -}; - -struct LoadedPak -{ - ePakLoadSource m_nLoadSource; - int m_nPakHandle; - size_t m_nPakNameHash; -}; - -class PakLoadManager -{ - private: - std::map m_vLoadedPaks {}; - std::unordered_map m_HashToPakHandle {}; - - public: - int LoadPakAsync(const char* pPath, const ePakLoadSource nLoadSource); - void UnloadPak(const int nPakHandle); - void UnloadMapPaks(); - void* LoadFile(const char* path); // this is a guess - - LoadedPak* TrackLoadedPak(ePakLoadSource nLoadSource, int nPakHandle, size_t nPakNameHash); - void RemoveLoadedPak(int nPakHandle); - - LoadedPak* GetPakInfo(const int nPakHandle); - - int GetPakHandle(const size_t nPakNameHash); - int GetPakHandle(const char* pPath); -}; - -extern PakLoadManager* g_pPakLoadManager; diff --git a/NorthstarDLL/runframe.cpp b/NorthstarDLL/runframe.cpp deleted file mode 100644 index eb401f51..00000000 --- a/NorthstarDLL/runframe.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "pch.h" -#include "r2engine.h" -#include "r2server.h" -#include "hoststate.h" -#include "serverpresence.h" - -AUTOHOOK_INIT() - -// clang-format off -AUTOHOOK(CEngine__Frame, engine.dll + 0x1C8650, -void, __fastcall, (R2::CEngine* self)) -// clang-format on -{ - CEngine__Frame(self); -} - -ON_DLL_LOAD("engine.dll", RunFrame, (CModule module)) -{ - AUTOHOOK_DISPATCH() -} diff --git a/NorthstarDLL/scriptbrowserhooks.cpp b/NorthstarDLL/scriptbrowserhooks.cpp deleted file mode 100644 index df4014de..00000000 --- a/NorthstarDLL/scriptbrowserhooks.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "pch.h" - -AUTOHOOK_INIT() - -bool* bIsOriginOverlayEnabled; - -// clang-format off -AUTOHOOK(OpenExternalWebBrowser, engine.dll + 0x184E40, -void, __fastcall, (char* pUrl, char flags)) -// clang-format on -{ - bool bIsOriginOverlayEnabledOriginal = *bIsOriginOverlayEnabled; - if (flags & 2 && !strncmp(pUrl, "http", 4)) // custom force external browser flag - *bIsOriginOverlayEnabled = false; // if this bool is false, game will use an external browser rather than the origin overlay one - - OpenExternalWebBrowser(pUrl, flags); - *bIsOriginOverlayEnabled = bIsOriginOverlayEnabledOriginal; -} - -ON_DLL_LOAD_CLIENT("engine.dll", ScriptExternalBrowserHooks, (CModule module)) -{ - AUTOHOOK_DISPATCH() - - bIsOriginOverlayEnabled = module.Offset(0x13978255).As(); -} diff --git a/NorthstarDLL/scriptdatatables.cpp b/NorthstarDLL/scriptdatatables.cpp deleted file mode 100644 index dfa45737..00000000 --- a/NorthstarDLL/scriptdatatables.cpp +++ /dev/null @@ -1,910 +0,0 @@ -#include "pch.h" -#include "squirrel.h" -#include "rpakfilesystem.h" -#include "convar.h" -#include "dedicated.h" -#include "filesystem.h" -#include "vector.h" -#include "tier0.h" -#include "r2engine.h" -#include -#include -#include -#include -#include - -const uint64_t USERDATA_TYPE_DATATABLE = 0xFFF7FFF700000004; -const uint64_t USERDATA_TYPE_DATATABLE_CUSTOM = 0xFFFCFFFC12345678; - -enum class DatatableType : int -{ - BOOL = 0, - INT, - FLOAT, - VECTOR, - STRING, - ASSET, - UNK_STRING // unknown but deffo a string type -}; - -struct ColumnInfo -{ - char* name; - DatatableType type; - int offset; -}; - -struct Datatable -{ - int numColumns; - int numRows; - ColumnInfo* columnInfo; - char* data; // actually data pointer - int rowInfo; -}; - -ConVar* Cvar_ns_prefer_datatable_from_disk; - -template Datatable* (*SQ_GetDatatableInternal)(HSquirrelVM* sqvm); - -struct CSVData -{ - std::string m_sAssetName; - std::string m_sCSVString; - char* m_pDataBuf; - size_t m_nDataBufSize; - - std::vector columns; - std::vector> dataPointers; -}; - -std::unordered_map CSVCache; - -Vector3 StringToVector(char* pString) -{ - Vector3 vRet; - - int length = 0; - while (pString[length]) - { - if ((pString[length] == '<') || (pString[length] == '>')) - pString[length] = '\0'; - length++; - } - - int startOfFloat = 1; - int currentIndex = 1; - - while (pString[currentIndex] && (pString[currentIndex] != ',')) - currentIndex++; - pString[currentIndex] = '\0'; - vRet.x = std::stof(&pString[startOfFloat]); - startOfFloat = ++currentIndex; - - while (pString[currentIndex] && (pString[currentIndex] != ',')) - currentIndex++; - pString[currentIndex] = '\0'; - vRet.y = std::stof(&pString[startOfFloat]); - startOfFloat = ++currentIndex; - - while (pString[currentIndex] && (pString[currentIndex] != ',')) - currentIndex++; - pString[currentIndex] = '\0'; - vRet.z = std::stof(&pString[startOfFloat]); - startOfFloat = ++currentIndex; - - return vRet; -} - -// var function GetDataTable( asset path ) -REPLACE_SQFUNC(GetDataTable, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - const char* pAssetName; - g_pSquirrel->getasset(sqvm, 2, &pAssetName); - - if (strncmp(pAssetName, "datatable/", 10)) - { - g_pSquirrel->raiseerror(sqvm, fmt::format("Asset \"{}\" doesn't start with \"datatable/\"", pAssetName).c_str()); - return SQRESULT_ERROR; - } - else if (!Cvar_ns_prefer_datatable_from_disk->GetBool() && g_pPakLoadManager->LoadFile(pAssetName)) - return g_pSquirrel->m_funcOriginals["GetDataTable"](sqvm); - // either we prefer disk datatables, or we're loading a datatable that wasn't found in rpak - else - { - std::string sAssetPath(fmt::format("scripts/{}", pAssetName)); - - // first, check the cache - if (CSVCache.find(pAssetName) != CSVCache.end()) - { - CSVData** pUserdata = g_pSquirrel->createuserdata(sqvm, sizeof(CSVData*)); - g_pSquirrel->setuserdatatypeid(sqvm, -1, USERDATA_TYPE_DATATABLE_CUSTOM); - *pUserdata = &CSVCache[pAssetName]; - - return SQRESULT_NOTNULL; - } - - // check files on disk - // we don't use .rpak as the extension for on-disk datatables, so we need to replace .rpak with .csv in the filename we're reading - fs::path diskAssetPath("scripts"); - if (fs::path(pAssetName).extension() == ".rpak") - diskAssetPath /= fs::path(pAssetName).remove_filename() / (fs::path(pAssetName).stem().string() + ".csv"); - else - diskAssetPath /= fs::path(pAssetName); - - std::string sDiskAssetPath(diskAssetPath.string()); - if ((*R2::g_pFilesystem)->m_vtable2->FileExists(&(*R2::g_pFilesystem)->m_vtable2, sDiskAssetPath.c_str(), "GAME")) - { - std::string sTableCSV = R2::ReadVPKFile(sDiskAssetPath.c_str()); - if (!sTableCSV.size()) - { - g_pSquirrel->raiseerror(sqvm, fmt::format("Datatable \"{}\" is empty", pAssetName).c_str()); - return SQRESULT_ERROR; - } - - // somewhat shit, but ensure we end with a newline to make parsing easier - if (sTableCSV[sTableCSV.length() - 1] != '\n') - sTableCSV += '\n'; - - CSVData csv; - csv.m_sAssetName = pAssetName; - csv.m_sCSVString = sTableCSV; - csv.m_nDataBufSize = sTableCSV.size(); - csv.m_pDataBuf = new char[csv.m_nDataBufSize]; - memcpy(csv.m_pDataBuf, &sTableCSV[0], csv.m_nDataBufSize); - - // parse the csv - // csvs are essentially comma and newline-deliniated sets of strings for parsing, only thing we need to worry about is quoted - // entries when we parse an element of the csv, rather than allocating an entry for it, we just convert that element to a - // null-terminated string i.e., store the ptr to the first char of it, then make the comma that delinates it a nullchar - - bool bHasColumns = false; - bool bInQuotes = false; - - std::vector vCurrentRow; - char* pElemStart = csv.m_pDataBuf; - char* pElemEnd = nullptr; - - for (int i = 0; i < csv.m_nDataBufSize; i++) - { - if (csv.m_pDataBuf[i] == '\r' && csv.m_pDataBuf[i + 1] == '\n') - { - if (!pElemEnd) - pElemEnd = csv.m_pDataBuf + i; - - continue; // next iteration can handle the \n - } - - // newline, end of a row - if (csv.m_pDataBuf[i] == '\n') - { - // shouldn't have newline in string - if (bInQuotes) - { - g_pSquirrel->raiseerror(sqvm, "Unexpected \\n in string"); - return SQRESULT_ERROR; - } - - // push last entry to current row - if (pElemEnd) - *pElemEnd = '\0'; - else - csv.m_pDataBuf[i] = '\0'; - - vCurrentRow.push_back(pElemStart); - - // newline, push last line to csv data and go from there - if (!bHasColumns) - { - bHasColumns = true; - csv.columns = vCurrentRow; - } - else - csv.dataPointers.push_back(vCurrentRow); - - vCurrentRow.clear(); - // put start of current element at char after newline - pElemStart = csv.m_pDataBuf + i + 1; - pElemEnd = nullptr; - } - // we're starting or ending a quoted string - else if (csv.m_pDataBuf[i] == '"') - { - // start quoted string - if (!bInQuotes) - { - // shouldn't have quoted strings in column names - if (!bHasColumns) - { - g_pSquirrel->raiseerror(sqvm, "Unexpected \" in column name"); - return SQRESULT_ERROR; - } - - bInQuotes = true; - // put start of current element at char after string begin - pElemStart = csv.m_pDataBuf + i + 1; - } - // end quoted string - else - { - pElemEnd = csv.m_pDataBuf + i; - bInQuotes = false; - } - } - // don't parse commas in quotes - else if (bInQuotes) - { - continue; - } - // comma, push new entry to current row - else if (csv.m_pDataBuf[i] == ',') - { - if (pElemEnd) - *pElemEnd = '\0'; - else - csv.m_pDataBuf[i] = '\0'; - - vCurrentRow.push_back(pElemStart); - // put start of next element at char after comma - pElemStart = csv.m_pDataBuf + i + 1; - pElemEnd = nullptr; - } - } - - // add to cache and return - CSVData** pUserdata = g_pSquirrel->createuserdata(sqvm, sizeof(CSVData*)); - g_pSquirrel->setuserdatatypeid(sqvm, -1, USERDATA_TYPE_DATATABLE_CUSTOM); - CSVCache[pAssetName] = csv; - *pUserdata = &CSVCache[pAssetName]; - - return SQRESULT_NOTNULL; - } - // the file doesn't exist on disk, check rpak if we haven't already - else if (Cvar_ns_prefer_datatable_from_disk->GetBool() && g_pPakLoadManager->LoadFile(pAssetName)) - return g_pSquirrel->m_funcOriginals["GetDataTable"](sqvm); - // the file doesn't exist at all, error - else - { - g_pSquirrel->raiseerror(sqvm, fmt::format("Datatable {} not found", pAssetName).c_str()); - return SQRESULT_ERROR; - } - } -} - -// int function GetDataTableColumnByName( var datatable, string columnName ) -REPLACE_SQFUNC(GetDataTableColumnByName, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableColumnByName"](sqvm); - - CSVData* csv = *pData; - const char* pColumnName = g_pSquirrel->getstring(sqvm, 2); - - for (int i = 0; i < csv->columns.size(); i++) - { - if (!strcmp(csv->columns[i], pColumnName)) - { - g_pSquirrel->pushinteger(sqvm, i); - return SQRESULT_NOTNULL; - } - } - - // column not found - g_pSquirrel->pushinteger(sqvm, -1); - return SQRESULT_NOTNULL; -} - -// int function GetDataTableRowCount( var datatable ) -REPLACE_SQFUNC(GetDataTableRowCount, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDatatableRowCount"](sqvm); - - CSVData* csv = *pData; - g_pSquirrel->pushinteger(sqvm, csv->dataPointers.size()); - return SQRESULT_NOTNULL; -} - -// string function GetDataTableString( var datatable, int row, int col ) -REPLACE_SQFUNC(GetDataTableString, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableString"](sqvm); - - CSVData* csv = *pData; - const int nRow = g_pSquirrel->getinteger(sqvm, 2); - const int nCol = g_pSquirrel->getinteger(sqvm, 3); - if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushstring(sqvm, csv->dataPointers[nRow][nCol], -1); - return SQRESULT_NOTNULL; -} - -// asset function GetDataTableAsset( var datatable, int row, int col ) -REPLACE_SQFUNC(GetDataTableAsset, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableAsset"](sqvm); - - CSVData* csv = *pData; - const int nRow = g_pSquirrel->getinteger(sqvm, 2); - const int nCol = g_pSquirrel->getinteger(sqvm, 3); - if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushasset(sqvm, csv->dataPointers[nRow][nCol], -1); - return SQRESULT_NOTNULL; -} - -// int function GetDataTableInt( var datatable, int row, int col ) -REPLACE_SQFUNC(GetDataTableInt, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableInt"](sqvm); - - CSVData* csv = *pData; - const int nRow = g_pSquirrel->getinteger(sqvm, 2); - const int nCol = g_pSquirrel->getinteger(sqvm, 3); - if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushinteger(sqvm, std::stoi(csv->dataPointers[nRow][nCol])); - return SQRESULT_NOTNULL; -} - -// float function GetDataTableFloat( var datatable, int row, int col ) -REPLACE_SQFUNC(GetDataTableFloat, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableFloat"](sqvm); - - CSVData* csv = *pData; - const int nRow = g_pSquirrel->getinteger(sqvm, 2); - const int nCol = g_pSquirrel->getinteger(sqvm, 3); - if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushfloat(sqvm, std::stof(csv->dataPointers[nRow][nCol])); - return SQRESULT_NOTNULL; -} - -// bool function GetDataTableBool( var datatable, int row, int col ) -REPLACE_SQFUNC(GetDataTableBool, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableBool"](sqvm); - - CSVData* csv = *pData; - const int nRow = g_pSquirrel->getinteger(sqvm, 2); - const int nCol = g_pSquirrel->getinteger(sqvm, 3); - if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushbool(sqvm, std::stoi(csv->dataPointers[nRow][nCol])); - return SQRESULT_NOTNULL; -} - -// vector function GetDataTableVector( var datatable, int row, int col ) -REPLACE_SQFUNC(GetDataTableVector, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableVector"](sqvm); - - CSVData* csv = *pData; - const int nRow = g_pSquirrel->getinteger(sqvm, 2); - const int nCol = g_pSquirrel->getinteger(sqvm, 3); - if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushvector(sqvm, StringToVector(csv->dataPointers[nRow][nCol])); - return SQRESULT_NOTNULL; -} - -// int function GetDataTableRowMatchingStringValue( var datatable, int col, string value ) -REPLACE_SQFUNC(GetDataTableRowMatchingStringValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableRowMatchingStringValue"](sqvm); - - CSVData* csv = *pData; - int nCol = g_pSquirrel->getinteger(sqvm, 2); - const char* pStringVal = g_pSquirrel->getstring(sqvm, 3); - for (int i = 0; i < csv->dataPointers.size(); i++) - { - if (!strcmp(csv->dataPointers[i][nCol], pStringVal)) - { - g_pSquirrel->pushinteger(sqvm, i); - return SQRESULT_NOTNULL; - } - } - - g_pSquirrel->pushinteger(sqvm, -1); - return SQRESULT_NOTNULL; -} - -// int function GetDataTableRowMatchingAssetValue( var datatable, int col, asset value ) -REPLACE_SQFUNC(GetDataTableMatchingAssetValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableRowMatchingAssetValue"](sqvm); - - CSVData* csv = *pData; - int nCol = g_pSquirrel->getinteger(sqvm, 2); - const char* pStringVal; - g_pSquirrel->getasset(sqvm, 3, &pStringVal); - for (int i = 0; i < csv->dataPointers.size(); i++) - { - if (!strcmp(csv->dataPointers[i][nCol], pStringVal)) - { - g_pSquirrel->pushinteger(sqvm, i); - return SQRESULT_NOTNULL; - } - } - - g_pSquirrel->pushinteger(sqvm, -1); - return SQRESULT_NOTNULL; -} - -// int function GetDataTableRowMatchingFloatValue( var datatable, int col, float value ) -REPLACE_SQFUNC(GetDataTableRowMatchingFloatValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableRowMatchingFloatValue"](sqvm); - - CSVData* csv = *pData; - int nCol = g_pSquirrel->getinteger(sqvm, 2); - const float flFloatVal = g_pSquirrel->getfloat(sqvm, 3); - for (int i = 0; i < csv->dataPointers.size(); i++) - { - if (flFloatVal == std::stof(csv->dataPointers[i][nCol])) - { - g_pSquirrel->pushinteger(sqvm, i); - return SQRESULT_NOTNULL; - } - } - - g_pSquirrel->pushinteger(sqvm, -1); - return SQRESULT_NOTNULL; -} - -// int function GetDataTableRowMatchingIntValue( var datatable, int col, int value ) -REPLACE_SQFUNC(GetDataTableRowMatchingIntValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableRowMatchingIntValue"](sqvm); - - CSVData* csv = *pData; - int nCol = g_pSquirrel->getinteger(sqvm, 2); - const int nIntVal = g_pSquirrel->getinteger(sqvm, 3); - for (int i = 0; i < csv->dataPointers.size(); i++) - { - if (nIntVal == std::stoi(csv->dataPointers[i][nCol])) - { - g_pSquirrel->pushinteger(sqvm, i); - return SQRESULT_NOTNULL; - } - } - - g_pSquirrel->pushinteger(sqvm, -1); - return SQRESULT_NOTNULL; -} - -// int function GetDataTableRowMatchingVectorValue( var datatable, int col, vector value ) -REPLACE_SQFUNC(GetDataTableRowMatchingVectorValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableRowMatchingVectorValue"](sqvm); - - CSVData* csv = *pData; - int nCol = g_pSquirrel->getinteger(sqvm, 2); - const Vector3 vVectorVal = g_pSquirrel->getvector(sqvm, 3); - - for (int i = 0; i < csv->dataPointers.size(); i++) - { - if (vVectorVal == StringToVector(csv->dataPointers[i][nCol])) - { - g_pSquirrel->pushinteger(sqvm, i); - return SQRESULT_NOTNULL; - } - } - - g_pSquirrel->pushinteger(sqvm, -1); - return SQRESULT_NOTNULL; -} - -// int function GetDataTableRowGreaterThanOrEqualToIntValue( var datatable, int col, int value ) -REPLACE_SQFUNC(GetDataTableRowGreaterThanOrEqualToIntValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableRowGreaterThanOrEqualToIntValue"](sqvm); - - CSVData* csv = *pData; - int nCol = g_pSquirrel->getinteger(sqvm, 2); - const int nIntVal = g_pSquirrel->getinteger(sqvm, 3); - for (int i = 0; i < csv->dataPointers.size(); i++) - { - if (nIntVal >= std::stoi(csv->dataPointers[i][nCol])) - { - spdlog::info("datatable not loaded"); - g_pSquirrel->pushinteger(sqvm, 1); - return SQRESULT_NOTNULL; - } - } - - g_pSquirrel->pushinteger(sqvm, -1); - return SQRESULT_NOTNULL; -} - -// int function GetDataTableRowLessThanOrEqualToIntValue( var datatable, int col, int value ) -REPLACE_SQFUNC(GetDataTableRowLessThanOrEqualToIntValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableRowLessThanOrEqualToIntValue"](sqvm); - - CSVData* csv = *pData; - int nCol = g_pSquirrel->getinteger(sqvm, 2); - const int nIntVal = g_pSquirrel->getinteger(sqvm, 3); - for (int i = 0; i < csv->dataPointers.size(); i++) - { - if (nIntVal <= std::stoi(csv->dataPointers[i][nCol])) - { - g_pSquirrel->pushinteger(sqvm, i); - return SQRESULT_NOTNULL; - } - } - - g_pSquirrel->pushinteger(sqvm, -1); - return SQRESULT_NOTNULL; -} - -// int function GetDataTableRowGreaterThanOrEqualToFloatValue( var datatable, int col, float value ) -REPLACE_SQFUNC(GetDataTableRowGreaterThanOrEqualToFloatValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableRowGreaterThanOrEqualToFloatValue"](sqvm); - - CSVData* csv = *pData; - int nCol = g_pSquirrel->getinteger(sqvm, 2); - const float flFloatVal = g_pSquirrel->getfloat(sqvm, 3); - for (int i = 0; i < csv->dataPointers.size(); i++) - { - if (flFloatVal >= std::stof(csv->dataPointers[i][nCol])) - { - g_pSquirrel->pushinteger(sqvm, i); - return SQRESULT_NOTNULL; - } - } - - g_pSquirrel->pushinteger(sqvm, -1); - return SQRESULT_NOTNULL; -} - -// int function GetDataTableRowLessThanOrEqualToFloatValue( var datatable, int col, float value ) -REPLACE_SQFUNC(GetDataTableRowLessThanOrEqualToFloatValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) -{ - CSVData** pData; - uint64_t typeId; - g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); - - if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) - return g_pSquirrel->m_funcOriginals["GetDataTableRowLessThanOrEqualToFloatValue"](sqvm); - - CSVData* csv = *pData; - int nCol = g_pSquirrel->getinteger(sqvm, 2); - const float flFloatVal = g_pSquirrel->getfloat(sqvm, 3); - for (int i = 0; i < csv->dataPointers.size(); i++) - { - if (flFloatVal <= std::stof(csv->dataPointers[i][nCol])) - { - g_pSquirrel->pushinteger(sqvm, i); - return SQRESULT_NOTNULL; - } - } - - g_pSquirrel->pushinteger(sqvm, -1); - return SQRESULT_NOTNULL; -} - -std::string DataTableToString(Datatable* datatable) -{ - std::string sCSVString; - - // write columns - bool bShouldComma = false; - for (int i = 0; i < datatable->numColumns; i++) - { - if (bShouldComma) - sCSVString += ','; - else - bShouldComma = true; - - sCSVString += datatable->columnInfo[i].name; - } - - // write rows - for (int row = 0; row < datatable->numRows; row++) - { - sCSVString += '\n'; - - bool bShouldComma = false; - for (int col = 0; col < datatable->numColumns; col++) - { - if (bShouldComma) - sCSVString += ','; - else - bShouldComma = true; - - // output typed data - ColumnInfo column = datatable->columnInfo[col]; - const void* pUntypedVal = datatable->data + column.offset + row * datatable->rowInfo; - switch (column.type) - { - case DatatableType::BOOL: - { - sCSVString += *(bool*)pUntypedVal ? '1' : '0'; - break; - } - - case DatatableType::INT: - { - sCSVString += std::to_string(*(int*)pUntypedVal); - break; - } - - case DatatableType::FLOAT: - { - sCSVString += std::to_string(*(float*)pUntypedVal); - break; - } - - case DatatableType::VECTOR: - { - Vector3 pVector((float*)pUntypedVal); - sCSVString += fmt::format("<{},{},{}>", pVector.x, pVector.y, pVector.z); - break; - } - - case DatatableType::STRING: - case DatatableType::ASSET: - case DatatableType::UNK_STRING: - { - sCSVString += fmt::format("\"{}\"", *(char**)pUntypedVal); - break; - } - } - } - } - - return sCSVString; -} - -void DumpDatatable(const char* pDatatablePath) -{ - Datatable* pDatatable = (Datatable*)g_pPakLoadManager->LoadFile(pDatatablePath); - if (!pDatatable) - { - spdlog::error("couldn't load datatable {} (rpak containing it may not be loaded?)", pDatatablePath); - return; - } - - std::string sOutputPath(fmt::format("{}/scripts/datatable/{}.csv", R2::g_pModName, fs::path(pDatatablePath).stem().string())); - std::string sDatatableContents(DataTableToString(pDatatable)); - - fs::create_directories(fs::path(sOutputPath).remove_filename()); - std::ofstream outputStream(sOutputPath); - outputStream.write(sDatatableContents.c_str(), sDatatableContents.size()); - outputStream.close(); - - spdlog::info("dumped datatable {} {} to {}", pDatatablePath, (void*)pDatatable, sOutputPath); -} - -void ConCommand_dump_datatable(const CCommand& args) -{ - if (args.ArgC() < 2) - { - spdlog::info("usage: dump_datatable datatable/tablename.rpak"); - return; - } - - DumpDatatable(args.Arg(1)); -} - -void ConCommand_dump_datatables(const CCommand& args) -{ - // likely not a comprehensive list, might be missing a couple? - static const std::vector VANILLA_DATATABLE_PATHS = { - "datatable/burn_meter_rewards.rpak", - "datatable/burn_meter_store.rpak", - "datatable/calling_cards.rpak", - "datatable/callsign_icons.rpak", - "datatable/camo_skins.rpak", - "datatable/default_pilot_loadouts.rpak", - "datatable/default_titan_loadouts.rpak", - "datatable/faction_leaders.rpak", - "datatable/fd_awards.rpak", - "datatable/features_mp.rpak", - "datatable/non_loadout_weapons.rpak", - "datatable/pilot_abilities.rpak", - "datatable/pilot_executions.rpak", - "datatable/pilot_passives.rpak", - "datatable/pilot_properties.rpak", - "datatable/pilot_weapons.rpak", - "datatable/pilot_weapon_features.rpak", - "datatable/pilot_weapon_mods.rpak", - "datatable/pilot_weapon_mods_common.rpak", - "datatable/playlist_items.rpak", - "datatable/titans_mp.rpak", - "datatable/titan_abilities.rpak", - "datatable/titan_executions.rpak", - "datatable/titan_fd_upgrades.rpak", - "datatable/titan_nose_art.rpak", - "datatable/titan_passives.rpak", - "datatable/titan_primary_mods.rpak", - "datatable/titan_primary_mods_common.rpak", - "datatable/titan_primary_weapons.rpak", - "datatable/titan_properties.rpak", - "datatable/titan_skins.rpak", - "datatable/titan_voices.rpak", - "datatable/unlocks_faction_level.rpak", - "datatable/unlocks_fd_titan_level.rpak", - "datatable/unlocks_player_level.rpak", - "datatable/unlocks_random.rpak", - "datatable/unlocks_titan_level.rpak", - "datatable/unlocks_weapon_level_pilot.rpak", - "datatable/weapon_skins.rpak", - "datatable/xp_per_faction_level.rpak", - "datatable/xp_per_fd_titan_level.rpak", - "datatable/xp_per_player_level.rpak", - "datatable/xp_per_titan_level.rpak", - "datatable/xp_per_weapon_level.rpak", - "datatable/faction_leaders_dropship_anims.rpak", - "datatable/score_events.rpak", - "datatable/startpoints.rpak", - "datatable/sp_levels.rpak", - "datatable/community_entries.rpak", - "datatable/spotlight_images.rpak", - "datatable/death_hints_mp.rpak", - "datatable/flightpath_assets.rpak", - "datatable/earn_meter_mp.rpak", - "datatable/battle_chatter_voices.rpak", - "datatable/battle_chatter.rpak", - "datatable/titan_os_conversations.rpak", - "datatable/faction_dialogue.rpak", - "datatable/grunt_chatter_mp.rpak", - "datatable/spectre_chatter_mp.rpak", - "datatable/pain_death_sounds.rpak", - "datatable/caller_ids_mp.rpak"}; - - for (const char* datatable : VANILLA_DATATABLE_PATHS) - DumpDatatable(datatable); -} - -ON_DLL_LOAD_RELIESON("server.dll", ServerScriptDatatables, ServerSquirrel, (CModule module)) -{ - SQ_GetDatatableInternal = module.Offset(0x1250f0).As(); -} - -ON_DLL_LOAD_RELIESON("client.dll", ClientScriptDatatables, ClientSquirrel, (CModule module)) -{ - SQ_GetDatatableInternal = module.Offset(0x1C9070).As(); - SQ_GetDatatableInternal = SQ_GetDatatableInternal; -} - -ON_DLL_LOAD_RELIESON("engine.dll", SharedScriptDataTables, ConVar, (CModule module)) -{ - Cvar_ns_prefer_datatable_from_disk = new ConVar( - "ns_prefer_datatable_from_disk", - IsDedicatedServer() && Tier0::CommandLine()->CheckParm("-nopakdedi") ? "1" : "0", - FCVAR_NONE, - "whether to prefer loading datatables from disk, rather than rpak"); - - RegisterConCommand("dump_datatables", ConCommand_dump_datatables, "dumps all datatables from a hardcoded list", FCVAR_NONE); - RegisterConCommand("dump_datatable", ConCommand_dump_datatable, "dump a datatable", FCVAR_NONE); -} diff --git a/NorthstarDLL/scriptjson.cpp b/NorthstarDLL/scriptjson.cpp deleted file mode 100644 index 049a7b20..00000000 --- a/NorthstarDLL/scriptjson.cpp +++ /dev/null @@ -1,266 +0,0 @@ -#include "pch.h" -#include "squirrel.h" - -#include "rapidjson/error/en.h" -#include "rapidjson/document.h" -#include "rapidjson/writer.h" -#include "rapidjson/stringbuffer.h" - -#ifdef _MSC_VER -#undef GetObject // fuck microsoft developers -#endif - -template void -DecodeJsonArray(HSquirrelVM* sqvm, rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>* arr) -{ - g_pSquirrel->newarray(sqvm, 0); - - for (auto& itr : arr->GetArray()) - { - switch (itr.GetType()) - { - case rapidjson::kObjectType: - DecodeJsonTable(sqvm, &itr); - g_pSquirrel->arrayappend(sqvm, -2); - break; - case rapidjson::kArrayType: - DecodeJsonArray(sqvm, &itr); - g_pSquirrel->arrayappend(sqvm, -2); - break; - case rapidjson::kStringType: - g_pSquirrel->pushstring(sqvm, itr.GetString(), -1); - g_pSquirrel->arrayappend(sqvm, -2); - break; - case rapidjson::kTrueType: - case rapidjson::kFalseType: - g_pSquirrel->pushbool(sqvm, itr.GetBool()); - g_pSquirrel->arrayappend(sqvm, -2); - break; - case rapidjson::kNumberType: - if (itr.IsDouble() || itr.IsFloat()) - g_pSquirrel->pushfloat(sqvm, itr.GetFloat()); - else - g_pSquirrel->pushinteger(sqvm, itr.GetInt()); - g_pSquirrel->arrayappend(sqvm, -2); - break; - } - } -} - -template void -DecodeJsonTable(HSquirrelVM* sqvm, rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>* obj) -{ - g_pSquirrel->newtable(sqvm); - - for (auto itr = obj->MemberBegin(); itr != obj->MemberEnd(); itr++) - { - switch (itr->value.GetType()) - { - case rapidjson::kObjectType: - g_pSquirrel->pushstring(sqvm, itr->name.GetString(), -1); - DecodeJsonTable( - sqvm, (rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>*)&itr->value); - g_pSquirrel->newslot(sqvm, -3, false); - break; - case rapidjson::kArrayType: - g_pSquirrel->pushstring(sqvm, itr->name.GetString(), -1); - DecodeJsonArray( - sqvm, (rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>*)&itr->value); - g_pSquirrel->newslot(sqvm, -3, false); - break; - case rapidjson::kStringType: - g_pSquirrel->pushstring(sqvm, itr->name.GetString(), -1); - g_pSquirrel->pushstring(sqvm, itr->value.GetString(), -1); - - g_pSquirrel->newslot(sqvm, -3, false); - break; - case rapidjson::kTrueType: - case rapidjson::kFalseType: - g_pSquirrel->pushstring(sqvm, itr->name.GetString(), -1); - g_pSquirrel->pushbool(sqvm, itr->value.GetBool()); - g_pSquirrel->newslot(sqvm, -3, false); - break; - case rapidjson::kNumberType: - if (itr->value.IsDouble() || itr->value.IsFloat()) - { - g_pSquirrel->pushstring(sqvm, itr->name.GetString(), -1); - g_pSquirrel->pushfloat(sqvm, itr->value.GetFloat()); - } - else - { - g_pSquirrel->pushstring(sqvm, itr->name.GetString(), -1); - g_pSquirrel->pushinteger(sqvm, itr->value.GetInt()); - } - g_pSquirrel->newslot(sqvm, -3, false); - break; - } - } -} - -template void EncodeJSONTable( - SQTable* table, - rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>* obj, - rapidjson::MemoryPoolAllocator& allocator) -{ - for (int i = 0; i < table->_numOfNodes; i++) - { - tableNode* node = &table->_nodes[i]; - if (node->key._Type == OT_STRING) - { - rapidjson::GenericValue, rapidjson::MemoryPoolAllocator> newObj(rapidjson::kObjectType); - rapidjson::GenericValue, rapidjson::MemoryPoolAllocator> newArray(rapidjson::kArrayType); - - switch (node->val._Type) - { - case OT_STRING: - obj->AddMember( - rapidjson::StringRef(node->key._VAL.asString->_val), rapidjson::StringRef(node->val._VAL.asString->_val), allocator); - break; - case OT_INTEGER: - obj->AddMember( - rapidjson::StringRef(node->key._VAL.asString->_val), - rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>( - (int)node->val._VAL.asInteger), - allocator); - break; - case OT_FLOAT: - obj->AddMember( - rapidjson::StringRef(node->key._VAL.asString->_val), - rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>(node->val._VAL.asFloat), - allocator); - break; - case OT_BOOL: - if (node->val._VAL.asInteger) - { - obj->AddMember( - rapidjson::StringRef(node->key._VAL.asString->_val), - rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>(true), - allocator); - } - else - { - obj->AddMember( - rapidjson::StringRef(node->key._VAL.asString->_val), - rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>(false), - allocator); - } - break; - case OT_TABLE: - EncodeJSONTable(node->val._VAL.asTable, &newObj, allocator); - obj->AddMember(rapidjson::StringRef(node->key._VAL.asString->_val), newObj, allocator); - break; - case OT_ARRAY: - EncodeJSONArray(node->val._VAL.asArray, &newArray, allocator); - obj->AddMember(rapidjson::StringRef(node->key._VAL.asString->_val), newArray, allocator); - break; - default: - spdlog::warn("SQ_EncodeJSON: squirrel type {} not supported", SQTypeNameFromID(node->val._Type)); - break; - } - } - } -} - -template void EncodeJSONArray( - SQArray* arr, - rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>* obj, - rapidjson::MemoryPoolAllocator& allocator) -{ - for (int i = 0; i < arr->_usedSlots; i++) - { - SQObject* node = &arr->_values[i]; - - rapidjson::GenericValue, rapidjson::MemoryPoolAllocator> newObj(rapidjson::kObjectType); - rapidjson::GenericValue, rapidjson::MemoryPoolAllocator> newArray(rapidjson::kArrayType); - - switch (node->_Type) - { - case OT_STRING: - obj->PushBack(rapidjson::StringRef(node->_VAL.asString->_val), allocator); - break; - case OT_INTEGER: - obj->PushBack( - rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>((int)node->_VAL.asInteger), - allocator); - break; - case OT_FLOAT: - obj->PushBack( - rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>(node->_VAL.asFloat), - allocator); - break; - case OT_BOOL: - if (node->_VAL.asInteger) - obj->PushBack(rapidjson::StringRef("true"), allocator); - else - obj->PushBack(rapidjson::StringRef("false"), allocator); - break; - case OT_TABLE: - EncodeJSONTable(node->_VAL.asTable, &newObj, allocator); - obj->PushBack(newObj, allocator); - break; - case OT_ARRAY: - EncodeJSONArray(node->_VAL.asArray, &newArray, allocator); - obj->PushBack(newArray, allocator); - break; - default: - spdlog::info("SQ encode Json type {} not supported", SQTypeNameFromID(node->_Type)); - } - } -} - -ADD_SQFUNC( - "table", - DecodeJSON, - "string json, bool fatalParseErrors = false", - "converts a json string to a squirrel table", - ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER) -{ - const char* pJson = g_pSquirrel->getstring(sqvm, 1); - const bool bFatalParseErrors = g_pSquirrel->getbool(sqvm, 2); - - rapidjson_document doc; - doc.Parse(pJson); - if (doc.HasParseError()) - { - g_pSquirrel->newtable(sqvm); - - std::string sErrorString = fmt::format( - "Failed parsing json file: encountered parse error \"{}\" at offset {}", - GetParseError_En(doc.GetParseError()), - doc.GetErrorOffset()); - - if (bFatalParseErrors) - g_pSquirrel->raiseerror(sqvm, sErrorString.c_str()); - else - spdlog::warn(sErrorString); - - return SQRESULT_NOTNULL; - } - - DecodeJsonTable(sqvm, (rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>*)&doc); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC( - "string", - EncodeJSON, - "table data", - "converts a squirrel table to a json string", - ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER) -{ - rapidjson_document doc; - doc.SetObject(); - - // temp until this is just the func parameter type - HSquirrelVM* vm = (HSquirrelVM*)sqvm; - SQTable* table = vm->_stackOfCurrentFunction[1]._VAL.asTable; - EncodeJSONTable(table, &doc, doc.GetAllocator()); - - rapidjson::StringBuffer buffer; - rapidjson::Writer writer(buffer); - doc.Accept(writer); - const char* pJsonString = buffer.GetString(); - - g_pSquirrel->pushstring(sqvm, pJsonString, -1); - return SQRESULT_NOTNULL; -} diff --git a/NorthstarDLL/scriptmainmenupromos.cpp b/NorthstarDLL/scriptmainmenupromos.cpp deleted file mode 100644 index 5f7a09c1..00000000 --- a/NorthstarDLL/scriptmainmenupromos.cpp +++ /dev/null @@ -1,124 +0,0 @@ -#include "pch.h" -#include "squirrel.h" -#include "masterserver.h" - -// mirror this in script -enum eMainMenuPromoDataProperty -{ - newInfoTitle1, - newInfoTitle2, - newInfoTitle3, - - largeButtonTitle, - largeButtonText, - largeButtonUrl, - largeButtonImageIndex, - - smallButton1Title, - smallButton1Url, - smallButton1ImageIndex, - - smallButton2Title, - smallButton2Url, - smallButton2ImageIndex -}; -ADD_SQFUNC("void", NSRequestCustomMainMenuPromos, "", "", ScriptContext::UI) -{ - g_pMasterServerManager->RequestMainMenuPromos(); - return SQRESULT_NULL; -} - -ADD_SQFUNC("bool", NSHasCustomMainMenuPromoData, "", "", ScriptContext::UI) -{ - g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_bHasMainMenuPromoData); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("var", NSGetCustomMainMenuPromoData, "int promoDataKey", "", ScriptContext::UI) -{ - if (!g_pMasterServerManager->m_bHasMainMenuPromoData) - return SQRESULT_NULL; - - switch (g_pSquirrel->getinteger(sqvm, 1)) - { - case eMainMenuPromoDataProperty::newInfoTitle1: - { - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.newInfoTitle1.c_str()); - break; - } - - case eMainMenuPromoDataProperty::newInfoTitle2: - { - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.newInfoTitle2.c_str()); - break; - } - - case eMainMenuPromoDataProperty::newInfoTitle3: - { - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.newInfoTitle3.c_str()); - break; - } - - case eMainMenuPromoDataProperty::largeButtonTitle: - { - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.largeButtonTitle.c_str()); - break; - } - - case eMainMenuPromoDataProperty::largeButtonText: - { - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.largeButtonText.c_str()); - break; - } - - case eMainMenuPromoDataProperty::largeButtonUrl: - { - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.largeButtonUrl.c_str()); - break; - } - - case eMainMenuPromoDataProperty::largeButtonImageIndex: - { - g_pSquirrel->pushinteger(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.largeButtonImageIndex); - break; - } - - case eMainMenuPromoDataProperty::smallButton1Title: - { - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton1Title.c_str()); - break; - } - - case eMainMenuPromoDataProperty::smallButton1Url: - { - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton1Url.c_str()); - break; - } - - case eMainMenuPromoDataProperty::smallButton1ImageIndex: - { - g_pSquirrel->pushinteger(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton1ImageIndex); - break; - } - - case eMainMenuPromoDataProperty::smallButton2Title: - { - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton2Title.c_str()); - break; - } - - case eMainMenuPromoDataProperty::smallButton2Url: - { - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton2Url.c_str()); - break; - } - - case eMainMenuPromoDataProperty::smallButton2ImageIndex: - { - g_pSquirrel->pushinteger(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton2ImageIndex); - break; - } - } - - return SQRESULT_NOTNULL; -} \ No newline at end of file diff --git a/NorthstarDLL/scriptmodmenu.cpp b/NorthstarDLL/scriptmodmenu.cpp deleted file mode 100644 index 1dd80261..00000000 --- a/NorthstarDLL/scriptmodmenu.cpp +++ /dev/null @@ -1,166 +0,0 @@ -#include "pch.h" -#include "modmanager.h" -#include "squirrel.h" - -ADD_SQFUNC("array", NSGetModNames, "", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - g_pSquirrel->newarray(sqvm, 0); - - for (Mod& mod : g_pModManager->m_LoadedMods) - { - g_pSquirrel->pushstring(sqvm, mod.Name.c_str()); - g_pSquirrel->arrayappend(sqvm, -2); - } - - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("bool", NSIsModEnabled, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); - - // manual lookup, not super performant but eh not a big deal - for (Mod& mod : g_pModManager->m_LoadedMods) - { - if (!mod.Name.compare(modName)) - { - g_pSquirrel->pushbool(sqvm, mod.m_bEnabled); - return SQRESULT_NOTNULL; - } - } - - return SQRESULT_NULL; -} - -ADD_SQFUNC("void", NSSetModEnabled, "string modName, bool enabled", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); - const SQBool enabled = g_pSquirrel->getbool(sqvm, 2); - - // manual lookup, not super performant but eh not a big deal - for (Mod& mod : g_pModManager->m_LoadedMods) - { - if (!mod.Name.compare(modName)) - { - mod.m_bEnabled = enabled; - return SQRESULT_NULL; - } - } - - return SQRESULT_NULL; -} - -ADD_SQFUNC("string", NSGetModDescriptionByModName, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); - - // manual lookup, not super performant but eh not a big deal - for (Mod& mod : g_pModManager->m_LoadedMods) - { - if (!mod.Name.compare(modName)) - { - g_pSquirrel->pushstring(sqvm, mod.Description.c_str()); - return SQRESULT_NOTNULL; - } - } - - return SQRESULT_NULL; -} - -ADD_SQFUNC("string", NSGetModVersionByModName, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); - - // manual lookup, not super performant but eh not a big deal - for (Mod& mod : g_pModManager->m_LoadedMods) - { - if (!mod.Name.compare(modName)) - { - g_pSquirrel->pushstring(sqvm, mod.Version.c_str()); - return SQRESULT_NOTNULL; - } - } - - return SQRESULT_NULL; -} - -ADD_SQFUNC("string", NSGetModDownloadLinkByModName, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); - - // manual lookup, not super performant but eh not a big deal - for (Mod& mod : g_pModManager->m_LoadedMods) - { - if (!mod.Name.compare(modName)) - { - g_pSquirrel->pushstring(sqvm, mod.DownloadLink.c_str()); - return SQRESULT_NOTNULL; - } - } - - return SQRESULT_NULL; -} - -ADD_SQFUNC("int", NSGetModLoadPriority, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); - - // manual lookup, not super performant but eh not a big deal - for (Mod& mod : g_pModManager->m_LoadedMods) - { - if (!mod.Name.compare(modName)) - { - g_pSquirrel->pushinteger(sqvm, mod.LoadPriority); - return SQRESULT_NOTNULL; - } - } - - return SQRESULT_NULL; -} - -ADD_SQFUNC("bool", NSIsModRequiredOnClient, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); - - // manual lookup, not super performant but eh not a big deal - for (Mod& mod : g_pModManager->m_LoadedMods) - { - if (!mod.Name.compare(modName)) - { - g_pSquirrel->pushbool(sqvm, mod.RequiredOnClient); - return SQRESULT_NOTNULL; - } - } - - return SQRESULT_NULL; -} - -ADD_SQFUNC( - "array", NSGetModConvarsByModName, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) -{ - const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); - g_pSquirrel->newarray(sqvm, 0); - - // manual lookup, not super performant but eh not a big deal - for (Mod& mod : g_pModManager->m_LoadedMods) - { - if (!mod.Name.compare(modName)) - { - for (ModConVar* cvar : mod.ConVars) - { - g_pSquirrel->pushstring(sqvm, cvar->Name.c_str()); - g_pSquirrel->arrayappend(sqvm, -2); - } - - return SQRESULT_NOTNULL; - } - } - - return SQRESULT_NOTNULL; // return empty array -} - -ADD_SQFUNC("void", NSReloadMods, "", "", ScriptContext::UI) -{ - g_pModManager->LoadMods(); - return SQRESULT_NULL; -} diff --git a/NorthstarDLL/scripts/client/clientchathooks.cpp b/NorthstarDLL/scripts/client/clientchathooks.cpp new file mode 100644 index 00000000..0fc68302 --- /dev/null +++ b/NorthstarDLL/scripts/client/clientchathooks.cpp @@ -0,0 +1,70 @@ +#include "pch.h" +#include "squirrel/squirrel.h" + +#include "server/serverchathooks.h" +#include "client/localchatwriter.h" + +#include + +AUTOHOOK_INIT() + +// 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; + + int senderId = inboxId & CUSTOM_MESSAGE_INDEX_MASK; + bool isAnonymous = senderId == 0; + bool isCustom = isAnonymous || (inboxId & CUSTOM_MESSAGE_INDEX_BIT); + + // Type is set to 0 for non-custom messages, custom messages have a type encoded as the first byte + int type = 0; + const char* payload = message; + if (isCustom) + { + type = message[0]; + payload = message + 1; + } + + SQRESULT result = g_pSquirrel->Call( + "CHudChat_ProcessMessageStartThread", static_cast(senderId) - 1, payload, isTeam, isDead, type); + if (result == SQRESULT_ERROR) + for (CHudChat* hud = *CHudChat::allHuds; hud != NULL; hud = hud->next) + CHudChat__AddGameLine(hud, message, inboxId, isTeam, isDead); +} + +ADD_SQFUNC("void", NSChatWrite, "int context, string text", "", ScriptContext::CLIENT) +{ + int chatContext = g_pSquirrel->getinteger(sqvm, 1); + const char* str = g_pSquirrel->getstring(sqvm, 2); + + LocalChatWriter((LocalChatWriter::Context)chatContext).Write(str); + return SQRESULT_NULL; +} + +ADD_SQFUNC("void", NSChatWriteRaw, "int context, string text", "", ScriptContext::CLIENT) +{ + int chatContext = g_pSquirrel->getinteger(sqvm, 1); + const char* str = g_pSquirrel->getstring(sqvm, 2); + + LocalChatWriter((LocalChatWriter::Context)chatContext).InsertText(str); + return SQRESULT_NULL; +} + +ADD_SQFUNC("void", NSChatWriteLine, "int context, string text", "", ScriptContext::CLIENT) +{ + int chatContext = g_pSquirrel->getinteger(sqvm, 1); + const char* str = g_pSquirrel->getstring(sqvm, 2); + + LocalChatWriter((LocalChatWriter::Context)chatContext).WriteLine(str); + return SQRESULT_NULL; +} + +ON_DLL_LOAD_CLIENT("client.dll", ClientChatHooks, (CModule module)) +{ + AUTOHOOK_DISPATCH() +} diff --git a/NorthstarDLL/scripts/client/scriptbrowserhooks.cpp b/NorthstarDLL/scripts/client/scriptbrowserhooks.cpp new file mode 100644 index 00000000..df4014de --- /dev/null +++ b/NorthstarDLL/scripts/client/scriptbrowserhooks.cpp @@ -0,0 +1,25 @@ +#include "pch.h" + +AUTOHOOK_INIT() + +bool* bIsOriginOverlayEnabled; + +// clang-format off +AUTOHOOK(OpenExternalWebBrowser, engine.dll + 0x184E40, +void, __fastcall, (char* pUrl, char flags)) +// clang-format on +{ + bool bIsOriginOverlayEnabledOriginal = *bIsOriginOverlayEnabled; + if (flags & 2 && !strncmp(pUrl, "http", 4)) // custom force external browser flag + *bIsOriginOverlayEnabled = false; // if this bool is false, game will use an external browser rather than the origin overlay one + + OpenExternalWebBrowser(pUrl, flags); + *bIsOriginOverlayEnabled = bIsOriginOverlayEnabledOriginal; +} + +ON_DLL_LOAD_CLIENT("engine.dll", ScriptExternalBrowserHooks, (CModule module)) +{ + AUTOHOOK_DISPATCH() + + bIsOriginOverlayEnabled = module.Offset(0x13978255).As(); +} diff --git a/NorthstarDLL/scripts/client/scriptmainmenupromos.cpp b/NorthstarDLL/scripts/client/scriptmainmenupromos.cpp new file mode 100644 index 00000000..0ea167f8 --- /dev/null +++ b/NorthstarDLL/scripts/client/scriptmainmenupromos.cpp @@ -0,0 +1,124 @@ +#include "pch.h" +#include "squirrel/squirrel.h" +#include "masterserver/masterserver.h" + +// mirror this in script +enum eMainMenuPromoDataProperty +{ + newInfoTitle1, + newInfoTitle2, + newInfoTitle3, + + largeButtonTitle, + largeButtonText, + largeButtonUrl, + largeButtonImageIndex, + + smallButton1Title, + smallButton1Url, + smallButton1ImageIndex, + + smallButton2Title, + smallButton2Url, + smallButton2ImageIndex +}; +ADD_SQFUNC("void", NSRequestCustomMainMenuPromos, "", "", ScriptContext::UI) +{ + g_pMasterServerManager->RequestMainMenuPromos(); + return SQRESULT_NULL; +} + +ADD_SQFUNC("bool", NSHasCustomMainMenuPromoData, "", "", ScriptContext::UI) +{ + g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_bHasMainMenuPromoData); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("var", NSGetCustomMainMenuPromoData, "int promoDataKey", "", ScriptContext::UI) +{ + if (!g_pMasterServerManager->m_bHasMainMenuPromoData) + return SQRESULT_NULL; + + switch (g_pSquirrel->getinteger(sqvm, 1)) + { + case eMainMenuPromoDataProperty::newInfoTitle1: + { + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.newInfoTitle1.c_str()); + break; + } + + case eMainMenuPromoDataProperty::newInfoTitle2: + { + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.newInfoTitle2.c_str()); + break; + } + + case eMainMenuPromoDataProperty::newInfoTitle3: + { + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.newInfoTitle3.c_str()); + break; + } + + case eMainMenuPromoDataProperty::largeButtonTitle: + { + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.largeButtonTitle.c_str()); + break; + } + + case eMainMenuPromoDataProperty::largeButtonText: + { + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.largeButtonText.c_str()); + break; + } + + case eMainMenuPromoDataProperty::largeButtonUrl: + { + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.largeButtonUrl.c_str()); + break; + } + + case eMainMenuPromoDataProperty::largeButtonImageIndex: + { + g_pSquirrel->pushinteger(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.largeButtonImageIndex); + break; + } + + case eMainMenuPromoDataProperty::smallButton1Title: + { + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton1Title.c_str()); + break; + } + + case eMainMenuPromoDataProperty::smallButton1Url: + { + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton1Url.c_str()); + break; + } + + case eMainMenuPromoDataProperty::smallButton1ImageIndex: + { + g_pSquirrel->pushinteger(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton1ImageIndex); + break; + } + + case eMainMenuPromoDataProperty::smallButton2Title: + { + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton2Title.c_str()); + break; + } + + case eMainMenuPromoDataProperty::smallButton2Url: + { + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton2Url.c_str()); + break; + } + + case eMainMenuPromoDataProperty::smallButton2ImageIndex: + { + g_pSquirrel->pushinteger(sqvm, g_pMasterServerManager->m_sMainMenuPromoData.smallButton2ImageIndex); + break; + } + } + + return SQRESULT_NOTNULL; +} diff --git a/NorthstarDLL/scripts/client/scriptmodmenu.cpp b/NorthstarDLL/scripts/client/scriptmodmenu.cpp new file mode 100644 index 00000000..75d05acc --- /dev/null +++ b/NorthstarDLL/scripts/client/scriptmodmenu.cpp @@ -0,0 +1,166 @@ +#include "pch.h" +#include "mods/modmanager.h" +#include "squirrel/squirrel.h" + +ADD_SQFUNC("array", NSGetModNames, "", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) +{ + g_pSquirrel->newarray(sqvm, 0); + + for (Mod& mod : g_pModManager->m_LoadedMods) + { + g_pSquirrel->pushstring(sqvm, mod.Name.c_str()); + g_pSquirrel->arrayappend(sqvm, -2); + } + + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("bool", NSIsModEnabled, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) +{ + const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); + + // manual lookup, not super performant but eh not a big deal + for (Mod& mod : g_pModManager->m_LoadedMods) + { + if (!mod.Name.compare(modName)) + { + g_pSquirrel->pushbool(sqvm, mod.m_bEnabled); + return SQRESULT_NOTNULL; + } + } + + return SQRESULT_NULL; +} + +ADD_SQFUNC("void", NSSetModEnabled, "string modName, bool enabled", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) +{ + const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); + const SQBool enabled = g_pSquirrel->getbool(sqvm, 2); + + // manual lookup, not super performant but eh not a big deal + for (Mod& mod : g_pModManager->m_LoadedMods) + { + if (!mod.Name.compare(modName)) + { + mod.m_bEnabled = enabled; + return SQRESULT_NULL; + } + } + + return SQRESULT_NULL; +} + +ADD_SQFUNC("string", NSGetModDescriptionByModName, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) +{ + const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); + + // manual lookup, not super performant but eh not a big deal + for (Mod& mod : g_pModManager->m_LoadedMods) + { + if (!mod.Name.compare(modName)) + { + g_pSquirrel->pushstring(sqvm, mod.Description.c_str()); + return SQRESULT_NOTNULL; + } + } + + return SQRESULT_NULL; +} + +ADD_SQFUNC("string", NSGetModVersionByModName, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) +{ + const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); + + // manual lookup, not super performant but eh not a big deal + for (Mod& mod : g_pModManager->m_LoadedMods) + { + if (!mod.Name.compare(modName)) + { + g_pSquirrel->pushstring(sqvm, mod.Version.c_str()); + return SQRESULT_NOTNULL; + } + } + + return SQRESULT_NULL; +} + +ADD_SQFUNC("string", NSGetModDownloadLinkByModName, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) +{ + const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); + + // manual lookup, not super performant but eh not a big deal + for (Mod& mod : g_pModManager->m_LoadedMods) + { + if (!mod.Name.compare(modName)) + { + g_pSquirrel->pushstring(sqvm, mod.DownloadLink.c_str()); + return SQRESULT_NOTNULL; + } + } + + return SQRESULT_NULL; +} + +ADD_SQFUNC("int", NSGetModLoadPriority, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) +{ + const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); + + // manual lookup, not super performant but eh not a big deal + for (Mod& mod : g_pModManager->m_LoadedMods) + { + if (!mod.Name.compare(modName)) + { + g_pSquirrel->pushinteger(sqvm, mod.LoadPriority); + return SQRESULT_NOTNULL; + } + } + + return SQRESULT_NULL; +} + +ADD_SQFUNC("bool", NSIsModRequiredOnClient, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) +{ + const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); + + // manual lookup, not super performant but eh not a big deal + for (Mod& mod : g_pModManager->m_LoadedMods) + { + if (!mod.Name.compare(modName)) + { + g_pSquirrel->pushbool(sqvm, mod.RequiredOnClient); + return SQRESULT_NOTNULL; + } + } + + return SQRESULT_NULL; +} + +ADD_SQFUNC( + "array", NSGetModConvarsByModName, "string modName", "", ScriptContext::SERVER | ScriptContext::CLIENT | ScriptContext::UI) +{ + const SQChar* modName = g_pSquirrel->getstring(sqvm, 1); + g_pSquirrel->newarray(sqvm, 0); + + // manual lookup, not super performant but eh not a big deal + for (Mod& mod : g_pModManager->m_LoadedMods) + { + if (!mod.Name.compare(modName)) + { + for (ModConVar* cvar : mod.ConVars) + { + g_pSquirrel->pushstring(sqvm, cvar->Name.c_str()); + g_pSquirrel->arrayappend(sqvm, -2); + } + + return SQRESULT_NOTNULL; + } + } + + return SQRESULT_NOTNULL; // return empty array +} + +ADD_SQFUNC("void", NSReloadMods, "", "", ScriptContext::UI) +{ + g_pModManager->LoadMods(); + return SQRESULT_NULL; +} diff --git a/NorthstarDLL/scripts/client/scriptserverbrowser.cpp b/NorthstarDLL/scripts/client/scriptserverbrowser.cpp new file mode 100644 index 00000000..5f1287ad --- /dev/null +++ b/NorthstarDLL/scripts/client/scriptserverbrowser.cpp @@ -0,0 +1,410 @@ +#include "pch.h" +#include "squirrel/squirrel.h" +#include "masterserver/masterserver.h" +#include "server/auth/serverauthentication.h" +#include "engine/r2engine.h" +#include "client/r2client.h" + +// functions for viewing server browser + +ADD_SQFUNC("bool", NSIsMasterServerAuthenticated, "", "", ScriptContext::UI) +{ + g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_bOriginAuthWithMasterServerDone); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("void", NSRequestServerList, "", "", ScriptContext::UI) +{ + g_pMasterServerManager->RequestServerList(); + return SQRESULT_NULL; +} + +ADD_SQFUNC("bool", NSIsRequestingServerList, "", "", ScriptContext::UI) +{ + g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_bScriptRequestingServerList); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("bool", NSMasterServerConnectionSuccessful, "", "", ScriptContext::UI) +{ + g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_bSuccessfullyConnected); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("int", NSGetServerCount, "", "", ScriptContext::UI) +{ + g_pSquirrel->pushinteger(sqvm, g_pMasterServerManager->m_vRemoteServers.size()); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("string", NSGetServerName, "int serverIndex", "", ScriptContext::UI) +{ + SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); + + if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "Tried to get name of server index {} when only {} servers are available", + serverIndex, + g_pMasterServerManager->m_vRemoteServers.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].name); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("string", NSGetServerDescription, "int serverIndex", "", ScriptContext::UI) +{ + SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); + + if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "Tried to get description of server index {} when only {} servers are available", + serverIndex, + g_pMasterServerManager->m_vRemoteServers.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].description.c_str()); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("string", NSGetServerMap, "int serverIndex", "", ScriptContext::UI) +{ + SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); + + if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "Tried to get map of server index {} when only {} servers are available", + serverIndex, + g_pMasterServerManager->m_vRemoteServers.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].map); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("string", NSGetServerPlaylist, "int serverIndex", "", ScriptContext::UI) +{ + SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); + + if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "Tried to get playlist of server index {} when only {} servers are available", + serverIndex, + g_pMasterServerManager->m_vRemoteServers.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].playlist); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("int", NSGetServerPlayerCount, "int serverIndex", "", ScriptContext::UI) +{ + SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); + + if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "Tried to get playercount of server index {} when only {} servers are available", + serverIndex, + g_pMasterServerManager->m_vRemoteServers.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushinteger(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].playerCount); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("int", NSGetServerMaxPlayerCount, "int serverIndex", "", ScriptContext::UI) +{ + SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); + + if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "Tried to get max playercount of server index {} when only {} servers are available", + serverIndex, + g_pMasterServerManager->m_vRemoteServers.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushinteger(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].maxPlayers); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("string", NSGetServerID, "int serverIndex", "", ScriptContext::UI) +{ + SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); + + if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "Tried to get id of server index {} when only {} servers are available", + serverIndex, + g_pMasterServerManager->m_vRemoteServers.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].id); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("bool", NSServerRequiresPassword, "int serverIndex", "", ScriptContext::UI) +{ + SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); + + if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "Tried to get hasPassword of server index {} when only {} servers are available", + serverIndex, + g_pMasterServerManager->m_vRemoteServers.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].requiresPassword); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("int", NSGetServerRequiredModsCount, "int serverIndex", "", ScriptContext::UI) +{ + SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); + + if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "Tried to get required mods count of server index {} when only {} servers are available", + serverIndex, + g_pMasterServerManager->m_vRemoteServers.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushinteger(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods.size()); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("string", NSGetServerRegion, "int serverIndex", "", ScriptContext::UI) +{ + SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); + + if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "Tried to get region of server index {} when only {} servers are available", + serverIndex, + g_pMasterServerManager->m_vRemoteServers.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].region, -1); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("string", NSGetServerRequiredModName, "int serverIndex, int modIndex", "", ScriptContext::UI) +{ + SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); + SQInteger modIndex = g_pSquirrel->getinteger(sqvm, 2); + + if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "Tried to get hasPassword of server index {} when only {} servers are available", + serverIndex, + g_pMasterServerManager->m_vRemoteServers.size()) + .c_str()); + return SQRESULT_ERROR; + } + + if (modIndex >= g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods.size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "Tried to get required mod name of mod index {} when only {} mod are available", + modIndex, + g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods[modIndex].Name.c_str()); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("string", NSGetServerRequiredModVersion, "int serverIndex, int modIndex", "", ScriptContext::UI) +{ + SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); + SQInteger modIndex = g_pSquirrel->getinteger(sqvm, 2); + + if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "Tried to get required mod version of server index {} when only {} servers are available", + serverIndex, + g_pMasterServerManager->m_vRemoteServers.size()) + .c_str()); + return SQRESULT_ERROR; + } + + if (modIndex >= g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods.size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "Tried to get required mod version of mod index {} when only {} mod are available", + modIndex, + g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods[modIndex].Version.c_str()); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("void", NSClearRecievedServerList, "", "", ScriptContext::UI) +{ + g_pMasterServerManager->ClearServerList(); + return SQRESULT_NULL; +} + +// functions for authenticating with servers + +ADD_SQFUNC("void", NSTryAuthWithServer, "int serverIndex, string password = ''", "", ScriptContext::UI) +{ + SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); + const SQChar* password = g_pSquirrel->getstring(sqvm, 2); + + if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "Tried to auth with server index {} when only {} servers are available", + serverIndex, + g_pMasterServerManager->m_vRemoteServers.size()) + .c_str()); + return SQRESULT_ERROR; + } + + // send off persistent data first, don't worry about server/client stuff, since m_additionalPlayerData should only have entries when + // we're a local server note: this seems like it could create a race condition, test later + for (auto& pair : g_pServerAuthentication->m_PlayerAuthenticationData) + g_pServerAuthentication->WritePersistentData(pair.first); + + // do auth + g_pMasterServerManager->AuthenticateWithServer( + R2::g_pLocalPlayerUserID, + g_pMasterServerManager->m_sOwnClientAuthToken, + g_pMasterServerManager->m_vRemoteServers[serverIndex].id, + (char*)password); + + return SQRESULT_NULL; +} + +ADD_SQFUNC("bool", NSIsAuthenticatingWithServer, "", "", ScriptContext::UI) +{ + g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_bScriptAuthenticatingWithGameServer); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("bool", NSWasAuthSuccessful, "", "", ScriptContext::UI) +{ + g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_bSuccessfullyAuthenticatedWithGameServer); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("void", NSConnectToAuthedServer, "", "", ScriptContext::UI) +{ + if (!g_pMasterServerManager->m_bHasPendingConnectionInfo) + { + g_pSquirrel->raiseerror( + sqvm, fmt::format("Tried to connect to authed server before any pending connection info was available").c_str()); + return SQRESULT_ERROR; + } + + RemoteServerConnectionInfo& info = g_pMasterServerManager->m_pendingConnectionInfo; + + // set auth token, then try to connect + // i'm honestly not entirely sure how silentconnect works regarding ports and encryption so using connect for now + R2::g_pCVar->FindVar("serverfilter")->SetValue(info.authToken); + R2::Cbuf_AddText( + R2::Cbuf_GetCurrentPlayer(), + fmt::format( + "connect {}.{}.{}.{}:{}", + info.ip.S_un.S_un_b.s_b1, + info.ip.S_un.S_un_b.s_b2, + info.ip.S_un.S_un_b.s_b3, + info.ip.S_un.S_un_b.s_b4, + info.port) + .c_str(), + R2::cmd_source_t::kCommandSrcCode); + + g_pMasterServerManager->m_bHasPendingConnectionInfo = false; + return SQRESULT_NULL; +} + +ADD_SQFUNC("void", NSTryAuthWithLocalServer, "", "", ScriptContext::UI) +{ + // do auth request + g_pMasterServerManager->AuthenticateWithOwnServer(R2::g_pLocalPlayerUserID, g_pMasterServerManager->m_sOwnClientAuthToken); + + return SQRESULT_NULL; +} + +ADD_SQFUNC("void", NSCompleteAuthWithLocalServer, "", "", ScriptContext::UI) +{ + // literally just set serverfilter + // note: this assumes we have no authdata other than our own + if (g_pServerAuthentication->m_RemoteAuthenticationData.size()) + R2::g_pCVar->FindVar("serverfilter")->SetValue(g_pServerAuthentication->m_RemoteAuthenticationData.begin()->first.c_str()); + + return SQRESULT_NULL; +} + +ADD_SQFUNC("string", NSGetAuthFailReason, "", "", ScriptContext::UI) +{ + g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sAuthFailureReason.c_str(), -1); + return SQRESULT_NOTNULL; +} diff --git a/NorthstarDLL/scripts/client/scriptservertoclientstringcommand.cpp b/NorthstarDLL/scripts/client/scriptservertoclientstringcommand.cpp new file mode 100644 index 00000000..f3cb2f18 --- /dev/null +++ b/NorthstarDLL/scripts/client/scriptservertoclientstringcommand.cpp @@ -0,0 +1,19 @@ +#include "pch.h" +#include "squirrel/squirrel.h" +#include "core/convar/convar.h" +#include "core/convar/concommand.h" + +void ConCommand_ns_script_servertoclientstringcommand(const CCommand& arg) +{ + if (g_pSquirrel->m_pSQVM) + g_pSquirrel->Call("NSClientCodeCallback_RecievedServerToClientStringCommand", arg.ArgS()); +} + +ON_DLL_LOAD_CLIENT_RELIESON("client.dll", ScriptServerToClientStringCommand, ClientSquirrel, (CModule module)) +{ + RegisterConCommand( + "ns_script_servertoclientstringcommand", + ConCommand_ns_script_servertoclientstringcommand, + "", + FCVAR_CLIENTDLL | FCVAR_SERVER_CAN_EXECUTE); +} diff --git a/NorthstarDLL/scripts/scriptdatatables.cpp b/NorthstarDLL/scripts/scriptdatatables.cpp new file mode 100644 index 00000000..915d4df0 --- /dev/null +++ b/NorthstarDLL/scripts/scriptdatatables.cpp @@ -0,0 +1,910 @@ +#include "pch.h" +#include "squirrel/squirrel.h" +#include "core/filesystem/rpakfilesystem.h" +#include "core/convar/convar.h" +#include "dedicated/dedicated.h" +#include "core/filesystem/filesystem.h" +#include "core/math/vector.h" +#include "core/tier0.h" +#include "engine/r2engine.h" +#include +#include +#include +#include +#include + +const uint64_t USERDATA_TYPE_DATATABLE = 0xFFF7FFF700000004; +const uint64_t USERDATA_TYPE_DATATABLE_CUSTOM = 0xFFFCFFFC12345678; + +enum class DatatableType : int +{ + BOOL = 0, + INT, + FLOAT, + VECTOR, + STRING, + ASSET, + UNK_STRING // unknown but deffo a string type +}; + +struct ColumnInfo +{ + char* name; + DatatableType type; + int offset; +}; + +struct Datatable +{ + int numColumns; + int numRows; + ColumnInfo* columnInfo; + char* data; // actually data pointer + int rowInfo; +}; + +ConVar* Cvar_ns_prefer_datatable_from_disk; + +template Datatable* (*SQ_GetDatatableInternal)(HSquirrelVM* sqvm); + +struct CSVData +{ + std::string m_sAssetName; + std::string m_sCSVString; + char* m_pDataBuf; + size_t m_nDataBufSize; + + std::vector columns; + std::vector> dataPointers; +}; + +std::unordered_map CSVCache; + +Vector3 StringToVector(char* pString) +{ + Vector3 vRet; + + int length = 0; + while (pString[length]) + { + if ((pString[length] == '<') || (pString[length] == '>')) + pString[length] = '\0'; + length++; + } + + int startOfFloat = 1; + int currentIndex = 1; + + while (pString[currentIndex] && (pString[currentIndex] != ',')) + currentIndex++; + pString[currentIndex] = '\0'; + vRet.x = std::stof(&pString[startOfFloat]); + startOfFloat = ++currentIndex; + + while (pString[currentIndex] && (pString[currentIndex] != ',')) + currentIndex++; + pString[currentIndex] = '\0'; + vRet.y = std::stof(&pString[startOfFloat]); + startOfFloat = ++currentIndex; + + while (pString[currentIndex] && (pString[currentIndex] != ',')) + currentIndex++; + pString[currentIndex] = '\0'; + vRet.z = std::stof(&pString[startOfFloat]); + startOfFloat = ++currentIndex; + + return vRet; +} + +// var function GetDataTable( asset path ) +REPLACE_SQFUNC(GetDataTable, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + const char* pAssetName; + g_pSquirrel->getasset(sqvm, 2, &pAssetName); + + if (strncmp(pAssetName, "datatable/", 10)) + { + g_pSquirrel->raiseerror(sqvm, fmt::format("Asset \"{}\" doesn't start with \"datatable/\"", pAssetName).c_str()); + return SQRESULT_ERROR; + } + else if (!Cvar_ns_prefer_datatable_from_disk->GetBool() && g_pPakLoadManager->LoadFile(pAssetName)) + return g_pSquirrel->m_funcOriginals["GetDataTable"](sqvm); + // either we prefer disk datatables, or we're loading a datatable that wasn't found in rpak + else + { + std::string sAssetPath(fmt::format("scripts/{}", pAssetName)); + + // first, check the cache + if (CSVCache.find(pAssetName) != CSVCache.end()) + { + CSVData** pUserdata = g_pSquirrel->createuserdata(sqvm, sizeof(CSVData*)); + g_pSquirrel->setuserdatatypeid(sqvm, -1, USERDATA_TYPE_DATATABLE_CUSTOM); + *pUserdata = &CSVCache[pAssetName]; + + return SQRESULT_NOTNULL; + } + + // check files on disk + // we don't use .rpak as the extension for on-disk datatables, so we need to replace .rpak with .csv in the filename we're reading + fs::path diskAssetPath("scripts"); + if (fs::path(pAssetName).extension() == ".rpak") + diskAssetPath /= fs::path(pAssetName).remove_filename() / (fs::path(pAssetName).stem().string() + ".csv"); + else + diskAssetPath /= fs::path(pAssetName); + + std::string sDiskAssetPath(diskAssetPath.string()); + if ((*R2::g_pFilesystem)->m_vtable2->FileExists(&(*R2::g_pFilesystem)->m_vtable2, sDiskAssetPath.c_str(), "GAME")) + { + std::string sTableCSV = R2::ReadVPKFile(sDiskAssetPath.c_str()); + if (!sTableCSV.size()) + { + g_pSquirrel->raiseerror(sqvm, fmt::format("Datatable \"{}\" is empty", pAssetName).c_str()); + return SQRESULT_ERROR; + } + + // somewhat shit, but ensure we end with a newline to make parsing easier + if (sTableCSV[sTableCSV.length() - 1] != '\n') + sTableCSV += '\n'; + + CSVData csv; + csv.m_sAssetName = pAssetName; + csv.m_sCSVString = sTableCSV; + csv.m_nDataBufSize = sTableCSV.size(); + csv.m_pDataBuf = new char[csv.m_nDataBufSize]; + memcpy(csv.m_pDataBuf, &sTableCSV[0], csv.m_nDataBufSize); + + // parse the csv + // csvs are essentially comma and newline-deliniated sets of strings for parsing, only thing we need to worry about is quoted + // entries when we parse an element of the csv, rather than allocating an entry for it, we just convert that element to a + // null-terminated string i.e., store the ptr to the first char of it, then make the comma that delinates it a nullchar + + bool bHasColumns = false; + bool bInQuotes = false; + + std::vector vCurrentRow; + char* pElemStart = csv.m_pDataBuf; + char* pElemEnd = nullptr; + + for (int i = 0; i < csv.m_nDataBufSize; i++) + { + if (csv.m_pDataBuf[i] == '\r' && csv.m_pDataBuf[i + 1] == '\n') + { + if (!pElemEnd) + pElemEnd = csv.m_pDataBuf + i; + + continue; // next iteration can handle the \n + } + + // newline, end of a row + if (csv.m_pDataBuf[i] == '\n') + { + // shouldn't have newline in string + if (bInQuotes) + { + g_pSquirrel->raiseerror(sqvm, "Unexpected \\n in string"); + return SQRESULT_ERROR; + } + + // push last entry to current row + if (pElemEnd) + *pElemEnd = '\0'; + else + csv.m_pDataBuf[i] = '\0'; + + vCurrentRow.push_back(pElemStart); + + // newline, push last line to csv data and go from there + if (!bHasColumns) + { + bHasColumns = true; + csv.columns = vCurrentRow; + } + else + csv.dataPointers.push_back(vCurrentRow); + + vCurrentRow.clear(); + // put start of current element at char after newline + pElemStart = csv.m_pDataBuf + i + 1; + pElemEnd = nullptr; + } + // we're starting or ending a quoted string + else if (csv.m_pDataBuf[i] == '"') + { + // start quoted string + if (!bInQuotes) + { + // shouldn't have quoted strings in column names + if (!bHasColumns) + { + g_pSquirrel->raiseerror(sqvm, "Unexpected \" in column name"); + return SQRESULT_ERROR; + } + + bInQuotes = true; + // put start of current element at char after string begin + pElemStart = csv.m_pDataBuf + i + 1; + } + // end quoted string + else + { + pElemEnd = csv.m_pDataBuf + i; + bInQuotes = false; + } + } + // don't parse commas in quotes + else if (bInQuotes) + { + continue; + } + // comma, push new entry to current row + else if (csv.m_pDataBuf[i] == ',') + { + if (pElemEnd) + *pElemEnd = '\0'; + else + csv.m_pDataBuf[i] = '\0'; + + vCurrentRow.push_back(pElemStart); + // put start of next element at char after comma + pElemStart = csv.m_pDataBuf + i + 1; + pElemEnd = nullptr; + } + } + + // add to cache and return + CSVData** pUserdata = g_pSquirrel->createuserdata(sqvm, sizeof(CSVData*)); + g_pSquirrel->setuserdatatypeid(sqvm, -1, USERDATA_TYPE_DATATABLE_CUSTOM); + CSVCache[pAssetName] = csv; + *pUserdata = &CSVCache[pAssetName]; + + return SQRESULT_NOTNULL; + } + // the file doesn't exist on disk, check rpak if we haven't already + else if (Cvar_ns_prefer_datatable_from_disk->GetBool() && g_pPakLoadManager->LoadFile(pAssetName)) + return g_pSquirrel->m_funcOriginals["GetDataTable"](sqvm); + // the file doesn't exist at all, error + else + { + g_pSquirrel->raiseerror(sqvm, fmt::format("Datatable {} not found", pAssetName).c_str()); + return SQRESULT_ERROR; + } + } +} + +// int function GetDataTableColumnByName( var datatable, string columnName ) +REPLACE_SQFUNC(GetDataTableColumnByName, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableColumnByName"](sqvm); + + CSVData* csv = *pData; + const char* pColumnName = g_pSquirrel->getstring(sqvm, 2); + + for (int i = 0; i < csv->columns.size(); i++) + { + if (!strcmp(csv->columns[i], pColumnName)) + { + g_pSquirrel->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + // column not found + g_pSquirrel->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowCount( var datatable ) +REPLACE_SQFUNC(GetDataTableRowCount, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDatatableRowCount"](sqvm); + + CSVData* csv = *pData; + g_pSquirrel->pushinteger(sqvm, csv->dataPointers.size()); + return SQRESULT_NOTNULL; +} + +// string function GetDataTableString( var datatable, int row, int col ) +REPLACE_SQFUNC(GetDataTableString, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableString"](sqvm); + + CSVData* csv = *pData; + const int nRow = g_pSquirrel->getinteger(sqvm, 2); + const int nCol = g_pSquirrel->getinteger(sqvm, 3); + if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushstring(sqvm, csv->dataPointers[nRow][nCol], -1); + return SQRESULT_NOTNULL; +} + +// asset function GetDataTableAsset( var datatable, int row, int col ) +REPLACE_SQFUNC(GetDataTableAsset, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableAsset"](sqvm); + + CSVData* csv = *pData; + const int nRow = g_pSquirrel->getinteger(sqvm, 2); + const int nCol = g_pSquirrel->getinteger(sqvm, 3); + if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushasset(sqvm, csv->dataPointers[nRow][nCol], -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableInt( var datatable, int row, int col ) +REPLACE_SQFUNC(GetDataTableInt, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableInt"](sqvm); + + CSVData* csv = *pData; + const int nRow = g_pSquirrel->getinteger(sqvm, 2); + const int nCol = g_pSquirrel->getinteger(sqvm, 3); + if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushinteger(sqvm, std::stoi(csv->dataPointers[nRow][nCol])); + return SQRESULT_NOTNULL; +} + +// float function GetDataTableFloat( var datatable, int row, int col ) +REPLACE_SQFUNC(GetDataTableFloat, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableFloat"](sqvm); + + CSVData* csv = *pData; + const int nRow = g_pSquirrel->getinteger(sqvm, 2); + const int nCol = g_pSquirrel->getinteger(sqvm, 3); + if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushfloat(sqvm, std::stof(csv->dataPointers[nRow][nCol])); + return SQRESULT_NOTNULL; +} + +// bool function GetDataTableBool( var datatable, int row, int col ) +REPLACE_SQFUNC(GetDataTableBool, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableBool"](sqvm); + + CSVData* csv = *pData; + const int nRow = g_pSquirrel->getinteger(sqvm, 2); + const int nCol = g_pSquirrel->getinteger(sqvm, 3); + if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushbool(sqvm, std::stoi(csv->dataPointers[nRow][nCol])); + return SQRESULT_NOTNULL; +} + +// vector function GetDataTableVector( var datatable, int row, int col ) +REPLACE_SQFUNC(GetDataTableVector, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableVector"](sqvm); + + CSVData* csv = *pData; + const int nRow = g_pSquirrel->getinteger(sqvm, 2); + const int nCol = g_pSquirrel->getinteger(sqvm, 3); + if (nRow >= csv->dataPointers.size() || nCol >= csv->dataPointers[nRow].size()) + { + g_pSquirrel->raiseerror( + sqvm, + fmt::format( + "row {} and col {} are outside of range row {} and col {}", nRow, nCol, csv->dataPointers.size(), csv->columns.size()) + .c_str()); + return SQRESULT_ERROR; + } + + g_pSquirrel->pushvector(sqvm, StringToVector(csv->dataPointers[nRow][nCol])); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowMatchingStringValue( var datatable, int col, string value ) +REPLACE_SQFUNC(GetDataTableRowMatchingStringValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableRowMatchingStringValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel->getinteger(sqvm, 2); + const char* pStringVal = g_pSquirrel->getstring(sqvm, 3); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (!strcmp(csv->dataPointers[i][nCol], pStringVal)) + { + g_pSquirrel->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowMatchingAssetValue( var datatable, int col, asset value ) +REPLACE_SQFUNC(GetDataTableMatchingAssetValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableRowMatchingAssetValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel->getinteger(sqvm, 2); + const char* pStringVal; + g_pSquirrel->getasset(sqvm, 3, &pStringVal); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (!strcmp(csv->dataPointers[i][nCol], pStringVal)) + { + g_pSquirrel->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowMatchingFloatValue( var datatable, int col, float value ) +REPLACE_SQFUNC(GetDataTableRowMatchingFloatValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableRowMatchingFloatValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel->getinteger(sqvm, 2); + const float flFloatVal = g_pSquirrel->getfloat(sqvm, 3); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (flFloatVal == std::stof(csv->dataPointers[i][nCol])) + { + g_pSquirrel->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowMatchingIntValue( var datatable, int col, int value ) +REPLACE_SQFUNC(GetDataTableRowMatchingIntValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableRowMatchingIntValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel->getinteger(sqvm, 2); + const int nIntVal = g_pSquirrel->getinteger(sqvm, 3); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (nIntVal == std::stoi(csv->dataPointers[i][nCol])) + { + g_pSquirrel->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowMatchingVectorValue( var datatable, int col, vector value ) +REPLACE_SQFUNC(GetDataTableRowMatchingVectorValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableRowMatchingVectorValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel->getinteger(sqvm, 2); + const Vector3 vVectorVal = g_pSquirrel->getvector(sqvm, 3); + + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (vVectorVal == StringToVector(csv->dataPointers[i][nCol])) + { + g_pSquirrel->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowGreaterThanOrEqualToIntValue( var datatable, int col, int value ) +REPLACE_SQFUNC(GetDataTableRowGreaterThanOrEqualToIntValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableRowGreaterThanOrEqualToIntValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel->getinteger(sqvm, 2); + const int nIntVal = g_pSquirrel->getinteger(sqvm, 3); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (nIntVal >= std::stoi(csv->dataPointers[i][nCol])) + { + spdlog::info("datatable not loaded"); + g_pSquirrel->pushinteger(sqvm, 1); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowLessThanOrEqualToIntValue( var datatable, int col, int value ) +REPLACE_SQFUNC(GetDataTableRowLessThanOrEqualToIntValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableRowLessThanOrEqualToIntValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel->getinteger(sqvm, 2); + const int nIntVal = g_pSquirrel->getinteger(sqvm, 3); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (nIntVal <= std::stoi(csv->dataPointers[i][nCol])) + { + g_pSquirrel->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowGreaterThanOrEqualToFloatValue( var datatable, int col, float value ) +REPLACE_SQFUNC(GetDataTableRowGreaterThanOrEqualToFloatValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableRowGreaterThanOrEqualToFloatValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel->getinteger(sqvm, 2); + const float flFloatVal = g_pSquirrel->getfloat(sqvm, 3); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (flFloatVal >= std::stof(csv->dataPointers[i][nCol])) + { + g_pSquirrel->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +// int function GetDataTableRowLessThanOrEqualToFloatValue( var datatable, int col, float value ) +REPLACE_SQFUNC(GetDataTableRowLessThanOrEqualToFloatValue, (ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER)) +{ + CSVData** pData; + uint64_t typeId; + g_pSquirrel->getuserdata(sqvm, 2, &pData, &typeId); + + if (typeId != USERDATA_TYPE_DATATABLE_CUSTOM) + return g_pSquirrel->m_funcOriginals["GetDataTableRowLessThanOrEqualToFloatValue"](sqvm); + + CSVData* csv = *pData; + int nCol = g_pSquirrel->getinteger(sqvm, 2); + const float flFloatVal = g_pSquirrel->getfloat(sqvm, 3); + for (int i = 0; i < csv->dataPointers.size(); i++) + { + if (flFloatVal <= std::stof(csv->dataPointers[i][nCol])) + { + g_pSquirrel->pushinteger(sqvm, i); + return SQRESULT_NOTNULL; + } + } + + g_pSquirrel->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; +} + +std::string DataTableToString(Datatable* datatable) +{ + std::string sCSVString; + + // write columns + bool bShouldComma = false; + for (int i = 0; i < datatable->numColumns; i++) + { + if (bShouldComma) + sCSVString += ','; + else + bShouldComma = true; + + sCSVString += datatable->columnInfo[i].name; + } + + // write rows + for (int row = 0; row < datatable->numRows; row++) + { + sCSVString += '\n'; + + bool bShouldComma = false; + for (int col = 0; col < datatable->numColumns; col++) + { + if (bShouldComma) + sCSVString += ','; + else + bShouldComma = true; + + // output typed data + ColumnInfo column = datatable->columnInfo[col]; + const void* pUntypedVal = datatable->data + column.offset + row * datatable->rowInfo; + switch (column.type) + { + case DatatableType::BOOL: + { + sCSVString += *(bool*)pUntypedVal ? '1' : '0'; + break; + } + + case DatatableType::INT: + { + sCSVString += std::to_string(*(int*)pUntypedVal); + break; + } + + case DatatableType::FLOAT: + { + sCSVString += std::to_string(*(float*)pUntypedVal); + break; + } + + case DatatableType::VECTOR: + { + Vector3 pVector((float*)pUntypedVal); + sCSVString += fmt::format("<{},{},{}>", pVector.x, pVector.y, pVector.z); + break; + } + + case DatatableType::STRING: + case DatatableType::ASSET: + case DatatableType::UNK_STRING: + { + sCSVString += fmt::format("\"{}\"", *(char**)pUntypedVal); + break; + } + } + } + } + + return sCSVString; +} + +void DumpDatatable(const char* pDatatablePath) +{ + Datatable* pDatatable = (Datatable*)g_pPakLoadManager->LoadFile(pDatatablePath); + if (!pDatatable) + { + spdlog::error("couldn't load datatable {} (rpak containing it may not be loaded?)", pDatatablePath); + return; + } + + std::string sOutputPath(fmt::format("{}/scripts/datatable/{}.csv", R2::g_pModName, fs::path(pDatatablePath).stem().string())); + std::string sDatatableContents(DataTableToString(pDatatable)); + + fs::create_directories(fs::path(sOutputPath).remove_filename()); + std::ofstream outputStream(sOutputPath); + outputStream.write(sDatatableContents.c_str(), sDatatableContents.size()); + outputStream.close(); + + spdlog::info("dumped datatable {} {} to {}", pDatatablePath, (void*)pDatatable, sOutputPath); +} + +void ConCommand_dump_datatable(const CCommand& args) +{ + if (args.ArgC() < 2) + { + spdlog::info("usage: dump_datatable datatable/tablename.rpak"); + return; + } + + DumpDatatable(args.Arg(1)); +} + +void ConCommand_dump_datatables(const CCommand& args) +{ + // likely not a comprehensive list, might be missing a couple? + static const std::vector VANILLA_DATATABLE_PATHS = { + "datatable/burn_meter_rewards.rpak", + "datatable/burn_meter_store.rpak", + "datatable/calling_cards.rpak", + "datatable/callsign_icons.rpak", + "datatable/camo_skins.rpak", + "datatable/default_pilot_loadouts.rpak", + "datatable/default_titan_loadouts.rpak", + "datatable/faction_leaders.rpak", + "datatable/fd_awards.rpak", + "datatable/features_mp.rpak", + "datatable/non_loadout_weapons.rpak", + "datatable/pilot_abilities.rpak", + "datatable/pilot_executions.rpak", + "datatable/pilot_passives.rpak", + "datatable/pilot_properties.rpak", + "datatable/pilot_weapons.rpak", + "datatable/pilot_weapon_features.rpak", + "datatable/pilot_weapon_mods.rpak", + "datatable/pilot_weapon_mods_common.rpak", + "datatable/playlist_items.rpak", + "datatable/titans_mp.rpak", + "datatable/titan_abilities.rpak", + "datatable/titan_executions.rpak", + "datatable/titan_fd_upgrades.rpak", + "datatable/titan_nose_art.rpak", + "datatable/titan_passives.rpak", + "datatable/titan_primary_mods.rpak", + "datatable/titan_primary_mods_common.rpak", + "datatable/titan_primary_weapons.rpak", + "datatable/titan_properties.rpak", + "datatable/titan_skins.rpak", + "datatable/titan_voices.rpak", + "datatable/unlocks_faction_level.rpak", + "datatable/unlocks_fd_titan_level.rpak", + "datatable/unlocks_player_level.rpak", + "datatable/unlocks_random.rpak", + "datatable/unlocks_titan_level.rpak", + "datatable/unlocks_weapon_level_pilot.rpak", + "datatable/weapon_skins.rpak", + "datatable/xp_per_faction_level.rpak", + "datatable/xp_per_fd_titan_level.rpak", + "datatable/xp_per_player_level.rpak", + "datatable/xp_per_titan_level.rpak", + "datatable/xp_per_weapon_level.rpak", + "datatable/faction_leaders_dropship_anims.rpak", + "datatable/score_events.rpak", + "datatable/startpoints.rpak", + "datatable/sp_levels.rpak", + "datatable/community_entries.rpak", + "datatable/spotlight_images.rpak", + "datatable/death_hints_mp.rpak", + "datatable/flightpath_assets.rpak", + "datatable/earn_meter_mp.rpak", + "datatable/battle_chatter_voices.rpak", + "datatable/battle_chatter.rpak", + "datatable/titan_os_conversations.rpak", + "datatable/faction_dialogue.rpak", + "datatable/grunt_chatter_mp.rpak", + "datatable/spectre_chatter_mp.rpak", + "datatable/pain_death_sounds.rpak", + "datatable/caller_ids_mp.rpak"}; + + for (const char* datatable : VANILLA_DATATABLE_PATHS) + DumpDatatable(datatable); +} + +ON_DLL_LOAD_RELIESON("server.dll", ServerScriptDatatables, ServerSquirrel, (CModule module)) +{ + SQ_GetDatatableInternal = module.Offset(0x1250f0).As(); +} + +ON_DLL_LOAD_RELIESON("client.dll", ClientScriptDatatables, ClientSquirrel, (CModule module)) +{ + SQ_GetDatatableInternal = module.Offset(0x1C9070).As(); + SQ_GetDatatableInternal = SQ_GetDatatableInternal; +} + +ON_DLL_LOAD_RELIESON("engine.dll", SharedScriptDataTables, ConVar, (CModule module)) +{ + Cvar_ns_prefer_datatable_from_disk = new ConVar( + "ns_prefer_datatable_from_disk", + IsDedicatedServer() && Tier0::CommandLine()->CheckParm("-nopakdedi") ? "1" : "0", + FCVAR_NONE, + "whether to prefer loading datatables from disk, rather than rpak"); + + RegisterConCommand("dump_datatables", ConCommand_dump_datatables, "dumps all datatables from a hardcoded list", FCVAR_NONE); + RegisterConCommand("dump_datatable", ConCommand_dump_datatable, "dump a datatable", FCVAR_NONE); +} diff --git a/NorthstarDLL/scripts/scripthttprequesthandler.cpp b/NorthstarDLL/scripts/scripthttprequesthandler.cpp new file mode 100644 index 00000000..17ccc888 --- /dev/null +++ b/NorthstarDLL/scripts/scripthttprequesthandler.cpp @@ -0,0 +1,586 @@ +#include "pch.h" +#include "scripthttprequesthandler.h" +#include "util/version.h" +#include "squirrel/squirrel.h" +#include "core/tier0.h" + +HttpRequestHandler* g_httpRequestHandler; + +bool IsHttpDisabled() +{ + const static bool bIsHttpDisabled = Tier0::CommandLine()->FindParm("-disablehttprequests"); + return bIsHttpDisabled; +} + +bool IsLocalHttpAllowed() +{ + const static bool bIsLocalHttpAllowed = Tier0::CommandLine()->FindParm("-allowlocalhttp"); + return bIsLocalHttpAllowed; +} + +bool DisableHttpSsl() +{ + const static bool bDisableHttpSsl = Tier0::CommandLine()->FindParm("-disablehttpssl"); + return bDisableHttpSsl; +} + +HttpRequestHandler::HttpRequestHandler() +{ + // Cache the launch parameters as early as possible in order to avoid possible exploits that change them at runtime. + IsHttpDisabled(); + IsLocalHttpAllowed(); + DisableHttpSsl(); +} + +void HttpRequestHandler::StartHttpRequestHandler() +{ + if (IsRunning()) + { + spdlog::warn("%s was called while IsRunning() is true!", __FUNCTION__); + return; + } + + m_bIsHttpRequestHandlerRunning = true; + spdlog::info("HttpRequestHandler started."); +} + +void HttpRequestHandler::StopHttpRequestHandler() +{ + if (!IsRunning()) + { + spdlog::warn("%s was called while IsRunning() is false", __FUNCTION__); + return; + } + + m_bIsHttpRequestHandlerRunning = false; + spdlog::info("HttpRequestHandler stopped."); +} + +bool IsHttpDestinationHostAllowed(const std::string& host, std::string& outHostname, std::string& outAddress, std::string& outPort) +{ + CURLU* url = curl_url(); + if (!url) + { + spdlog::error("Failed to call curl_url() for http request."); + return false; + } + + if (curl_url_set(url, CURLUPART_URL, host.c_str(), CURLU_DEFAULT_SCHEME) != CURLUE_OK) + { + spdlog::error("Failed to parse destination URL for http request."); + + curl_url_cleanup(url); + return false; + } + + char* urlHostname = nullptr; + if (curl_url_get(url, CURLUPART_HOST, &urlHostname, 0) != CURLUE_OK) + { + spdlog::error("Failed to parse hostname from destination URL for http request."); + + curl_url_cleanup(url); + return false; + } + + char* urlScheme = nullptr; + if (curl_url_get(url, CURLUPART_SCHEME, &urlScheme, CURLU_DEFAULT_SCHEME) != CURLUE_OK) + { + spdlog::error("Failed to parse scheme from destination URL for http request."); + + curl_url_cleanup(url); + curl_free(urlHostname); + return false; + } + + char* urlPort = nullptr; + if (curl_url_get(url, CURLUPART_PORT, &urlPort, CURLU_DEFAULT_PORT) != CURLUE_OK) + { + spdlog::error("Failed to parse port from destination URL for http request."); + + curl_url_cleanup(url); + curl_free(urlHostname); + curl_free(urlScheme); + return false; + } + + // Resolve the hostname into an address. + addrinfo* result; + addrinfo hints; + std::memset(&hints, 0, sizeof(addrinfo)); + hints.ai_family = AF_UNSPEC; + + if (getaddrinfo(urlHostname, urlScheme, &hints, &result) != 0) + { + spdlog::error("Failed to resolve http request destination {} using getaddrinfo().", urlHostname); + + curl_url_cleanup(url); + curl_free(urlHostname); + curl_free(urlScheme); + curl_free(urlPort); + return false; + } + + bool bFoundIPv6 = false; + sockaddr_in* sockaddr_ipv4 = nullptr; + for (addrinfo* info = result; info; info = info->ai_next) + { + if (info->ai_family == AF_INET) + { + sockaddr_ipv4 = (sockaddr_in*)info->ai_addr; + break; + } + + bFoundIPv6 = bFoundIPv6 || info->ai_family == AF_INET6; + } + + if (sockaddr_ipv4 == nullptr) + { + if (bFoundIPv6) + { + spdlog::error("Only IPv4 destinations are supported for HTTP requests. To allow IPv6, launch the game using -allowlocalhttp."); + } + else + { + spdlog::error("Failed to resolve http request destination {} into a valid IPv4 address.", urlHostname); + } + + curl_free(urlHostname); + curl_free(urlScheme); + curl_free(urlPort); + curl_url_cleanup(url); + + return false; + } + + // Fast checks for private ranges of IPv4. + // clang-format off + { + auto addrBytes = sockaddr_ipv4->sin_addr.S_un.S_un_b; + + if (addrBytes.s_b1 == 10 // 10.0.0.0 - 10.255.255.255 (Class A Private) + || addrBytes.s_b1 == 172 && addrBytes.s_b2 >= 16 && addrBytes.s_b2 <= 31 // 172.16.0.0 - 172.31.255.255 (Class B Private) + || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 168 // 192.168.0.0 - 192.168.255.255 (Class C Private) + || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 0 && addrBytes.s_b3 == 0 // 192.0.0.0 - 192.0.0.255 (IETF Assignment) + || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 0 && addrBytes.s_b3 == 2 // 192.0.2.0 - 192.0.2.255 (TEST-NET-1) + || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 88 && addrBytes.s_b3 == 99 // 192.88.99.0 - 192.88.99.255 (IPv4-IPv6 Relay) + || addrBytes.s_b1 == 192 && addrBytes.s_b2 >= 18 && addrBytes.s_b2 <= 19 // 192.18.0.0 - 192.19.255.255 (Internet Benchmark) + || addrBytes.s_b1 == 192 && addrBytes.s_b2 == 51 && addrBytes.s_b3 == 100 // 192.51.100.0 - 192.51.100.255 (TEST-NET-2) + || addrBytes.s_b1 == 203 && addrBytes.s_b2 == 0 && addrBytes.s_b3 == 113 // 203.0.113.0 - 203.0.113.255 (TEST-NET-3) + || addrBytes.s_b1 == 169 && addrBytes.s_b2 == 254 // 169.254.00 - 169.254.255.255 (Link-local/APIPA) + || addrBytes.s_b1 == 127 // 127.0.0.0 - 127.255.255.255 (Loopback) + || addrBytes.s_b1 == 0 // 0.0.0.0 - 0.255.255.255 (Current network) + || addrBytes.s_b1 == 100 && addrBytes.s_b2 >= 64 && addrBytes.s_b2 <= 127 // 100.64.0.0 - 100.127.255.255 (Shared address space) + || sockaddr_ipv4->sin_addr.S_un.S_addr == 0xFFFFFFFF // 255.255.255.255 (Broadcast) + || addrBytes.s_b1 >= 224 && addrBytes.s_b2 <= 239 // 224.0.0.0 - 239.255.255.255 (Multicast) + || addrBytes.s_b1 == 233 && addrBytes.s_b2 == 252 && addrBytes.s_b3 == 0 // 233.252.0.0 - 233.252.0.255 (MCAST-TEST-NET) + || addrBytes.s_b1 >= 240 && addrBytes.s_b4 <= 254) // 240.0.0.0 - 255.255.255.254 (Future Use Class E) + { + curl_free(urlHostname); + curl_free(urlScheme); + curl_free(urlPort); + curl_url_cleanup(url); + + return false; + } + } + + // clang-format on + + char resolvedStr[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &sockaddr_ipv4->sin_addr, resolvedStr, INET_ADDRSTRLEN); + + // Use the resolved address as the new request host. + outHostname = urlHostname; + outAddress = resolvedStr; + outPort = urlPort; + + freeaddrinfo(result); + + curl_free(urlHostname); + curl_free(urlScheme); + curl_free(urlPort); + curl_url_cleanup(url); + + return true; +} + +size_t HttpCurlWriteToStringBufferCallback(char* contents, size_t size, size_t nmemb, void* userp) +{ + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; +} + +template int HttpRequestHandler::MakeHttpRequest(const HttpRequest& requestParameters) +{ + if (!IsRunning()) + { + spdlog::warn("%s was called while IsRunning() is false!", __FUNCTION__); + return -1; + } + + if (IsHttpDisabled()) + { + spdlog::warn("NS_InternalMakeHttpRequest called while the game is running with -disablehttprequests." + " Please check if requests are allowed using NSIsHttpEnabled() first."); + return -1; + } + + bool bAllowLocalHttp = IsLocalHttpAllowed(); + + // This handle will be returned to Squirrel so it can wait for the response and assign a callback for it. + int handle = ++m_iLastRequestHandle; + + std::thread requestThread( + [this, handle, requestParameters, bAllowLocalHttp]() + { + std::string hostname, resolvedAddress, resolvedPort; + + if (!bAllowLocalHttp) + { + if (!IsHttpDestinationHostAllowed(requestParameters.baseUrl, hostname, resolvedAddress, resolvedPort)) + { + spdlog::warn( + "HttpRequestHandler::MakeHttpRequest attempted to make a request to a private network. This is only allowed when " + "running the game with -allowlocalhttp."); + g_pSquirrel->AsyncCall( + "NSHandleFailedHttpRequest", + handle, + (int)0, + "Cannot make HTTP requests to private network hosts without -allowlocalhttp. Check your console for more " + "information."); + return; + } + } + + CURL* curl = curl_easy_init(); + if (!curl) + { + spdlog::error("HttpRequestHandler::MakeHttpRequest failed to init libcurl for request."); + g_pSquirrel->AsyncCall( + "NSHandleFailedHttpRequest", handle, static_cast(CURLE_FAILED_INIT), curl_easy_strerror(CURLE_FAILED_INIT)); + return; + } + + // HEAD has no body. + if (requestParameters.method == HttpRequestMethod::HRM_HEAD) + { + curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + } + + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, HttpRequestMethod::ToString(requestParameters.method).c_str()); + + // Only resolve to IPv4 if we don't allow private network requests. + curl_slist* host = nullptr; + if (!bAllowLocalHttp) + { + curl_easy_setopt(curl, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + host = curl_slist_append(host, fmt::format("{}:{}:{}", hostname, resolvedPort, resolvedAddress).c_str()); + curl_easy_setopt(curl, CURLOPT_RESOLVE, host); + } + + // Ensure we only allow HTTP or HTTPS. + curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + + // Allow redirects + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 3L); + + // Check if the url already contains a query. + // If so, we'll know to append with & instead of start with ? + std::string queryUrl = requestParameters.baseUrl; + bool bUrlContainsQuery = false; + + // If this fails, just ignore the parsing and trust what the user wants to query. + // Probably will fail but handling it here would be annoying. + CURLU* curlUrl = curl_url(); + if (curlUrl) + { + if (curl_url_set(curlUrl, CURLUPART_URL, queryUrl.c_str(), CURLU_DEFAULT_SCHEME) == CURLUE_OK) + { + char* currentQuery; + if (curl_url_get(curlUrl, CURLUPART_QUERY, ¤tQuery, 0) == CURLUE_OK) + { + if (currentQuery && std::strlen(currentQuery) != 0) + { + bUrlContainsQuery = true; + } + } + + curl_free(currentQuery); + } + + curl_url_cleanup(curlUrl); + } + + // GET requests, or POST-like requests with an empty body, can have query parameters. + // Append them to the base url. + if (HttpRequestMethod::CanHaveQueryParameters(requestParameters.method) && + !HttpRequestMethod::UsesCurlPostOptions(requestParameters.method) || + requestParameters.body.empty()) + { + bool isFirstValue = true; + for (const auto& kv : requestParameters.queryParameters) + { + char* key = curl_easy_escape(curl, kv.first.c_str(), kv.first.length()); + + for (const std::string& queryValue : kv.second) + { + char* value = curl_easy_escape(curl, queryValue.c_str(), queryValue.length()); + + if (isFirstValue && !bUrlContainsQuery) + { + queryUrl.append(fmt::format("?{}={}", key, value)); + isFirstValue = false; + } + else + { + queryUrl.append(fmt::format("&{}={}", key, value)); + } + + curl_free(value); + } + + curl_free(key); + } + } + + // If this method uses POST-like curl options, set those and set the body. + // The body won't be sent if it's empty anyway, meaning the query parameters above, if any, would be. + if (HttpRequestMethod::UsesCurlPostOptions(requestParameters.method)) + { + // Grab the body and set it as a POST field + curl_easy_setopt(curl, CURLOPT_POST, 1L); + + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, requestParameters.body.length()); + curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, requestParameters.body.c_str()); + } + + // Set the full URL for this http request. + curl_easy_setopt(curl, CURLOPT_URL, queryUrl.c_str()); + + std::string bodyBuffer; + std::string headerBuffer; + + // Set up buffers to write the response headers and body. + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, HttpCurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &bodyBuffer); + curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, HttpCurlWriteToStringBufferCallback); + curl_easy_setopt(curl, CURLOPT_HEADERDATA, &headerBuffer); + + // Add all the headers for the request. + curl_slist* headers = nullptr; + + // Content-Type header for POST-like requests. + if (HttpRequestMethod::UsesCurlPostOptions(requestParameters.method) && !requestParameters.body.empty()) + { + headers = curl_slist_append(headers, fmt::format("Content-Type: {}", requestParameters.contentType).c_str()); + } + + for (const auto& kv : requestParameters.headers) + { + for (const std::string& headerValue : kv.second) + { + headers = curl_slist_append(headers, fmt::format("{}: {}", kv.first, headerValue).c_str()); + } + } + + if (headers != nullptr) + { + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + } + + // Disable SSL checks if requested by the user. + if (DisableHttpSsl()) + { + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYSTATUS, 0L); + } + + // Enforce the Northstar user agent, unless an override was specified. + if (requestParameters.userAgent.empty()) + { + curl_easy_setopt(curl, CURLOPT_USERAGENT, &NSUserAgent); + } + else + { + curl_easy_setopt(curl, CURLOPT_USERAGENT, requestParameters.userAgent.c_str()); + } + + // Set the timeout for this request. Max 60 seconds so mods can't just spin up native threads all the time. + curl_easy_setopt(curl, CURLOPT_TIMEOUT, std::clamp(requestParameters.timeout, 1, 60)); + + CURLcode result = curl_easy_perform(curl); + if (IsRunning()) + { + if (result == CURLE_OK) + { + // While the curl request is OK, it could return a non success code. + // Squirrel side will handle firing the correct callback. + long httpCode = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + g_pSquirrel->AsyncCall( + "NSHandleSuccessfulHttpRequest", handle, static_cast(httpCode), bodyBuffer, headerBuffer); + } + else + { + // Pass CURL result code & error. + spdlog::error( + "curl_easy_perform() failed with code {}, error: {}", static_cast(result), curl_easy_strerror(result)); + + // If it's an SSL issue, tell the user they may disable SSL checks using -disablehttpssl. + if (result == CURLE_PEER_FAILED_VERIFICATION || result == CURLE_SSL_CERTPROBLEM || + result == CURLE_SSL_INVALIDCERTSTATUS) + { + spdlog::error("You can try disabling SSL verifications for this issue using the -disablehttpssl launch argument. " + "Keep in mind this is potentially dangerous!"); + } + + g_pSquirrel->AsyncCall( + "NSHandleFailedHttpRequest", handle, static_cast(result), curl_easy_strerror(result)); + } + } + + curl_easy_cleanup(curl); + curl_slist_free_all(headers); + curl_slist_free_all(host); + }); + + requestThread.detach(); + return handle; +} + +template void HttpRequestHandler::RegisterSQFuncs() +{ + g_pSquirrel->AddFuncRegistration( + "int", + "NS_InternalMakeHttpRequest", + "int method, string baseUrl, table > headers, table > queryParams, string contentType, " + "string body, " + "int timeout, string userAgent", + "[Internal use only] Passes the HttpRequest struct fields to be reconstructed in native and used for an http request", + SQ_InternalMakeHttpRequest); + + g_pSquirrel->AddFuncRegistration( + "bool", + "NSIsHttpEnabled", + "", + "Whether or not HTTP requests are enabled. You can opt-out by starting the game with -disablehttprequests.", + SQ_IsHttpEnabled); + + g_pSquirrel->AddFuncRegistration( + "bool", + "NSIsLocalHttpAllowed", + "", + "Whether or not HTTP requests can be made to a private network address. You can enable this by starting the game with " + "-allowlocalhttp.", + SQ_IsLocalHttpAllowed); +} + +// int NS_InternalMakeHttpRequest(int method, string baseUrl, table headers, table queryParams, +// string contentType, string body, int timeout, string userAgent) +template SQRESULT SQ_InternalMakeHttpRequest(HSquirrelVM* sqvm) +{ + if (!g_httpRequestHandler || !g_httpRequestHandler->IsRunning()) + { + spdlog::warn("NS_InternalMakeHttpRequest called while the http request handler isn't running."); + g_pSquirrel->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; + } + + if (IsHttpDisabled()) + { + spdlog::warn("NS_InternalMakeHttpRequest called while the game is running with -disablehttprequests." + " Please check if requests are allowed using NSIsHttpEnabled() first."); + g_pSquirrel->pushinteger(sqvm, -1); + return SQRESULT_NOTNULL; + } + + HttpRequest request; + request.method = static_cast(g_pSquirrel->getinteger(sqvm, 1)); + request.baseUrl = g_pSquirrel->getstring(sqvm, 2); + + // Read the tables for headers and query parameters. + SQTable* headerTable = sqvm->_stackOfCurrentFunction[3]._VAL.asTable; + for (int idx = 0; idx < headerTable->_numOfNodes; ++idx) + { + tableNode* node = &headerTable->_nodes[idx]; + + if (node->key._Type == OT_STRING && node->val._Type == OT_ARRAY) + { + SQArray* valueArray = node->val._VAL.asArray; + std::vector headerValues; + + for (int vIdx = 0; vIdx < valueArray->_usedSlots; ++vIdx) + { + if (valueArray->_values[vIdx]._Type == OT_STRING) + { + headerValues.push_back(valueArray->_values[vIdx]._VAL.asString->_val); + } + } + + request.headers[node->key._VAL.asString->_val] = headerValues; + } + } + + SQTable* queryTable = sqvm->_stackOfCurrentFunction[4]._VAL.asTable; + for (int idx = 0; idx < queryTable->_numOfNodes; ++idx) + { + tableNode* node = &queryTable->_nodes[idx]; + + if (node->key._Type == OT_STRING && node->val._Type == OT_ARRAY) + { + SQArray* valueArray = node->val._VAL.asArray; + std::vector queryValues; + + for (int vIdx = 0; vIdx < valueArray->_usedSlots; ++vIdx) + { + if (valueArray->_values[vIdx]._Type == OT_STRING) + { + queryValues.push_back(valueArray->_values[vIdx]._VAL.asString->_val); + } + } + + request.queryParameters[node->key._VAL.asString->_val] = queryValues; + } + } + + request.contentType = g_pSquirrel->getstring(sqvm, 5); + request.body = g_pSquirrel->getstring(sqvm, 6); + request.timeout = g_pSquirrel->getinteger(sqvm, 7); + request.userAgent = g_pSquirrel->getstring(sqvm, 8); + + int handle = g_httpRequestHandler->MakeHttpRequest(request); + g_pSquirrel->pushinteger(sqvm, handle); + return SQRESULT_NOTNULL; +} + +// bool NSIsHttpEnabled() +template SQRESULT SQ_IsHttpEnabled(HSquirrelVM* sqvm) +{ + g_pSquirrel->pushbool(sqvm, !IsHttpDisabled()); + return SQRESULT_NOTNULL; +} + +// bool NSIsLocalHttpAllowed() +template SQRESULT SQ_IsLocalHttpAllowed(HSquirrelVM* sqvm) +{ + g_pSquirrel->pushbool(sqvm, IsLocalHttpAllowed()); + return SQRESULT_NOTNULL; +} + +ON_DLL_LOAD_RELIESON("client.dll", HttpRequestHandler_ClientInit, ClientSquirrel, (CModule module)) +{ + g_httpRequestHandler->RegisterSQFuncs(); + g_httpRequestHandler->RegisterSQFuncs(); +} + +ON_DLL_LOAD_RELIESON("server.dll", HttpRequestHandler_ServerInit, ServerSquirrel, (CModule module)) +{ + g_httpRequestHandler->RegisterSQFuncs(); +} + +ON_DLL_LOAD("engine.dll", HttpRequestHandler_Init, (CModule module)) +{ + g_httpRequestHandler = new HttpRequestHandler; + g_httpRequestHandler->StartHttpRequestHandler(); +} diff --git a/NorthstarDLL/scripts/scripthttprequesthandler.h b/NorthstarDLL/scripts/scripthttprequesthandler.h new file mode 100644 index 00000000..0f888b6e --- /dev/null +++ b/NorthstarDLL/scripts/scripthttprequesthandler.h @@ -0,0 +1,132 @@ +#pragma once + +#include "pch.h" + +enum class ScriptContext; + +// These definitions below should match on the Squirrel side so we can easily pass them along through a function. + +/** + * Allowed methods for an HttpRequest. + */ +namespace HttpRequestMethod +{ + enum Type + { + HRM_GET = 0, + HRM_POST = 1, + HRM_HEAD = 2, + HRM_PUT = 3, + HRM_DELETE = 4, + HRM_PATCH = 5, + HRM_OPTIONS = 6, + }; + + /** Returns the HTTP string representation of the given method. */ + inline std::string ToString(HttpRequestMethod::Type method) + { + switch (method) + { + case HttpRequestMethod::HRM_GET: + return "GET"; + case HttpRequestMethod::HRM_POST: + return "POST"; + case HttpRequestMethod::HRM_HEAD: + return "HEAD"; + case HttpRequestMethod::HRM_PUT: + return "PUT"; + case HttpRequestMethod::HRM_DELETE: + return "DELETE"; + case HttpRequestMethod::HRM_PATCH: + return "PATCH"; + case HttpRequestMethod::HRM_OPTIONS: + return "OPTIONS"; + default: + return "INVALID"; + } + } + + /** Whether or not the given method should be treated like a POST for curlopts. */ + bool UsesCurlPostOptions(HttpRequestMethod::Type method) + { + switch (method) + { + case HttpRequestMethod::HRM_POST: + case HttpRequestMethod::HRM_PUT: + case HttpRequestMethod::HRM_DELETE: + case HttpRequestMethod::HRM_PATCH: + return true; + default: + return false; + } + } + + /** Whether or not the given http request method can have query parameters in the URL. */ + bool CanHaveQueryParameters(HttpRequestMethod::Type method) + { + return method == HttpRequestMethod::HRM_GET || UsesCurlPostOptions(method); + } +}; // namespace HttpRequestMethod + +/** Contains data about an http request that has been queued. */ +struct HttpRequest +{ + /** Method used for this http request. */ + HttpRequestMethod::Type method; + + /** Base URL of this http request. */ + std::string baseUrl; + + /** Headers used for this http request. Some may get overridden or ignored. */ + std::unordered_map> headers; + + /** Query parameters for this http request. */ + std::unordered_map> queryParameters; + + /** The content type of this http request. Defaults to text/plain & UTF-8 charset. */ + std::string contentType = "text/plain; charset=utf-8"; + + /** The body of this http request. If set, will override queryParameters.*/ + std::string body; + + /** The timeout for the http request, in seconds. Must be between 1 and 60. */ + int timeout; + + /** If set, the override to use for the User-Agent header. */ + std::string userAgent; +}; + +/** + * Handles making HTTP requests and sending the responses back to Squirrel. + */ +class HttpRequestHandler +{ + public: + HttpRequestHandler(); + + // Start/Stop the HTTP request handler. Right now this doesn't do much. + void StartHttpRequestHandler(); + void StopHttpRequestHandler(); + + // Whether or not this http request handler is currently running. + bool IsRunning() const + { + return m_bIsHttpRequestHandlerRunning; + } + + /** + * Creates a new thread to execute an HTTP request. + * @param requestParameters The parameters to use for this http request. + * @returns The handle for the http request being sent, or -1 if the request failed. + */ + template int MakeHttpRequest(const HttpRequest& requestParameters); + + /** Registers the HTTP request Squirrel functions for the given script context. */ + template void RegisterSQFuncs(); + + private: + int m_iLastRequestHandle = 0; + std::atomic_bool m_bIsHttpRequestHandlerRunning = false; +}; + +extern HttpRequestHandler* g_httpRequestHandler; diff --git a/NorthstarDLL/scripts/scriptjson.cpp b/NorthstarDLL/scripts/scriptjson.cpp new file mode 100644 index 00000000..f41b0457 --- /dev/null +++ b/NorthstarDLL/scripts/scriptjson.cpp @@ -0,0 +1,249 @@ +#include "pch.h" +#include "squirrel/squirrel.h" + +#include "rapidjson/error/en.h" +#include "rapidjson/document.h" +#include "rapidjson/writer.h" +#include "rapidjson/stringbuffer.h" + +#ifdef _MSC_VER +#undef GetObject // fuck microsoft developers +#endif + +template void +DecodeJsonArray(HSquirrelVM* sqvm, rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>* arr) +{ + g_pSquirrel->newarray(sqvm, 0); + + for (auto& itr : arr->GetArray()) + { + switch (itr.GetType()) + { + case rapidjson::kObjectType: + DecodeJsonTable(sqvm, &itr); + g_pSquirrel->arrayappend(sqvm, -2); + break; + case rapidjson::kArrayType: + DecodeJsonArray(sqvm, &itr); + g_pSquirrel->arrayappend(sqvm, -2); + break; + case rapidjson::kStringType: + g_pSquirrel->pushstring(sqvm, itr.GetString(), -1); + g_pSquirrel->arrayappend(sqvm, -2); + break; + case rapidjson::kTrueType: + case rapidjson::kFalseType: + g_pSquirrel->pushbool(sqvm, itr.GetBool()); + g_pSquirrel->arrayappend(sqvm, -2); + break; + case rapidjson::kNumberType: + if (itr.IsDouble() || itr.IsFloat()) + g_pSquirrel->pushfloat(sqvm, itr.GetFloat()); + else + g_pSquirrel->pushinteger(sqvm, itr.GetInt()); + g_pSquirrel->arrayappend(sqvm, -2); + break; + } + } +} + +template void +DecodeJsonTable(HSquirrelVM* sqvm, rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>* obj) +{ + g_pSquirrel->newtable(sqvm); + + for (auto itr = obj->MemberBegin(); itr != obj->MemberEnd(); itr++) + { + switch (itr->value.GetType()) + { + case rapidjson::kObjectType: + g_pSquirrel->pushstring(sqvm, itr->name.GetString(), -1); + DecodeJsonTable( + sqvm, (rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>*)&itr->value); + g_pSquirrel->newslot(sqvm, -3, false); + break; + case rapidjson::kArrayType: + g_pSquirrel->pushstring(sqvm, itr->name.GetString(), -1); + DecodeJsonArray( + sqvm, (rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>*)&itr->value); + g_pSquirrel->newslot(sqvm, -3, false); + break; + case rapidjson::kStringType: + g_pSquirrel->pushstring(sqvm, itr->name.GetString(), -1); + g_pSquirrel->pushstring(sqvm, itr->value.GetString(), -1); + + g_pSquirrel->newslot(sqvm, -3, false); + break; + case rapidjson::kTrueType: + case rapidjson::kFalseType: + g_pSquirrel->pushstring(sqvm, itr->name.GetString(), -1); + g_pSquirrel->pushbool(sqvm, itr->value.GetBool()); + g_pSquirrel->newslot(sqvm, -3, false); + break; + case rapidjson::kNumberType: + if (itr->value.IsDouble() || itr->value.IsFloat()) + { + g_pSquirrel->pushstring(sqvm, itr->name.GetString(), -1); + g_pSquirrel->pushfloat(sqvm, itr->value.GetFloat()); + } + else + { + g_pSquirrel->pushstring(sqvm, itr->name.GetString(), -1); + g_pSquirrel->pushinteger(sqvm, itr->value.GetInt()); + } + g_pSquirrel->newslot(sqvm, -3, false); + break; + } + } +} + +template void EncodeJSONTable( + SQTable* table, + rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>* obj, + rapidjson::MemoryPoolAllocator& allocator) +{ + for (int i = 0; i < table->_numOfNodes; i++) + { + tableNode* node = &table->_nodes[i]; + if (node->key._Type == OT_STRING) + { + rapidjson::GenericValue, rapidjson::MemoryPoolAllocator> newObj(rapidjson::kObjectType); + rapidjson::GenericValue, rapidjson::MemoryPoolAllocator> newArray(rapidjson::kArrayType); + + switch (node->val._Type) + { + case OT_STRING: + obj->AddMember( + rapidjson::StringRef(node->key._VAL.asString->_val), rapidjson::StringRef(node->val._VAL.asString->_val), allocator); + break; + case OT_INTEGER: + obj->AddMember(rapidjson::StringRef(node->key._VAL.asString->_val), node->val._VAL.asInteger, allocator); + break; + case OT_FLOAT: + obj->AddMember(rapidjson::StringRef(node->key._VAL.asString->_val), node->val._VAL.asFloat, allocator); + break; + case OT_BOOL: + if (node->val._VAL.asInteger) + { + obj->AddMember(rapidjson::StringRef(node->key._VAL.asString->_val), true, allocator); + } + else + { + obj->AddMember(rapidjson::StringRef(node->key._VAL.asString->_val), false, allocator); + } + break; + case OT_TABLE: + EncodeJSONTable(node->val._VAL.asTable, &newObj, allocator); + obj->AddMember(rapidjson::StringRef(node->key._VAL.asString->_val), newObj, allocator); + break; + case OT_ARRAY: + EncodeJSONArray(node->val._VAL.asArray, &newArray, allocator); + obj->AddMember(rapidjson::StringRef(node->key._VAL.asString->_val), newArray, allocator); + break; + default: + spdlog::warn("SQ_EncodeJSON: squirrel type {} not supported", SQTypeNameFromID(node->val._Type)); + break; + } + } + } +} + +template void EncodeJSONArray( + SQArray* arr, + rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>* obj, + rapidjson::MemoryPoolAllocator& allocator) +{ + for (int i = 0; i < arr->_usedSlots; i++) + { + SQObject* node = &arr->_values[i]; + + rapidjson::GenericValue, rapidjson::MemoryPoolAllocator> newObj(rapidjson::kObjectType); + rapidjson::GenericValue, rapidjson::MemoryPoolAllocator> newArray(rapidjson::kArrayType); + + switch (node->_Type) + { + case OT_STRING: + obj->PushBack(rapidjson::StringRef(node->_VAL.asString->_val), allocator); + break; + case OT_INTEGER: + obj->PushBack(node->_VAL.asInteger, allocator); + break; + case OT_FLOAT: + obj->PushBack(node->_VAL.asFloat, allocator); + break; + case OT_BOOL: + if (node->_VAL.asInteger) + obj->PushBack(rapidjson::StringRef("true"), allocator); + else + obj->PushBack(rapidjson::StringRef("false"), allocator); + break; + case OT_TABLE: + EncodeJSONTable(node->_VAL.asTable, &newObj, allocator); + obj->PushBack(newObj, allocator); + break; + case OT_ARRAY: + EncodeJSONArray(node->_VAL.asArray, &newArray, allocator); + obj->PushBack(newArray, allocator); + break; + default: + spdlog::info("SQ encode Json type {} not supported", SQTypeNameFromID(node->_Type)); + } + } +} + +ADD_SQFUNC( + "table", + DecodeJSON, + "string json, bool fatalParseErrors = false", + "converts a json string to a squirrel table", + ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER) +{ + const char* pJson = g_pSquirrel->getstring(sqvm, 1); + const bool bFatalParseErrors = g_pSquirrel->getbool(sqvm, 2); + + rapidjson_document doc; + doc.Parse(pJson); + if (doc.HasParseError()) + { + g_pSquirrel->newtable(sqvm); + + std::string sErrorString = fmt::format( + "Failed parsing json file: encountered parse error \"{}\" at offset {}", + GetParseError_En(doc.GetParseError()), + doc.GetErrorOffset()); + + if (bFatalParseErrors) + g_pSquirrel->raiseerror(sqvm, sErrorString.c_str()); + else + spdlog::warn(sErrorString); + + return SQRESULT_NOTNULL; + } + + DecodeJsonTable(sqvm, (rapidjson::GenericValue, rapidjson::MemoryPoolAllocator>*)&doc); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC( + "string", + EncodeJSON, + "table data", + "converts a squirrel table to a json string", + ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER) +{ + rapidjson_document doc; + doc.SetObject(); + + // temp until this is just the func parameter type + HSquirrelVM* vm = (HSquirrelVM*)sqvm; + SQTable* table = vm->_stackOfCurrentFunction[1]._VAL.asTable; + EncodeJSONTable(table, &doc, doc.GetAllocator()); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + doc.Accept(writer); + const char* pJsonString = buffer.GetString(); + + g_pSquirrel->pushstring(sqvm, pJsonString, -1); + return SQRESULT_NOTNULL; +} diff --git a/NorthstarDLL/scripts/scriptutility.cpp b/NorthstarDLL/scripts/scriptutility.cpp new file mode 100644 index 00000000..8dae49cf --- /dev/null +++ b/NorthstarDLL/scripts/scriptutility.cpp @@ -0,0 +1,14 @@ +#include "pch.h" +#include "squirrel/squirrel.h" + +// asset function StringToAsset( string assetName ) +ADD_SQFUNC( + "asset", + StringToAsset, + "string assetName", + "converts a given string to an asset", + ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER) +{ + g_pSquirrel->pushasset(sqvm, g_pSquirrel->getstring(sqvm, 1), -1); + return SQRESULT_NOTNULL; +} diff --git a/NorthstarDLL/scripts/server/miscserverfixes.cpp b/NorthstarDLL/scripts/server/miscserverfixes.cpp new file mode 100644 index 00000000..4feca505 --- /dev/null +++ b/NorthstarDLL/scripts/server/miscserverfixes.cpp @@ -0,0 +1,7 @@ +#include "pch.h" + +ON_DLL_LOAD("server.dll", MiscServerFixes, (CModule module)) +{ + // nop out call to VGUI shutdown since it crashes the game when quitting from the console + module.Offset(0x154A96).NOP(5); +} diff --git a/NorthstarDLL/scripts/server/miscserverscript.cpp b/NorthstarDLL/scripts/server/miscserverscript.cpp new file mode 100644 index 00000000..0d865388 --- /dev/null +++ b/NorthstarDLL/scripts/server/miscserverscript.cpp @@ -0,0 +1,60 @@ +#include "pch.h" +#include "squirrel/squirrel.h" +#include "masterserver/masterserver.h" +#include "server/auth/serverauthentication.h" +#include "dedicated/dedicated.h" +#include "client/r2client.h" +#include "server/r2server.h" + +#include + +ADD_SQFUNC("void", NSEarlyWritePlayerPersistenceForLeave, "entity player", "", ScriptContext::SERVER) +{ + const R2::CBasePlayer* pPlayer = g_pSquirrel->getentity(sqvm, 1); + if (!pPlayer) + { + spdlog::warn("NSEarlyWritePlayerPersistenceForLeave got null player"); + + g_pSquirrel->pushbool(sqvm, false); + return SQRESULT_NOTNULL; + } + + R2::CBaseClient* pClient = &R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1]; + if (g_pServerAuthentication->m_PlayerAuthenticationData.find(pClient) == g_pServerAuthentication->m_PlayerAuthenticationData.end()) + { + g_pSquirrel->pushbool(sqvm, false); + return SQRESULT_NOTNULL; + } + + g_pServerAuthentication->m_PlayerAuthenticationData[pClient].needPersistenceWriteOnLeave = false; + g_pServerAuthentication->WritePersistentData(pClient); + return SQRESULT_NULL; +} + +ADD_SQFUNC("bool", NSIsWritingPlayerPersistence, "", "", ScriptContext::SERVER) +{ + g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_bSavingPersistentData); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("bool", NSIsPlayerLocalPlayer, "entity player", "", ScriptContext::SERVER) +{ + const R2::CBasePlayer* pPlayer = g_pSquirrel->getentity(sqvm, 1); + if (!pPlayer) + { + spdlog::warn("NSIsPlayerLocalPlayer got null player"); + + g_pSquirrel->pushbool(sqvm, false); + return SQRESULT_NOTNULL; + } + + R2::CBaseClient* pClient = &R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1]; + g_pSquirrel->pushbool(sqvm, !strcmp(R2::g_pLocalPlayerUserID, pClient->m_UID)); + return SQRESULT_NOTNULL; +} + +ADD_SQFUNC("bool", NSIsDedicated, "", "", ScriptContext::SERVER) +{ + g_pSquirrel->pushbool(sqvm, IsDedicatedServer()); + return SQRESULT_NOTNULL; +} diff --git a/NorthstarDLL/scripts/server/scriptuserinfo.cpp b/NorthstarDLL/scripts/server/scriptuserinfo.cpp new file mode 100644 index 00000000..68baac0e --- /dev/null +++ b/NorthstarDLL/scripts/server/scriptuserinfo.cpp @@ -0,0 +1,105 @@ +#include "pch.h" +#include "squirrel/squirrel.h" +#include "engine/r2engine.h" +#include "server/r2server.h" + +// clang-format off +ADD_SQFUNC("string", GetUserInfoKVString_Internal, "entity player, string key, string defaultValue = \"\"", + "Gets the string value of a given player's userinfo convar by name", ScriptContext::SERVER) +// clang-format on +{ + const R2::CBasePlayer* pPlayer = g_pSquirrel->getentity(sqvm, 1); + if (!pPlayer) + { + g_pSquirrel->raiseerror(sqvm, "player is null"); + return SQRESULT_ERROR; + } + + const char* pKey = g_pSquirrel->getstring(sqvm, 2); + const char* pDefaultValue = g_pSquirrel->getstring(sqvm, 3); + + const char* pResult = R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetString(pKey, pDefaultValue); + g_pSquirrel->pushstring(sqvm, pResult); + return SQRESULT_NOTNULL; +} + +// clang-format off +ADD_SQFUNC("asset", GetUserInfoKVAsset_Internal, "entity player, string key, asset defaultValue = $\"\"", + "Gets the asset value of a given player's userinfo convar by name", ScriptContext::SERVER) +// clang-format on +{ + const R2::CBasePlayer* pPlayer = g_pSquirrel->getentity(sqvm, 1); + if (!pPlayer) + { + g_pSquirrel->raiseerror(sqvm, "player is null"); + return SQRESULT_ERROR; + } + + const char* pKey = g_pSquirrel->getstring(sqvm, 2); + const char* pDefaultValue; + g_pSquirrel->getasset(sqvm, 3, &pDefaultValue); + + const char* pResult = R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetString(pKey, pDefaultValue); + g_pSquirrel->pushasset(sqvm, pResult); + return SQRESULT_NOTNULL; +} + +// clang-format off +ADD_SQFUNC("int", GetUserInfoKVInt_Internal, "entity player, string key, int defaultValue = 0", + "Gets the int value of a given player's userinfo convar by name", ScriptContext::SERVER) +// clang-format on +{ + const R2::CBasePlayer* pPlayer = g_pSquirrel->getentity(sqvm, 1); + if (!pPlayer) + { + g_pSquirrel->raiseerror(sqvm, "player is null"); + return SQRESULT_ERROR; + } + + const char* pKey = g_pSquirrel->getstring(sqvm, 2); + const int iDefaultValue = g_pSquirrel->getinteger(sqvm, 3); + + const int iResult = R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetInt(pKey, iDefaultValue); + g_pSquirrel->pushinteger(sqvm, iResult); + return SQRESULT_NOTNULL; +} + +// clang-format off +ADD_SQFUNC("float", GetUserInfoKVFloat_Internal, "entity player, string key, float defaultValue = 0", + "Gets the float value of a given player's userinfo convar by name", ScriptContext::SERVER) +// clang-format on +{ + const R2::CBasePlayer* pPlayer = g_pSquirrel->getentity(sqvm, 1); + if (!pPlayer) + { + g_pSquirrel->raiseerror(sqvm, "player is null"); + return SQRESULT_ERROR; + } + + const char* pKey = g_pSquirrel->getstring(sqvm, 2); + const float flDefaultValue = g_pSquirrel->getfloat(sqvm, 3); + + const float flResult = R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetFloat(pKey, flDefaultValue); + g_pSquirrel->pushfloat(sqvm, flResult); + return SQRESULT_NOTNULL; +} + +// clang-format off +ADD_SQFUNC("bool", GetUserInfoKVBool_Internal, "entity player, string key, bool defaultValue = false", + "Gets the bool value of a given player's userinfo convar by name", ScriptContext::SERVER) +// clang-format on +{ + const R2::CBasePlayer* pPlayer = g_pSquirrel->getentity(sqvm, 1); + if (!pPlayer) + { + g_pSquirrel->raiseerror(sqvm, "player is null"); + return SQRESULT_ERROR; + } + + const char* pKey = g_pSquirrel->getstring(sqvm, 2); + const bool bDefaultValue = g_pSquirrel->getbool(sqvm, 3); + + const bool bResult = R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetInt(pKey, bDefaultValue); + g_pSquirrel->pushbool(sqvm, bResult); + return SQRESULT_NOTNULL; +} diff --git a/NorthstarDLL/scriptserverbrowser.cpp b/NorthstarDLL/scriptserverbrowser.cpp deleted file mode 100644 index 05f83269..00000000 --- a/NorthstarDLL/scriptserverbrowser.cpp +++ /dev/null @@ -1,410 +0,0 @@ -#include "pch.h" -#include "squirrel.h" -#include "masterserver.h" -#include "serverauthentication.h" -#include "r2engine.h" -#include "r2client.h" - -// functions for viewing server browser - -ADD_SQFUNC("bool", NSIsMasterServerAuthenticated, "", "", ScriptContext::UI) -{ - g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_bOriginAuthWithMasterServerDone); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("void", NSRequestServerList, "", "", ScriptContext::UI) -{ - g_pMasterServerManager->RequestServerList(); - return SQRESULT_NULL; -} - -ADD_SQFUNC("bool", NSIsRequestingServerList, "", "", ScriptContext::UI) -{ - g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_bScriptRequestingServerList); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("bool", NSMasterServerConnectionSuccessful, "", "", ScriptContext::UI) -{ - g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_bSuccessfullyConnected); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("int", NSGetServerCount, "", "", ScriptContext::UI) -{ - g_pSquirrel->pushinteger(sqvm, g_pMasterServerManager->m_vRemoteServers.size()); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("string", NSGetServerName, "int serverIndex", "", ScriptContext::UI) -{ - SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); - - if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "Tried to get name of server index {} when only {} servers are available", - serverIndex, - g_pMasterServerManager->m_vRemoteServers.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].name); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("string", NSGetServerDescription, "int serverIndex", "", ScriptContext::UI) -{ - SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); - - if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "Tried to get description of server index {} when only {} servers are available", - serverIndex, - g_pMasterServerManager->m_vRemoteServers.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].description.c_str()); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("string", NSGetServerMap, "int serverIndex", "", ScriptContext::UI) -{ - SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); - - if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "Tried to get map of server index {} when only {} servers are available", - serverIndex, - g_pMasterServerManager->m_vRemoteServers.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].map); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("string", NSGetServerPlaylist, "int serverIndex", "", ScriptContext::UI) -{ - SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); - - if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "Tried to get playlist of server index {} when only {} servers are available", - serverIndex, - g_pMasterServerManager->m_vRemoteServers.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].playlist); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("int", NSGetServerPlayerCount, "int serverIndex", "", ScriptContext::UI) -{ - SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); - - if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "Tried to get playercount of server index {} when only {} servers are available", - serverIndex, - g_pMasterServerManager->m_vRemoteServers.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushinteger(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].playerCount); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("int", NSGetServerMaxPlayerCount, "int serverIndex", "", ScriptContext::UI) -{ - SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); - - if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "Tried to get max playercount of server index {} when only {} servers are available", - serverIndex, - g_pMasterServerManager->m_vRemoteServers.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushinteger(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].maxPlayers); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("string", NSGetServerID, "int serverIndex", "", ScriptContext::UI) -{ - SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); - - if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "Tried to get id of server index {} when only {} servers are available", - serverIndex, - g_pMasterServerManager->m_vRemoteServers.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].id); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("bool", NSServerRequiresPassword, "int serverIndex", "", ScriptContext::UI) -{ - SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); - - if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "Tried to get hasPassword of server index {} when only {} servers are available", - serverIndex, - g_pMasterServerManager->m_vRemoteServers.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].requiresPassword); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("int", NSGetServerRequiredModsCount, "int serverIndex", "", ScriptContext::UI) -{ - SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); - - if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "Tried to get required mods count of server index {} when only {} servers are available", - serverIndex, - g_pMasterServerManager->m_vRemoteServers.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushinteger(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods.size()); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("string", NSGetServerRegion, "int serverIndex", "", ScriptContext::UI) -{ - SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); - - if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "Tried to get region of server index {} when only {} servers are available", - serverIndex, - g_pMasterServerManager->m_vRemoteServers.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].region, -1); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("string", NSGetServerRequiredModName, "int serverIndex, int modIndex", "", ScriptContext::UI) -{ - SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); - SQInteger modIndex = g_pSquirrel->getinteger(sqvm, 2); - - if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "Tried to get hasPassword of server index {} when only {} servers are available", - serverIndex, - g_pMasterServerManager->m_vRemoteServers.size()) - .c_str()); - return SQRESULT_ERROR; - } - - if (modIndex >= g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods.size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "Tried to get required mod name of mod index {} when only {} mod are available", - modIndex, - g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods[modIndex].Name.c_str()); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("string", NSGetServerRequiredModVersion, "int serverIndex, int modIndex", "", ScriptContext::UI) -{ - SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); - SQInteger modIndex = g_pSquirrel->getinteger(sqvm, 2); - - if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "Tried to get required mod version of server index {} when only {} servers are available", - serverIndex, - g_pMasterServerManager->m_vRemoteServers.size()) - .c_str()); - return SQRESULT_ERROR; - } - - if (modIndex >= g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods.size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "Tried to get required mod version of mod index {} when only {} mod are available", - modIndex, - g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods.size()) - .c_str()); - return SQRESULT_ERROR; - } - - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_vRemoteServers[serverIndex].requiredMods[modIndex].Version.c_str()); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("void", NSClearRecievedServerList, "", "", ScriptContext::UI) -{ - g_pMasterServerManager->ClearServerList(); - return SQRESULT_NULL; -} - -// functions for authenticating with servers - -ADD_SQFUNC("void", NSTryAuthWithServer, "int serverIndex, string password = ''", "", ScriptContext::UI) -{ - SQInteger serverIndex = g_pSquirrel->getinteger(sqvm, 1); - const SQChar* password = g_pSquirrel->getstring(sqvm, 2); - - if (serverIndex >= g_pMasterServerManager->m_vRemoteServers.size()) - { - g_pSquirrel->raiseerror( - sqvm, - fmt::format( - "Tried to auth with server index {} when only {} servers are available", - serverIndex, - g_pMasterServerManager->m_vRemoteServers.size()) - .c_str()); - return SQRESULT_ERROR; - } - - // send off persistent data first, don't worry about server/client stuff, since m_additionalPlayerData should only have entries when - // we're a local server note: this seems like it could create a race condition, test later - for (auto& pair : g_pServerAuthentication->m_PlayerAuthenticationData) - g_pServerAuthentication->WritePersistentData(pair.first); - - // do auth - g_pMasterServerManager->AuthenticateWithServer( - R2::g_pLocalPlayerUserID, - g_pMasterServerManager->m_sOwnClientAuthToken, - g_pMasterServerManager->m_vRemoteServers[serverIndex].id, - (char*)password); - - return SQRESULT_NULL; -} - -ADD_SQFUNC("bool", NSIsAuthenticatingWithServer, "", "", ScriptContext::UI) -{ - g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_bScriptAuthenticatingWithGameServer); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("bool", NSWasAuthSuccessful, "", "", ScriptContext::UI) -{ - g_pSquirrel->pushbool(sqvm, g_pMasterServerManager->m_bSuccessfullyAuthenticatedWithGameServer); - return SQRESULT_NOTNULL; -} - -ADD_SQFUNC("void", NSConnectToAuthedServer, "", "", ScriptContext::UI) -{ - if (!g_pMasterServerManager->m_bHasPendingConnectionInfo) - { - g_pSquirrel->raiseerror( - sqvm, fmt::format("Tried to connect to authed server before any pending connection info was available").c_str()); - return SQRESULT_ERROR; - } - - RemoteServerConnectionInfo& info = g_pMasterServerManager->m_pendingConnectionInfo; - - // set auth token, then try to connect - // i'm honestly not entirely sure how silentconnect works regarding ports and encryption so using connect for now - R2::g_pCVar->FindVar("serverfilter")->SetValue(info.authToken); - R2::Cbuf_AddText( - R2::Cbuf_GetCurrentPlayer(), - fmt::format( - "connect {}.{}.{}.{}:{}", - info.ip.S_un.S_un_b.s_b1, - info.ip.S_un.S_un_b.s_b2, - info.ip.S_un.S_un_b.s_b3, - info.ip.S_un.S_un_b.s_b4, - info.port) - .c_str(), - R2::cmd_source_t::kCommandSrcCode); - - g_pMasterServerManager->m_bHasPendingConnectionInfo = false; - return SQRESULT_NULL; -} - -ADD_SQFUNC("void", NSTryAuthWithLocalServer, "", "", ScriptContext::UI) -{ - // do auth request - g_pMasterServerManager->AuthenticateWithOwnServer(R2::g_pLocalPlayerUserID, g_pMasterServerManager->m_sOwnClientAuthToken); - - return SQRESULT_NULL; -} - -ADD_SQFUNC("void", NSCompleteAuthWithLocalServer, "", "", ScriptContext::UI) -{ - // literally just set serverfilter - // note: this assumes we have no authdata other than our own - if (g_pServerAuthentication->m_RemoteAuthenticationData.size()) - R2::g_pCVar->FindVar("serverfilter")->SetValue(g_pServerAuthentication->m_RemoteAuthenticationData.begin()->first.c_str()); - - return SQRESULT_NULL; -} - -ADD_SQFUNC("string", NSGetAuthFailReason, "", "", ScriptContext::UI) -{ - g_pSquirrel->pushstring(sqvm, g_pMasterServerManager->m_sAuthFailureReason.c_str(), -1); - return SQRESULT_NOTNULL; -} diff --git a/NorthstarDLL/scriptservertoclientstringcommand.cpp b/NorthstarDLL/scriptservertoclientstringcommand.cpp deleted file mode 100644 index ac19c3af..00000000 --- a/NorthstarDLL/scriptservertoclientstringcommand.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "pch.h" -#include "squirrel.h" -#include "convar.h" -#include "concommand.h" - -void ConCommand_ns_script_servertoclientstringcommand(const CCommand& arg) -{ - if (g_pSquirrel->m_pSQVM) - g_pSquirrel->Call("NSClientCodeCallback_RecievedServerToClientStringCommand", arg.ArgS()); -} - -ON_DLL_LOAD_CLIENT_RELIESON("client.dll", ScriptServerToClientStringCommand, ClientSquirrel, (CModule module)) -{ - RegisterConCommand( - "ns_script_servertoclientstringcommand", - ConCommand_ns_script_servertoclientstringcommand, - "", - FCVAR_CLIENTDLL | FCVAR_SERVER_CAN_EXECUTE); -} diff --git a/NorthstarDLL/scriptuserinfo.cpp b/NorthstarDLL/scriptuserinfo.cpp deleted file mode 100644 index 415ae3ea..00000000 --- a/NorthstarDLL/scriptuserinfo.cpp +++ /dev/null @@ -1,105 +0,0 @@ -#include "pch.h" -#include "squirrel.h" -#include "r2engine.h" -#include "r2server.h" - -// clang-format off -ADD_SQFUNC("string", GetUserInfoKVString_Internal, "entity player, string key, string defaultValue = \"\"", - "Gets the string value of a given player's userinfo convar by name", ScriptContext::SERVER) -// clang-format on -{ - const R2::CBasePlayer* pPlayer = g_pSquirrel->getentity(sqvm, 1); - if (!pPlayer) - { - g_pSquirrel->raiseerror(sqvm, "player is null"); - return SQRESULT_ERROR; - } - - const char* pKey = g_pSquirrel->getstring(sqvm, 2); - const char* pDefaultValue = g_pSquirrel->getstring(sqvm, 3); - - const char* pResult = R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetString(pKey, pDefaultValue); - g_pSquirrel->pushstring(sqvm, pResult); - return SQRESULT_NOTNULL; -} - -// clang-format off -ADD_SQFUNC("asset", GetUserInfoKVAsset_Internal, "entity player, string key, asset defaultValue = $\"\"", - "Gets the asset value of a given player's userinfo convar by name", ScriptContext::SERVER) -// clang-format on -{ - const R2::CBasePlayer* pPlayer = g_pSquirrel->getentity(sqvm, 1); - if (!pPlayer) - { - g_pSquirrel->raiseerror(sqvm, "player is null"); - return SQRESULT_ERROR; - } - - const char* pKey = g_pSquirrel->getstring(sqvm, 2); - const char* pDefaultValue; - g_pSquirrel->getasset(sqvm, 3, &pDefaultValue); - - const char* pResult = R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetString(pKey, pDefaultValue); - g_pSquirrel->pushasset(sqvm, pResult); - return SQRESULT_NOTNULL; -} - -// clang-format off -ADD_SQFUNC("int", GetUserInfoKVInt_Internal, "entity player, string key, int defaultValue = 0", - "Gets the int value of a given player's userinfo convar by name", ScriptContext::SERVER) -// clang-format on -{ - const R2::CBasePlayer* pPlayer = g_pSquirrel->getentity(sqvm, 1); - if (!pPlayer) - { - g_pSquirrel->raiseerror(sqvm, "player is null"); - return SQRESULT_ERROR; - } - - const char* pKey = g_pSquirrel->getstring(sqvm, 2); - const int iDefaultValue = g_pSquirrel->getinteger(sqvm, 3); - - const int iResult = R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetInt(pKey, iDefaultValue); - g_pSquirrel->pushinteger(sqvm, iResult); - return SQRESULT_NOTNULL; -} - -// clang-format off -ADD_SQFUNC("float", GetUserInfoKVFloat_Internal, "entity player, string key, float defaultValue = 0", - "Gets the float value of a given player's userinfo convar by name", ScriptContext::SERVER) -// clang-format on -{ - const R2::CBasePlayer* pPlayer = g_pSquirrel->getentity(sqvm, 1); - if (!pPlayer) - { - g_pSquirrel->raiseerror(sqvm, "player is null"); - return SQRESULT_ERROR; - } - - const char* pKey = g_pSquirrel->getstring(sqvm, 2); - const float flDefaultValue = g_pSquirrel->getfloat(sqvm, 3); - - const float flResult = R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetFloat(pKey, flDefaultValue); - g_pSquirrel->pushfloat(sqvm, flResult); - return SQRESULT_NOTNULL; -} - -// clang-format off -ADD_SQFUNC("bool", GetUserInfoKVBool_Internal, "entity player, string key, bool defaultValue = false", - "Gets the bool value of a given player's userinfo convar by name", ScriptContext::SERVER) -// clang-format on -{ - const R2::CBasePlayer* pPlayer = g_pSquirrel->getentity(sqvm, 1); - if (!pPlayer) - { - g_pSquirrel->raiseerror(sqvm, "player is null"); - return SQRESULT_ERROR; - } - - const char* pKey = g_pSquirrel->getstring(sqvm, 2); - const bool bDefaultValue = g_pSquirrel->getbool(sqvm, 3); - - const bool bResult = R2::g_pClientArray[pPlayer->m_nPlayerIndex - 1].m_ConVars->GetInt(pKey, bDefaultValue); - g_pSquirrel->pushbool(sqvm, bResult); - return SQRESULT_NOTNULL; -} diff --git a/NorthstarDLL/scriptutility.cpp b/NorthstarDLL/scriptutility.cpp deleted file mode 100644 index 1ff8e4bb..00000000 --- a/NorthstarDLL/scriptutility.cpp +++ /dev/null @@ -1,14 +0,0 @@ -#include "pch.h" -#include "squirrel.h" - -// asset function StringToAsset( string assetName ) -ADD_SQFUNC( - "asset", - StringToAsset, - "string assetName", - "converts a given string to an asset", - ScriptContext::UI | ScriptContext::CLIENT | ScriptContext::SERVER) -{ - g_pSquirrel->pushasset(sqvm, g_pSquirrel->getstring(sqvm, 1), -1); - return SQRESULT_NOTNULL; -} diff --git a/NorthstarDLL/server/auth/bansystem.cpp b/NorthstarDLL/server/auth/bansystem.cpp new file mode 100644 index 00000000..56719ed9 --- /dev/null +++ b/NorthstarDLL/server/auth/bansystem.cpp @@ -0,0 +1,222 @@ +#pragma once +#include "pch.h" +#include "bansystem.h" +#include "serverauthentication.h" +#include "shared/maxplayers.h" +#include "core/convar/concommand.h" +#include "server/r2server.h" +#include "engine/r2engine.h" +#include "config/profile.h" + +#include + +const char* BANLIST_PATH_SUFFIX = "/banlist.txt"; +const char BANLIST_COMMENT_CHAR = '#'; + +ServerBanSystem* g_pBanSystem; + +void ServerBanSystem::OpenBanlist() +{ + std::ifstream banlistStream(GetNorthstarPrefix() + "/banlist.txt"); + + if (!banlistStream.fail()) + { + std::string line; + while (std::getline(banlistStream, line)) + { + // ignore line if first char is # or line is empty + if (line == "" || line.front() == BANLIST_COMMENT_CHAR) + continue; + + // remove tabs which shouldnt be there but maybe someone did the funny + line.erase(std::remove(line.begin(), line.end(), '\t'), line.end()); + // remove spaces to allow for spaces before uids + line.erase(std::remove(line.begin(), line.end(), ' '), line.end()); + + // check if line is empty to allow for newlines in the file + if (line == "") + continue; + + // for inline comments like: 123123123 #banned for unfunny + std::string uid = line.substr(0, line.find(BANLIST_COMMENT_CHAR)); + + m_vBannedUids.push_back(strtoull(uid.c_str(), nullptr, 10)); + } + + banlistStream.close(); + } + + // open write stream for banlist // dont do this to allow for all time access + // m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary | std::ofstream::app); +} + +void ServerBanSystem::ReloadBanlist() +{ + std::ifstream fsBanlist(GetNorthstarPrefix() + "/banlist.txt"); + + if (!fsBanlist.fail()) + { + std::string line; + // since we wanna use this as the reload func we need to clear the list + m_vBannedUids.clear(); + while (std::getline(fsBanlist, line)) + m_vBannedUids.push_back(strtoull(line.c_str(), nullptr, 10)); + + fsBanlist.close(); + } +} + +void ServerBanSystem::ClearBanlist() +{ + m_vBannedUids.clear(); + + // reopen the file, don't provide std::ofstream::app so it clears on open + m_sBanlistStream.close(); + m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary); + m_sBanlistStream.close(); +} + +void ServerBanSystem::BanUID(uint64_t uid) +{ + // checking if last char is \n to make sure uids arent getting fucked + std::ifstream fsBanlist(GetNorthstarPrefix() + "/banlist.txt"); + std::string content((std::istreambuf_iterator(fsBanlist)), (std::istreambuf_iterator())); + fsBanlist.close(); + + m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary | std::ofstream::app); + if (content.back() != '\n') + m_sBanlistStream << std::endl; + + m_vBannedUids.push_back(uid); + m_sBanlistStream << std::to_string(uid) << std::endl; + m_sBanlistStream.close(); + spdlog::info("{} was banned", uid); +} + +void ServerBanSystem::UnbanUID(uint64_t uid) +{ + auto findResult = std::find(m_vBannedUids.begin(), m_vBannedUids.end(), uid); + if (findResult == m_vBannedUids.end()) + return; + + m_vBannedUids.erase(findResult); + + std::vector banlistText; + std::ifstream fs_readBanlist(GetNorthstarPrefix() + "/banlist.txt"); + + if (!fs_readBanlist.fail()) + { + std::string line; + while (std::getline(fs_readBanlist, line)) + { + // support for comments and newlines added in https://github.com/R2Northstar/NorthstarLauncher/pull/227 + + std::string modLine = line; // copy the line into a free var that we can fuck with, line will be the original + + // remove tabs which shouldnt be there but maybe someone did the funny + modLine.erase(std::remove(modLine.begin(), modLine.end(), '\t'), modLine.end()); + // remove spaces to allow for spaces before uids + modLine.erase(std::remove(modLine.begin(), modLine.end(), ' '), modLine.end()); + + // ignore line if first char is # or empty line, just add it + if (line.front() == BANLIST_COMMENT_CHAR || modLine == "") + { + banlistText.push_back(line); + continue; + } + + // for inline comments like: 123123123 #banned for unfunny + std::string lineUid = line.substr(0, line.find(BANLIST_COMMENT_CHAR)); + // have to erase spaces or else inline comments will fuck up the uid finding + lineUid.erase(std::remove(lineUid.begin(), lineUid.end(), '\t'), lineUid.end()); + lineUid.erase(std::remove(lineUid.begin(), lineUid.end(), ' '), lineUid.end()); + + // if the uid in the line is the uid we wanna unban + if (std::to_string(uid) == lineUid) + { + // comment the uid out + line.insert(0, "# "); + + // add a comment with unban date + // not necessary but i feel like this makes it better + std::time_t t = std::time(0); + std::tm* now = std::localtime(&t); + + std::ostringstream unbanComment; + + //{y}/{m}/{d} {h}:{m} + unbanComment << " # unban date: "; + unbanComment << now->tm_year + 1900 << "-"; // this lib is so fucking awful + unbanComment << std::setw(2) << std::setfill('0') << now->tm_mon + 1 << "-"; + unbanComment << std::setw(2) << std::setfill('0') << now->tm_mday << " "; + unbanComment << std::setw(2) << std::setfill('0') << now->tm_hour << ":"; + unbanComment << std::setw(2) << std::setfill('0') << now->tm_min; + + line.append(unbanComment.str()); + } + + banlistText.push_back(line); + } + + fs_readBanlist.close(); + } + + // open write stream for banlist // without append so we clear the file + if (m_sBanlistStream.is_open()) + m_sBanlistStream.close(); + m_sBanlistStream.open(GetNorthstarPrefix() + "/banlist.txt", std::ofstream::out | std::ofstream::binary); + + for (std::string updatedLine : banlistText) + m_sBanlistStream << updatedLine << std::endl; + + m_sBanlistStream.close(); + spdlog::info("{} was unbanned", uid); +} + +bool ServerBanSystem::IsUIDAllowed(uint64_t uid) +{ + ReloadBanlist(); // Reload to have up to date list on join + return std::find(m_vBannedUids.begin(), m_vBannedUids.end(), uid) == m_vBannedUids.end(); +} + +void ConCommand_ban(const CCommand& args) +{ + if (args.ArgC() < 2) + return; + + for (int i = 0; i < R2::GetMaxPlayers(); i++) + { + R2::CBaseClient* player = &R2::g_pClientArray[i]; + + if (!strcmp(player->m_Name, args.Arg(1)) || !strcmp(player->m_UID, args.Arg(1))) + { + g_pBanSystem->BanUID(strtoull(player->m_UID, nullptr, 10)); + R2::CBaseClient__Disconnect(player, 1, "Banned from server"); + break; + } + } +} + +void ConCommand_unban(const CCommand& args) +{ + if (args.ArgC() < 2) + return; + + // assumedly the player being unbanned here wasn't already connected, so don't need to iterate over players or anything + g_pBanSystem->UnbanUID(strtoull(args.Arg(1), nullptr, 10)); +} + +void ConCommand_clearbanlist(const CCommand& args) +{ + g_pBanSystem->ClearBanlist(); +} + +ON_DLL_LOAD_RELIESON("engine.dll", BanSystem, ConCommand, (CModule module)) +{ + 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_GAMEDLL); + RegisterConCommand("clearbanlist", ConCommand_clearbanlist, "clears all uids on the banlist", FCVAR_GAMEDLL); +} diff --git a/NorthstarDLL/server/auth/bansystem.h b/NorthstarDLL/server/auth/bansystem.h new file mode 100644 index 00000000..6f180126 --- /dev/null +++ b/NorthstarDLL/server/auth/bansystem.h @@ -0,0 +1,19 @@ +#pragma once +#include + +class ServerBanSystem +{ + private: + std::ofstream m_sBanlistStream; + std::vector m_vBannedUids; + + public: + void OpenBanlist(); + void ReloadBanlist(); + void ClearBanlist(); + void BanUID(uint64_t uid); + void UnbanUID(uint64_t uid); + bool IsUIDAllowed(uint64_t uid); +}; + +extern ServerBanSystem* g_pBanSystem; diff --git a/NorthstarDLL/server/auth/serverauthentication.cpp b/NorthstarDLL/server/auth/serverauthentication.cpp new file mode 100644 index 00000000..ac1d5f17 --- /dev/null +++ b/NorthstarDLL/server/auth/serverauthentication.cpp @@ -0,0 +1,462 @@ +#include "pch.h" +#include "serverauthentication.h" +#include "shared/exploit_fixes/ns_limits.h" +#include "core/convar/cvar.h" +#include "core/convar/convar.h" +#include "masterserver/masterserver.h" +#include "server/serverpresence.h" +#include "engine/hoststate.h" +#include "shared/maxplayers.h" +#include "bansystem.h" +#include "core/convar/concommand.h" +#include "dedicated/dedicated.h" +#include "config/profile.h" +#include "core/tier0.h" +#include "engine/r2engine.h" +#include "client/r2client.h" +#include "server/r2server.h" + +#include "httplib.h" + +#include +#include +#include + +AUTOHOOK_INIT() + +const char* AUTHSERVER_VERIFY_STRING = "I am a northstar server!"; + +// global vars +ServerAuthenticationManager* g_pServerAuthentication; +CBaseServer__RejectConnectionType CBaseServer__RejectConnection; + +void ServerAuthenticationManager::StartPlayerAuthServer() +{ + if (m_bRunningPlayerAuthThread) + { + spdlog::warn("ServerAuthenticationManager::StartPlayerAuthServer was called while m_bRunningPlayerAuthThread is true"); + return; + } + + g_pServerPresence->SetAuthPort(Cvar_ns_player_auth_port->GetInt()); // set auth port for presence + m_bRunningPlayerAuthThread = true; + + // listen is a blocking call so thread this + std::thread serverThread( + [this] + { + // this is just a super basic way to verify that servers have ports open, masterserver will try to read this before ensuring + // server is legit + m_PlayerAuthServer.Get( + "/verify", + [](const httplib::Request& request, httplib::Response& response) + { response.set_content(AUTHSERVER_VERIFY_STRING, "text/plain"); }); + + m_PlayerAuthServer.Post( + "/authenticate_incoming_player", + [this](const httplib::Request& request, httplib::Response& response) + { + if (!request.has_param("id") || !request.has_param("authToken") || request.body.size() >= R2::PERSISTENCE_MAX_SIZE || + !request.has_param("serverAuthToken") || + strcmp(g_pMasterServerManager->m_sOwnServerAuthToken, request.get_param_value("serverAuthToken").c_str())) + { + response.set_content("{\"success\":false}", "application/json"); + return; + } + + RemoteAuthData newAuthData {}; + strncpy_s(newAuthData.uid, sizeof(newAuthData.uid), request.get_param_value("id").c_str(), sizeof(newAuthData.uid) - 1); + strncpy_s( + newAuthData.username, + sizeof(newAuthData.username), + request.get_param_value("username").c_str(), + sizeof(newAuthData.username) - 1); + + newAuthData.pdataSize = request.body.size(); + newAuthData.pdata = new char[newAuthData.pdataSize]; + memcpy(newAuthData.pdata, request.body.c_str(), newAuthData.pdataSize); + + std::lock_guard guard(m_AuthDataMutex); + m_RemoteAuthenticationData.insert(std::make_pair(request.get_param_value("authToken"), newAuthData)); + + response.set_content("{\"success\":true}", "application/json"); + }); + + m_PlayerAuthServer.listen("0.0.0.0", Cvar_ns_player_auth_port->GetInt()); + }); + + serverThread.detach(); +} + +void ServerAuthenticationManager::StopPlayerAuthServer() +{ + if (!m_bRunningPlayerAuthThread) + { + spdlog::warn("ServerAuthenticationManager::StopPlayerAuthServer was called while m_bRunningPlayerAuthThread is false"); + return; + } + + m_bRunningPlayerAuthThread = false; + m_PlayerAuthServer.stop(); +} + +void ServerAuthenticationManager::AddPlayer(R2::CBaseClient* player, const char* pToken) +{ + PlayerAuthenticationData additionalData; + additionalData.pdataSize = m_RemoteAuthenticationData[pToken].pdataSize; + additionalData.usingLocalPdata = player->m_iPersistenceReady == R2::ePersistenceReady::READY_INSECURE; + + m_PlayerAuthenticationData.insert(std::make_pair(player, additionalData)); +} + +void ServerAuthenticationManager::RemovePlayer(R2::CBaseClient* player) +{ + if (m_PlayerAuthenticationData.count(player)) + m_PlayerAuthenticationData.erase(player); +} + +bool checkIsPlayerNameValid(const char* name) +{ + int len = strlen(name); + // Restricts name to max 63 characters + if (len >= 64) + return false; + for (int i = 0; i < len; i++) + { + // Restricts the characters in the name to a restricted range in ASCII + if (static_cast(name[i]) < 32 || static_cast(name[i]) > 126) + { + return false; + } + } + return true; +} + +bool ServerAuthenticationManager::VerifyPlayerName(const char* authToken, const char* name) +{ + std::lock_guard guard(m_AuthDataMutex); + + if (CVar_ns_auth_allow_insecure->GetInt()) + { + spdlog::info("Allowing player with name '{}' because ns_auth_allow_insecure is enabled", name); + return true; + } + + if (!checkIsPlayerNameValid(name)) + { + spdlog::info("Rejecting player with name '{}' because the name contains forbidden characters", name); + return false; + } + // TODO: We should really have a better way of doing this for singleplayer + // Best way of doing this would be to check if server is actually in singleplayer mode, or just running a SP map in multiplayer + // Currently there's not an easy way of checking this, so we just disable this check if mapname starts with `sp_` + // This means that player names are not checked on singleplayer + if ((m_RemoteAuthenticationData.empty() || m_RemoteAuthenticationData.count(std::string(authToken)) == 0) && + strncmp(R2::g_pHostState->m_levelName, "sp_", 3) != 0) + { + spdlog::info("Rejecting player with name '{}' because authToken '{}' was not found", name, authToken); + return false; + } + + const RemoteAuthData& authData = m_RemoteAuthenticationData[authToken]; + + if (*authData.username && strncmp(name, authData.username, 64) != 0) + { + spdlog::info("Rejecting player with name '{}' because name does not match expected name '{}'", name, authData.username); + return false; + } + + return true; +} + +bool ServerAuthenticationManager::CheckDuplicateAccounts(R2::CBaseClient* player) +{ + if (m_bAllowDuplicateAccounts) + return true; + + bool bHasUidPlayer = false; + for (int i = 0; i < R2::GetMaxPlayers(); i++) + if (&R2::g_pClientArray[i] != player && !strcmp(R2::g_pClientArray[i].m_UID, player->m_UID)) + return false; + + return true; +} + +bool ServerAuthenticationManager::AuthenticatePlayer(R2::CBaseClient* player, uint64_t uid, char* authToken) +{ + std::string strUid = std::to_string(uid); + std::lock_guard guard(m_AuthDataMutex); + + if (!strncmp(R2::g_pHostState->m_levelName, "sp_", 3)) + return true; + + // copy uuid + strcpy(player->m_UID, strUid.c_str()); + + bool authFail = true; + if (!m_RemoteAuthenticationData.empty() && m_RemoteAuthenticationData.count(std::string(authToken))) + { + if (!CheckDuplicateAccounts(player)) + return false; + + // use stored auth data + RemoteAuthData authData = m_RemoteAuthenticationData[authToken]; + + if (!strcmp(strUid.c_str(), authData.uid)) // connecting client's uid is the same as auth's uid + { + // if we're resetting let script handle the reset + if (!m_bForceResetLocalPlayerPersistence || strcmp(authData.uid, R2::g_pLocalPlayerUserID)) + { + // copy pdata into buffer + memcpy(player->m_PersistenceBuffer, authData.pdata, authData.pdataSize); + } + + // set persistent data as ready + player->m_iPersistenceReady = R2::ePersistenceReady::READY_REMOTE; + authFail = false; + } + } + + if (authFail) + { + if (CVar_ns_auth_allow_insecure->GetBool()) + { + // set persistent data as ready + // note: actual placeholder persistent data is populated in script with InitPersistentData() + player->m_iPersistenceReady = R2::ePersistenceReady::READY_INSECURE; + return true; + } + else + return false; + } + + return true; // auth successful, client stays on +} + +bool ServerAuthenticationManager::RemovePlayerAuthData(R2::CBaseClient* player) +{ + if (!Cvar_ns_erase_auth_info->GetBool()) // keep auth data forever + return false; + + // hack for special case where we're on a local server, so we erase our own newly created auth data on disconnect + if (m_bNeedLocalAuthForNewgame && !strcmp(player->m_UID, R2::g_pLocalPlayerUserID)) + return false; + + // we don't have our auth token at this point, so lookup authdata by uid + for (auto& auth : m_RemoteAuthenticationData) + { + if (!strcmp(player->m_UID, auth.second.uid)) + { + // pretty sure this is fine, since we don't iterate after the erase + // i think if we iterated after it'd be undefined behaviour tho + std::lock_guard guard(m_AuthDataMutex); + + delete[] auth.second.pdata; + m_RemoteAuthenticationData.erase(auth.first); + return true; + } + } + + return false; +} + +void ServerAuthenticationManager::WritePersistentData(R2::CBaseClient* player) +{ + if (player->m_iPersistenceReady == R2::ePersistenceReady::READY_REMOTE) + { + g_pMasterServerManager->WritePlayerPersistentData( + player->m_UID, (const char*)player->m_PersistenceBuffer, m_PlayerAuthenticationData[player].pdataSize); + } + else if (CVar_ns_auth_allow_insecure_write->GetBool()) + { + // todo: write pdata to disk here + } +} + +// auth hooks + +// store these in vars so we can use them in CBaseClient::Connect +// this is fine because ptrs won't decay by the time we use this, just don't use it outside of calls from cbaseclient::connectclient +char* pNextPlayerToken; +uint64_t iNextPlayerUid; + +// clang-format off +AUTOHOOK(CBaseServer__ConnectClient, engine.dll + 0x114430, +void*,, ( + void* self, + void* addr, + void* a3, + uint32_t a4, + uint32_t a5, + int32_t a6, + void* a7, + char* playerName, + char* serverFilter, + void* a10, + char a11, + void* a12, + char a13, + char a14, + int64_t uid, + uint32_t a16, + uint32_t a17)) +// clang-format on +{ + // auth tokens are sent with serverfilter, can't be accessed from player struct to my knowledge, so have to do this here + pNextPlayerToken = serverFilter; + iNextPlayerUid = uid; + + spdlog::info( + "CBaseServer__ClientConnect attempted connection with uid {}, playerName '{}', serverFilter '{}'", uid, playerName, serverFilter); + + if (!g_pServerAuthentication->VerifyPlayerName(pNextPlayerToken, playerName)) + { + CBaseServer__RejectConnection(self, *((int*)self + 3), addr, "Invalid Name.\n"); + return nullptr; + } + if (!g_pBanSystem->IsUIDAllowed(uid)) + { + CBaseServer__RejectConnection(self, *((int*)self + 3), addr, "Banned From server.\n"); + return nullptr; + } + + return CBaseServer__ConnectClient(self, addr, a3, a4, a5, a6, a7, playerName, serverFilter, a10, a11, a12, a13, a14, uid, a16, a17); +} + +// clang-format off +AUTOHOOK(CBaseClient__Connect, engine.dll + 0x101740, +bool,, (R2::CBaseClient* self, char* name, void* netchan_ptr_arg, char b_fake_player_arg, void* a5, char* Buffer, void* a7)) +// clang-format on +{ + // try to auth player, dc if it fails + // we connect regardless of auth, because returning bad from this function can fuck client state p bad + bool ret = CBaseClient__Connect(self, name, netchan_ptr_arg, b_fake_player_arg, a5, Buffer, a7); + if (!ret) + return ret; + + if (!g_pServerAuthentication->VerifyPlayerName(pNextPlayerToken, name)) + { + R2::CBaseClient__Disconnect(self, 1, "Invalid Name.\n"); + return false; + } + if (!g_pBanSystem->IsUIDAllowed(iNextPlayerUid)) + { + R2::CBaseClient__Disconnect(self, 1, "Banned From server.\n"); + return false; + } + if (!g_pServerAuthentication->AuthenticatePlayer(self, iNextPlayerUid, pNextPlayerToken)) + { + R2::CBaseClient__Disconnect(self, 1, "Authentication Failed.\n"); + return false; + } + + g_pServerAuthentication->AddPlayer(self, pNextPlayerToken); + g_pServerLimits->AddPlayer(self); + + return ret; +} + +// clang-format off +AUTOHOOK(CBaseClient__ActivatePlayer, engine.dll + 0x100F80, +void,, (R2::CBaseClient* self)) +// clang-format on +{ + // if we're authed, write our persistent data + // RemovePlayerAuthData returns true if it removed successfully, i.e. on first call only, and we only want to write on >= second call + // (since this func is called on map loads) + if (self->m_iPersistenceReady >= R2::ePersistenceReady::READY && !g_pServerAuthentication->RemovePlayerAuthData(self)) + { + g_pServerAuthentication->m_bForceResetLocalPlayerPersistence = false; + g_pServerAuthentication->WritePersistentData(self); + g_pServerPresence->SetPlayerCount(g_pServerAuthentication->m_PlayerAuthenticationData.size()); + } + + CBaseClient__ActivatePlayer(self); +} + +// clang-format off +AUTOHOOK(_CBaseClient__Disconnect, engine.dll + 0x1012C0, +void,, (R2::CBaseClient* self, uint32_t unknownButAlways1, const char* pReason, ...)) +// clang-format on +{ + // have to manually format message because can't pass varargs to original func + char buf[1024]; + + va_list va; + va_start(va, pReason); + vsprintf(buf, pReason, va); + va_end(va); + + // this reason is used while connecting to a local server, hacky, but just ignore it + if (strcmp(pReason, "Connection closing")) + { + spdlog::info("Player {} disconnected: \"{}\"", self->m_Name, buf); + + // dcing, write persistent data + if (g_pServerAuthentication->m_PlayerAuthenticationData[self].needPersistenceWriteOnLeave) + g_pServerAuthentication->WritePersistentData(self); + g_pServerAuthentication->RemovePlayerAuthData(self); // won't do anything 99% of the time, but just in case + + g_pServerAuthentication->RemovePlayer(self); + g_pServerLimits->RemovePlayer(self); + } + + else if (g_pServerAuthentication->m_RemoteAuthenticationData.count(self->m_Name)) + { + g_pServerAuthentication->RemovePlayer(self); + g_pServerLimits->RemovePlayer(self); + } + + g_pServerPresence->SetPlayerCount(g_pServerAuthentication->m_PlayerAuthenticationData.size()); + + _CBaseClient__Disconnect(self, unknownButAlways1, buf); +} + +void ConCommand_ns_resetpersistence(const CCommand& args) +{ + if (*R2::g_pServerState == R2::server_state_t::ss_active) + { + spdlog::error("ns_resetpersistence must be entered from the main menu"); + return; + } + + spdlog::info("resetting persistence on next lobby load..."); + g_pServerAuthentication->m_bForceResetLocalPlayerPersistence = true; +} + +ON_DLL_LOAD_RELIESON("engine.dll", ServerAuthentication, (ConCommand, ConVar), (CModule module)) +{ + AUTOHOOK_DISPATCH() + + g_pServerAuthentication = new ServerAuthenticationManager; + + g_pServerAuthentication->Cvar_ns_player_auth_port = new ConVar("ns_player_auth_port", "8081", FCVAR_GAMEDLL, ""); + g_pServerAuthentication->Cvar_ns_erase_auth_info = + new ConVar("ns_erase_auth_info", "1", FCVAR_GAMEDLL, "Whether auth info should be erased from this server on disconnect or crash"); + g_pServerAuthentication->CVar_ns_auth_allow_insecure = + new ConVar("ns_auth_allow_insecure", "0", FCVAR_GAMEDLL, "Whether this server will allow unauthenicated players to connect"); + g_pServerAuthentication->CVar_ns_auth_allow_insecure_write = new ConVar( + "ns_auth_allow_insecure_write", + "0", + FCVAR_GAMEDLL, + "Whether the pdata of unauthenticated clients will be written to disk when changed"); + + RegisterConCommand( + "ns_resetpersistence", ConCommand_ns_resetpersistence, "resets your pdata when you next enter the lobby", FCVAR_NONE); + + // patch to disable kicking based on incorrect serverfilter in connectclient, since we repurpose it for use as an auth token + module.Offset(0x114655).Patch("EB"); + + // patch to disable fairfight marking players as cheaters and kicking them + module.Offset(0x101012).Patch("E9 90 00"); + + CBaseServer__RejectConnection = module.Offset(0x1182E0).As(); + + if (Tier0::CommandLine()->CheckParm("-allowdupeaccounts")) + { + // patch to allow same of multiple account + module.Offset(0x114510).Patch("EB"); + + g_pServerAuthentication->m_bAllowDuplicateAccounts = true; + } +} diff --git a/NorthstarDLL/server/auth/serverauthentication.h b/NorthstarDLL/server/auth/serverauthentication.h new file mode 100644 index 00000000..4ffbbfd6 --- /dev/null +++ b/NorthstarDLL/server/auth/serverauthentication.h @@ -0,0 +1,59 @@ +#pragma once +#include "core/convar/convar.h" +#include "httplib.h" +#include "engine/r2engine.h" +#include +#include + +struct RemoteAuthData +{ + char uid[33]; + char username[64]; + + // pdata + char* pdata; + size_t pdataSize; +}; + +struct PlayerAuthenticationData +{ + bool usingLocalPdata; + size_t pdataSize; + bool needPersistenceWriteOnLeave = true; +}; + +typedef int64_t (*CBaseServer__RejectConnectionType)(void* a1, unsigned int a2, void* a3, const char* a4, ...); +extern CBaseServer__RejectConnectionType CBaseServer__RejectConnection; + +class ServerAuthenticationManager +{ + private: + httplib::Server m_PlayerAuthServer; + + public: + ConVar* Cvar_ns_player_auth_port; + ConVar* Cvar_ns_erase_auth_info; + ConVar* CVar_ns_auth_allow_insecure; + ConVar* CVar_ns_auth_allow_insecure_write; + + std::mutex m_AuthDataMutex; + std::unordered_map m_RemoteAuthenticationData; + std::unordered_map m_PlayerAuthenticationData; + bool m_bAllowDuplicateAccounts = false; + bool m_bRunningPlayerAuthThread = false; + bool m_bNeedLocalAuthForNewgame = false; + bool m_bForceResetLocalPlayerPersistence = false; + + public: + void StartPlayerAuthServer(); + void StopPlayerAuthServer(); + void AddPlayer(R2::CBaseClient* player, const char* pToken); + void RemovePlayer(R2::CBaseClient* player); + bool CheckDuplicateAccounts(R2::CBaseClient* player); + bool AuthenticatePlayer(R2::CBaseClient* player, uint64_t uid, char* authToken); + bool VerifyPlayerName(const char* authToken, const char* name); + bool RemovePlayerAuthData(R2::CBaseClient* player); + void WritePersistentData(R2::CBaseClient* player); +}; + +extern ServerAuthenticationManager* g_pServerAuthentication; diff --git a/NorthstarDLL/server/buildainfile.cpp b/NorthstarDLL/server/buildainfile.cpp new file mode 100644 index 00000000..f6dc4717 --- /dev/null +++ b/NorthstarDLL/server/buildainfile.cpp @@ -0,0 +1,400 @@ +#include "pch.h" +#include "core/convar/convar.h" +#include "engine/hoststate.h" +#include "engine/r2engine.h" + +#include +#include + +AUTOHOOK_INIT() + +const int AINET_VERSION_NUMBER = 57; +const int AINET_SCRIPT_VERSION_NUMBER = 21; +const int PLACEHOLDER_CRC = 0; +const int MAX_HULLS = 5; + +#pragma pack(push, 1) +struct CAI_NodeLink +{ + short srcId; + short destId; + bool hulls[MAX_HULLS]; + char unk0; + char unk1; // maps => unk0 on disk + char unk2[5]; + int64_t flags; +}; +#pragma pack(pop) + +#pragma pack(push, 1) +struct CAI_NodeLinkDisk +{ + short srcId; + short destId; + char unk0; + bool hulls[MAX_HULLS]; +}; +#pragma pack(pop) + +#pragma pack(push, 1) +struct CAI_Node +{ + int index; // not present on disk + float x; + float y; + float z; + float hulls[MAX_HULLS]; + float yaw; + + int unk0; // always 2 in buildainfile, maps directly to unk0 in disk struct + int unk1; // maps directly to unk1 in disk struct + int unk2[MAX_HULLS]; // maps directly to unk2 in disk struct, despite being ints rather than shorts + + // view server.dll+393672 for context and death wish + char unk3[MAX_HULLS]; // hell on earth, should map to unk3 on disk + char pad[3]; // aligns next bytes + float unk4[MAX_HULLS]; // i have no fucking clue, calculated using some kind of demon hell function float magic + + CAI_NodeLink** links; + char unk5[16]; + int linkcount; + int unk11; // bad name lmao + short unk6; // should match up to unk4 on disk + char unk7[16]; // padding until next bit + short unk8; // should match up to unk5 on disk + 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) +struct CAI_NodeDisk +{ + float x; + float y; + float z; + float yaw; + float hulls[MAX_HULLS]; + + char unk0; + int unk1; + short unk2[MAX_HULLS]; + char unk3[MAX_HULLS]; + short unk4; + short unk5; + char unk6[8]; +}; // total size of 68 bytes +#pragma pack(pop) + +#pragma pack(push, 1) +struct UnkNodeStruct0 +{ + int index; + char unk0; + char unk1; // maps to unk1 on disk + char pad0[2]; // padding to +8 + + float x; + float y; + float z; + + char pad5[4]; + int* unk2; // maps to unk5 on disk; + char pad1[16]; // pad to +48 + int unkcount0; // maps to unkcount0 on disk + + char pad2[4]; // pad to +56 + int* unk3; + char pad3[16]; // pad to +80 + int unkcount1; + + char pad4[132]; + char unk5; +}; +#pragma pack(pop) + +int* pUnkStruct0Count; +UnkNodeStruct0*** pppUnkNodeStruct0s; + +#pragma pack(push, 1) +struct UnkLinkStruct1 +{ + short unk0; + short unk1; + int unk2; + char unk3; + char unk4; + char unk5; +}; +#pragma pack(pop) + +int* pUnkLinkStruct1Count; +UnkLinkStruct1*** pppUnkStruct1s; + +#pragma pack(push, 1) +struct CAI_ScriptNode +{ + float x; + float y; + float z; + uint64_t scriptdata; +}; +#pragma pack(pop) + +#pragma pack(push, 1) +struct CAI_Network +{ + // +0 + char unk0[8]; + // +8 + int linkcount; // this is uninitialised and never set on ain build, fun! + // +12 + char unk1[124]; + // +136 + int zonecount; + // +140 + char unk2[16]; + // +156 + int unk5; // unk8 on disk + // +160 + char unk6[4]; + // +164 + int hintcount; + // +168 + short hints[2000]; // these probably aren't actually hints, but there's 1 of them per hint so idk + // +4168 + int scriptnodecount; + // +4172 + CAI_ScriptNode scriptnodes[4000]; + // +84172 + int nodecount; + // +84176 + CAI_Node** nodes; +}; +#pragma pack(pop) + +char** pUnkServerMapversionGlobal; + +ConVar* Cvar_ns_ai_dumpAINfileFromLoad; + +void DumpAINInfo(CAI_Network* aiNetwork) +{ + fs::path writePath(fmt::format("{}/maps/graphs", R2::g_pModName)); + writePath /= R2::g_pHostState->m_levelName; + writePath += ".ain"; + + // dump from memory + spdlog::info("writing ain file {}", writePath.string()); + spdlog::info(""); + spdlog::info(""); + spdlog::info(""); + spdlog::info(""); + spdlog::info(""); + + std::ofstream writeStream(writePath, std::ofstream::binary); + spdlog::info("writing ainet version: {}", AINET_VERSION_NUMBER); + writeStream.write((char*)&AINET_VERSION_NUMBER, sizeof(int)); + + // could probably be cleaner but whatever + int mapVersion = *(int*)(*pUnkServerMapversionGlobal + 104); + spdlog::info("writing map version: {}", mapVersion); // temp + writeStream.write((char*)&mapVersion, sizeof(int)); + spdlog::info("writing placeholder crc: {}", PLACEHOLDER_CRC); + writeStream.write((char*)&PLACEHOLDER_CRC, sizeof(int)); + + int calculatedLinkcount = 0; + + // path nodes + spdlog::info("writing nodecount: {}", aiNetwork->nodecount); + writeStream.write((char*)&aiNetwork->nodecount, sizeof(int)); + + for (int i = 0; i < aiNetwork->nodecount; i++) + { + // construct on-disk node struct + CAI_NodeDisk diskNode; + diskNode.x = aiNetwork->nodes[i]->x; + diskNode.y = aiNetwork->nodes[i]->y; + diskNode.z = aiNetwork->nodes[i]->z; + diskNode.yaw = aiNetwork->nodes[i]->yaw; + memcpy(diskNode.hulls, aiNetwork->nodes[i]->hulls, sizeof(diskNode.hulls)); + diskNode.unk0 = (char)aiNetwork->nodes[i]->unk0; + diskNode.unk1 = aiNetwork->nodes[i]->unk1; + + for (int j = 0; j < MAX_HULLS; j++) + { + diskNode.unk2[j] = (short)aiNetwork->nodes[i]->unk2[j]; + spdlog::info((short)aiNetwork->nodes[i]->unk2[j]); + } + + memcpy(diskNode.unk3, aiNetwork->nodes[i]->unk3, sizeof(diskNode.unk3)); + diskNode.unk4 = aiNetwork->nodes[i]->unk6; + diskNode.unk5 = + -1; // aiNetwork->nodes[i]->unk8; // this field is wrong, however, it's always -1 in vanilla navmeshes anyway, so no biggie + memcpy(diskNode.unk6, aiNetwork->nodes[i]->unk10, sizeof(diskNode.unk6)); + + spdlog::info("writing node {} from {} to {:x}", aiNetwork->nodes[i]->index, (void*)aiNetwork->nodes[i], writeStream.tellp()); + writeStream.write((char*)&diskNode, sizeof(CAI_NodeDisk)); + + calculatedLinkcount += aiNetwork->nodes[i]->linkcount; + } + + // links + spdlog::info("linkcount: {}", aiNetwork->linkcount); + spdlog::info("calculated total linkcount: {}", calculatedLinkcount); + + calculatedLinkcount /= 2; + if (Cvar_ns_ai_dumpAINfileFromLoad->GetBool()) + { + if (aiNetwork->linkcount == calculatedLinkcount) + spdlog::info("caculated linkcount is normal!"); + else + spdlog::warn("calculated linkcount has weird value! this is expected on build!"); + } + + spdlog::info("writing linkcount: {}", calculatedLinkcount); + writeStream.write((char*)&calculatedLinkcount, sizeof(int)); + + for (int i = 0; i < aiNetwork->nodecount; i++) + { + for (int j = 0; j < aiNetwork->nodes[i]->linkcount; j++) + { + // skip links that don't originate from current node + if (aiNetwork->nodes[i]->links[j]->srcId != aiNetwork->nodes[i]->index) + continue; + + CAI_NodeLinkDisk diskLink; + diskLink.srcId = aiNetwork->nodes[i]->links[j]->srcId; + diskLink.destId = aiNetwork->nodes[i]->links[j]->destId; + diskLink.unk0 = aiNetwork->nodes[i]->links[j]->unk1; + memcpy(diskLink.hulls, aiNetwork->nodes[i]->links[j]->hulls, sizeof(diskLink.hulls)); + + spdlog::info("writing link {} => {} to {:x}", diskLink.srcId, diskLink.destId, writeStream.tellp()); + writeStream.write((char*)&diskLink, sizeof(CAI_NodeLinkDisk)); + } + } + + // don't know what this is, it's likely a block from tf1 that got deprecated? should just be 1 int per node + spdlog::info("writing {:x} bytes for unknown block at {:x}", aiNetwork->nodecount * sizeof(uint32_t), writeStream.tellp()); + uint32_t* unkNodeBlock = new uint32_t[aiNetwork->nodecount]; + memset(unkNodeBlock, 0, aiNetwork->nodecount * sizeof(uint32_t)); + writeStream.write((char*)unkNodeBlock, aiNetwork->nodecount * sizeof(uint32_t)); + delete[] unkNodeBlock; + + // TODO: this is traverse nodes i think? these aren't used in tf2 ains so we can get away with just writing count=0 and skipping + // but ideally should actually dump these + spdlog::info("writing {} traversal nodes at {:x}...", 0, writeStream.tellp()); + short traverseNodeCount = 0; + writeStream.write((char*)&traverseNodeCount, sizeof(short)); + // only write count since count=0 means we don't have to actually do anything here + + // TODO: ideally these should be actually dumped, but they're always 0 in tf2 from what i can tell + spdlog::info("writing {} bytes for unknown hull block at {:x}", MAX_HULLS * 8, writeStream.tellp()); + char* unkHullBlock = new char[MAX_HULLS * 8]; + memset(unkHullBlock, 0, MAX_HULLS * 8); + writeStream.write(unkHullBlock, MAX_HULLS * 8); + delete[] unkHullBlock; + + // unknown struct that's seemingly node-related + spdlog::info("writing {} unknown node structs at {:x}", *pUnkStruct0Count, writeStream.tellp()); + writeStream.write((char*)pUnkStruct0Count, sizeof(*pUnkStruct0Count)); + for (int i = 0; i < *pUnkStruct0Count; i++) + { + spdlog::info("writing unknown node struct {} at {:x}", i, writeStream.tellp()); + UnkNodeStruct0* nodeStruct = (*pppUnkNodeStruct0s)[i]; + + writeStream.write((char*)&nodeStruct->index, sizeof(nodeStruct->index)); + writeStream.write((char*)&nodeStruct->unk1, sizeof(nodeStruct->unk1)); + + writeStream.write((char*)&nodeStruct->x, sizeof(nodeStruct->x)); + writeStream.write((char*)&nodeStruct->y, sizeof(nodeStruct->y)); + writeStream.write((char*)&nodeStruct->z, sizeof(nodeStruct->z)); + + writeStream.write((char*)&nodeStruct->unkcount0, sizeof(nodeStruct->unkcount0)); + for (int j = 0; j < nodeStruct->unkcount0; j++) + { + short unk2Short = (short)nodeStruct->unk2[j]; + writeStream.write((char*)&unk2Short, sizeof(unk2Short)); + } + + writeStream.write((char*)&nodeStruct->unkcount1, sizeof(nodeStruct->unkcount1)); + for (int j = 0; j < nodeStruct->unkcount1; j++) + { + short unk3Short = (short)nodeStruct->unk3[j]; + writeStream.write((char*)&unk3Short, sizeof(unk3Short)); + } + + writeStream.write((char*)&nodeStruct->unk5, sizeof(nodeStruct->unk5)); + } + + // unknown struct that's seemingly link-related + spdlog::info("writing {} unknown link structs at {:x}", *pUnkLinkStruct1Count, writeStream.tellp()); + writeStream.write((char*)pUnkLinkStruct1Count, sizeof(*pUnkLinkStruct1Count)); + for (int i = 0; i < *pUnkLinkStruct1Count; i++) + { + // disk and memory structs are literally identical here so just directly write + spdlog::info("writing unknown link struct {} at {:x}", i, writeStream.tellp()); + writeStream.write((char*)(*pppUnkStruct1s)[i], sizeof(*(*pppUnkStruct1s)[i])); + } + + // some weird int idk what this is used for + writeStream.write((char*)&aiNetwork->unk5, sizeof(aiNetwork->unk5)); + + // tf2-exclusive stuff past this point, i.e. ain v57 only + spdlog::info("writing {} script nodes at {:x}", aiNetwork->scriptnodecount, writeStream.tellp()); + writeStream.write((char*)&aiNetwork->scriptnodecount, sizeof(aiNetwork->scriptnodecount)); + for (int i = 0; i < aiNetwork->scriptnodecount; i++) + { + // disk and memory structs are literally identical here so just directly write + spdlog::info("writing script node {} at {:x}", i, writeStream.tellp()); + writeStream.write((char*)&aiNetwork->scriptnodes[i], sizeof(aiNetwork->scriptnodes[i])); + } + + spdlog::info("writing {} hints at {:x}", aiNetwork->hintcount, writeStream.tellp()); + writeStream.write((char*)&aiNetwork->hintcount, sizeof(aiNetwork->hintcount)); + for (int i = 0; i < aiNetwork->hintcount; i++) + { + spdlog::info("writing hint data {} at {:x}", i, writeStream.tellp()); + writeStream.write((char*)&aiNetwork->hints[i], sizeof(aiNetwork->hints[i])); + } + + writeStream.close(); +} + +// 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); +} + +// clang-format off +AUTOHOOK(LoadAINFile, server.dll + 0x3933A0, +void, __fastcall, (void* aimanager, void* buf, const char* filename)) +// clang-format on +{ + LoadAINFile(aimanager, buf, filename); + + if (Cvar_ns_ai_dumpAINfileFromLoad->GetBool()) + { + spdlog::info("running DumpAINInfo for loaded file {}", filename); + DumpAINInfo(*(CAI_Network**)((char*)aimanager + 2536)); + } +} + +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"); + + pUnkStruct0Count = module.Offset(0x1063BF8).As(); + pppUnkNodeStruct0s = module.Offset(0x1063BE0).As(); + pUnkLinkStruct1Count = module.Offset(0x1063AA8).As(); + pppUnkStruct1s = module.Offset(0x1063A90).As(); + pUnkServerMapversionGlobal = module.Offset(0xBFBE08).As(); +} diff --git a/NorthstarDLL/server/r2server.cpp b/NorthstarDLL/server/r2server.cpp new file mode 100644 index 00000000..50cfa239 --- /dev/null +++ b/NorthstarDLL/server/r2server.cpp @@ -0,0 +1,17 @@ +#include "pch.h" +#include "r2server.h" + +using namespace R2; + +// use the R2 namespace for game funcs +namespace R2 +{ + CBaseEntity* (*Server_GetEntityByIndex)(int index); + CBasePlayer*(__fastcall* UTIL_PlayerByIndex)(int playerIndex); +} // namespace R2 + +ON_DLL_LOAD("server.dll", R2GameServer, (CModule module)) +{ + Server_GetEntityByIndex = module.Offset(0xFB820).As(); + UTIL_PlayerByIndex = module.Offset(0x26AA10).As(); +} diff --git a/NorthstarDLL/server/r2server.h b/NorthstarDLL/server/r2server.h new file mode 100644 index 00000000..313284be --- /dev/null +++ b/NorthstarDLL/server/r2server.h @@ -0,0 +1,28 @@ +#pragma once + +#include "core/math/vector.h" + +// use the R2 namespace for game funcs +namespace R2 +{ + // server entity stuff + class CBaseEntity; + extern CBaseEntity* (*Server_GetEntityByIndex)(int index); + + // clang-format off + OFFSET_STRUCT(CBasePlayer) + { + STRUCT_SIZE(0x1D02); + FIELD(0x58, uint32_t m_nPlayerIndex) + + FIELD(0x1C90, bool m_hasBadReputation) + FIELD(0x1C91, char m_communityName[64]) + FIELD(0x1CD1, char m_communityClanTag[16]) + FIELD(0x1CE1, char m_factionName[16]) + FIELD(0x1CF1, char m_hardwareIcon[16]) + FIELD(0x1D01, bool m_happyHourActive) + }; + // clang-format on + + extern CBasePlayer*(__fastcall* UTIL_PlayerByIndex)(int playerIndex); +} // namespace R2 diff --git a/NorthstarDLL/server/serverchathooks.cpp b/NorthstarDLL/server/serverchathooks.cpp new file mode 100644 index 00000000..57c2c31a --- /dev/null +++ b/NorthstarDLL/server/serverchathooks.cpp @@ -0,0 +1,172 @@ +#include "pch.h" +#include "serverchathooks.h" +#include "shared/exploit_fixes/ns_limits.h" +#include "squirrel/squirrel.h" +#include "server/r2server.h" + +#include +#include +#include + +AUTOHOOK_INIT() + +class CServerGameDLL; + +class CRecipientFilter +{ + char unknown[58]; +}; + +CServerGameDLL* g_pServerGameDLL; + +void(__fastcall* CServerGameDLL__OnReceivedSayTextMessage)( + CServerGameDLL* self, unsigned int senderPlayerId, const char* text, int channelId); + +void(__fastcall* CRecipientFilter__Construct)(CRecipientFilter* self); +void(__fastcall* CRecipientFilter__Destruct)(CRecipientFilter* self); +void(__fastcall* CRecipientFilter__AddAllPlayers)(CRecipientFilter* self); +void(__fastcall* CRecipientFilter__AddRecipient)(CRecipientFilter* self, const R2::CBasePlayer* player); +void(__fastcall* CRecipientFilter__MakeReliable)(CRecipientFilter* self); + +void(__fastcall* UserMessageBegin)(CRecipientFilter* filter, const char* messagename); +void(__fastcall* MessageEnd)(); +void(__fastcall* MessageWriteByte)(int iValue); +void(__fastcall* MessageWriteString)(const char* sz); +void(__fastcall* MessageWriteBool)(bool bValue); + +bool bShouldCallSayTextHook = false; +// clang-format off +AUTOHOOK(_CServerGameDLL__OnReceivedSayTextMessage, server.dll + 0x1595C0, +void, __fastcall, (CServerGameDLL* self, unsigned int senderPlayerId, const char* text, bool isTeam)) +// clang-format on +{ + // MiniHook doesn't allow calling the base function outside of anywhere but the hook function. + // To allow bypassing the hook, isSkippingHook can be set. + if (bShouldCallSayTextHook) + { + bShouldCallSayTextHook = false; + _CServerGameDLL__OnReceivedSayTextMessage(self, senderPlayerId, text, isTeam); + return; + } + + // check chat ratelimits + if (!g_pServerLimits->CheckChatLimits(&R2::g_pClientArray[senderPlayerId - 1])) + return; + + SQRESULT result = g_pSquirrel->Call( + "CServerGameDLL_ProcessMessageStartThread", static_cast(senderPlayerId) - 1, text, isTeam); + + if (result == SQRESULT_ERROR) + _CServerGameDLL__OnReceivedSayTextMessage(self, senderPlayerId, text, isTeam); +} + +void ChatSendMessage(unsigned int playerIndex, const char* text, bool isTeam) +{ + bShouldCallSayTextHook = true; + CServerGameDLL__OnReceivedSayTextMessage( + g_pServerGameDLL, + // Ensure the first bit isn't set, since this indicates a custom message + (playerIndex + 1) & CUSTOM_MESSAGE_INDEX_MASK, + text, + isTeam); +} + +void ChatBroadcastMessage(int fromPlayerIndex, int toPlayerIndex, const char* text, bool isTeam, bool isDead, CustomMessageType messageType) +{ + R2::CBasePlayer* toPlayer = NULL; + if (toPlayerIndex >= 0) + { + toPlayer = R2::UTIL_PlayerByIndex(toPlayerIndex + 1); + if (toPlayer == NULL) + return; + } + + // Build a new string where the first byte is the message type + char sendText[256]; + sendText[0] = (char)messageType; + strncpy_s(sendText + 1, 255, text, 254); + + // Anonymous custom messages use playerId=0, non-anonymous ones use a player ID with the first bit set + unsigned int fromPlayerId = fromPlayerIndex < 0 ? 0 : ((fromPlayerIndex + 1) | CUSTOM_MESSAGE_INDEX_BIT); + + CRecipientFilter filter; + CRecipientFilter__Construct(&filter); + if (toPlayer == NULL) + { + CRecipientFilter__AddAllPlayers(&filter); + } + else + { + CRecipientFilter__AddRecipient(&filter, toPlayer); + } + CRecipientFilter__MakeReliable(&filter); + + UserMessageBegin(&filter, "SayText"); + MessageWriteByte(fromPlayerId); + MessageWriteString(sendText); + MessageWriteBool(isTeam); + MessageWriteBool(isDead); + MessageEnd(); + + CRecipientFilter__Destruct(&filter); +} + +ADD_SQFUNC("void", NSSendMessage, "int playerIndex, string text, bool isTeam", "", ScriptContext::SERVER) +{ + int playerIndex = g_pSquirrel->getinteger(sqvm, 1); + const char* text = g_pSquirrel->getstring(sqvm, 2); + bool isTeam = g_pSquirrel->getbool(sqvm, 3); + + ChatSendMessage(playerIndex, text, isTeam); + + return SQRESULT_NULL; +} + +ADD_SQFUNC( + "void", + NSBroadcastMessage, + "int fromPlayerIndex, int toPlayerIndex, string text, bool isTeam, bool isDead, int messageType", + "", + ScriptContext::SERVER) +{ + int fromPlayerIndex = g_pSquirrel->getinteger(sqvm, 1); + int toPlayerIndex = g_pSquirrel->getinteger(sqvm, 2); + const char* text = g_pSquirrel->getstring(sqvm, 3); + bool isTeam = g_pSquirrel->getbool(sqvm, 4); + bool isDead = g_pSquirrel->getbool(sqvm, 5); + int messageType = g_pSquirrel->getinteger(sqvm, 6); + + if (messageType < 1) + { + g_pSquirrel->raiseerror(sqvm, fmt::format("Invalid message type {}", messageType).c_str()); + return SQRESULT_ERROR; + } + + ChatBroadcastMessage(fromPlayerIndex, toPlayerIndex, text, isTeam, isDead, (CustomMessageType)messageType); + + return SQRESULT_NULL; +} + +ON_DLL_LOAD("engine.dll", EngineServerChatHooks, (CModule module)) +{ + g_pServerGameDLL = module.Offset(0x13F0AA98).As(); +} + +ON_DLL_LOAD_RELIESON("server.dll", ServerChatHooks, ServerSquirrel, (CModule module)) +{ + AUTOHOOK_DISPATCH_MODULE(server.dll) + + CServerGameDLL__OnReceivedSayTextMessage = + module.Offset(0x1595C0).As(); + CRecipientFilter__Construct = module.Offset(0x1E9440).As(); + CRecipientFilter__Destruct = module.Offset(0x1E9700).As(); + CRecipientFilter__AddAllPlayers = module.Offset(0x1E9940).As(); + CRecipientFilter__AddRecipient = module.Offset(0x1E9B30).As(); + CRecipientFilter__MakeReliable = module.Offset(0x1EA4E0).As(); + + UserMessageBegin = module.Offset(0x15C520).As(); + MessageEnd = module.Offset(0x158880).As(); + MessageWriteByte = module.Offset(0x158A90).As(); + MessageWriteString = module.Offset(0x158D00).As(); + MessageWriteBool = module.Offset(0x158A00).As(); +} diff --git a/NorthstarDLL/server/serverchathooks.h b/NorthstarDLL/server/serverchathooks.h new file mode 100644 index 00000000..1d8a806a --- /dev/null +++ b/NorthstarDLL/server/serverchathooks.h @@ -0,0 +1,25 @@ +#pragma once +#include "pch.h" +#include +#include + +enum class CustomMessageType : char +{ + Chat = 1, + Whisper = 2 +}; + +constexpr unsigned char CUSTOM_MESSAGE_INDEX_BIT = 0b10000000; +constexpr unsigned char CUSTOM_MESSAGE_INDEX_MASK = ~CUSTOM_MESSAGE_INDEX_BIT; + +// Send a vanilla chat message as if it was from the player. +void ChatSendMessage(unsigned int playerIndex, const char* text, bool isteam); + +// Send a custom message. +// fromPlayerIndex: set to -1 for a [SERVER] message, or another value to send from a specific player +// toPlayerIndex: set to -1 to send to all players, or another value to send to a single player +// isTeam: display a [TEAM] badge +// isDead: display a [DEAD] badge +// messageType: send a specific message type +void ChatBroadcastMessage( + int fromPlayerIndex, int toPlayerIndex, const char* text, bool isTeam, bool isDead, CustomMessageType messageType); diff --git a/NorthstarDLL/server/serverpresence.cpp b/NorthstarDLL/server/serverpresence.cpp new file mode 100644 index 00000000..bda5e7fe --- /dev/null +++ b/NorthstarDLL/server/serverpresence.cpp @@ -0,0 +1,237 @@ +#include "pch.h" +#include "serverpresence.h" +#include "shared/playlist.h" +#include "core/tier0.h" +#include "core/convar/convar.h" + +#include + +ServerPresenceManager* g_pServerPresence; + +ConVar* Cvar_hostname; + +// Convert a hex digit char to integer. +inline int hctod(char c) +{ + if (c >= 'A' && c <= 'F') + { + return c - 'A' + 10; + } + else if (c >= 'a' && c <= 'f') + { + return c - 'a' + 10; + } + else + { + return c - '0'; + } +} + +// This function interprets all 4-hexadecimal-digit unicode codepoint characters like \u4E2D to UTF-8 encoding. +std::string UnescapeUnicode(const std::string& str) +{ + std::string result; + + std::regex r("\\\\u([a-f\\d]{4})", std::regex::icase); + auto matches_begin = std::sregex_iterator(str.begin(), str.end(), r); + auto matches_end = std::sregex_iterator(); + std::smatch last_match; + + for (std::sregex_iterator i = matches_begin; i != matches_end; ++i) + { + last_match = *i; + result.append(last_match.prefix()); + unsigned int cp = 0; + for (int i = 2; i <= 5; ++i) + { + cp *= 16; + cp += hctod(last_match.str()[i]); + } + if (cp <= 0x7F) + { + result.push_back(cp); + } + else if (cp <= 0x7FF) + { + result.push_back((cp >> 6) | 0b11000000 & (~(1 << 5))); + result.push_back(cp & ((1 << 6) - 1) | 0b10000000 & (~(1 << 6))); + } + else if (cp <= 0xFFFF) + { + result.push_back((cp >> 12) | 0b11100000 & (~(1 << 4))); + result.push_back((cp >> 6) & ((1 << 6) - 1) | 0b10000000 & (~(1 << 6))); + result.push_back(cp & ((1 << 6) - 1) | 0b10000000 & (~(1 << 6))); + } + } + + if (!last_match.ready()) + return str; + else + result.append(last_match.suffix()); + + return result; +} + +ServerPresenceManager::ServerPresenceManager() +{ + // clang-format off + // register convars + Cvar_ns_server_presence_update_rate = new ConVar( + "ns_server_presence_update_rate", "5000", FCVAR_GAMEDLL, "How often we update our server's presence on server lists in ms"); + + Cvar_ns_server_name = new ConVar("ns_server_name", "Unnamed Northstar Server", FCVAR_GAMEDLL, "This server's description", false, 0, false, 0, [](ConVar* cvar, const char* pOldValue, float flOldValue) { + g_pServerPresence->SetName(UnescapeUnicode(g_pServerPresence->Cvar_ns_server_name->GetString())); + + // update engine hostname cvar + Cvar_hostname->SetValue(g_pServerPresence->Cvar_ns_server_name->GetString()); + }); + + Cvar_ns_server_desc = new ConVar("ns_server_desc", "Default server description", FCVAR_GAMEDLL, "This server's name", false, 0, false, 0, [](ConVar* cvar, const char* pOldValue, float flOldValue) { + g_pServerPresence->SetDescription(UnescapeUnicode(g_pServerPresence->Cvar_ns_server_desc->GetString())); + }); + + Cvar_ns_server_password = new ConVar("ns_server_password", "", FCVAR_GAMEDLL, "This server's password", false, 0, false, 0, [](ConVar* cvar, const char* pOldValue, float flOldValue) { + g_pServerPresence->SetPassword(g_pServerPresence->Cvar_ns_server_password->GetString()); + }); + + Cvar_ns_report_server_to_masterserver = new ConVar("ns_report_server_to_masterserver", "1", FCVAR_GAMEDLL, "Whether we should report this server to the masterserver"); + Cvar_ns_report_sp_server_to_masterserver = new ConVar("ns_report_sp_server_to_masterserver", "0", FCVAR_GAMEDLL, "Whether we should report this server to the masterserver, when started in singleplayer"); + // clang-format on +} + +void ServerPresenceManager::AddPresenceReporter(ServerPresenceReporter* reporter) +{ + m_vPresenceReporters.push_back(reporter); +} + +void ServerPresenceManager::CreatePresence() +{ + // reset presence fields that rely on runtime server state + // these being: port/auth port, map/playlist name, and playercount/maxplayers + m_ServerPresence.m_iPort = 0; + m_ServerPresence.m_iAuthPort = 0; + + m_ServerPresence.m_iPlayerCount = 0; // this should actually be 0 at this point, so shouldn't need updating later + m_ServerPresence.m_iMaxPlayers = 0; + + memset(m_ServerPresence.m_MapName, 0, sizeof(m_ServerPresence.m_MapName)); + memset(m_ServerPresence.m_PlaylistName, 0, sizeof(m_ServerPresence.m_PlaylistName)); + m_ServerPresence.m_bIsSingleplayerServer = false; + + m_bHasPresence = true; + m_bFirstPresenceUpdate = true; + + // code that's calling this should set up the reset fields at this point +} + +void ServerPresenceManager::DestroyPresence() +{ + m_bHasPresence = false; + + for (ServerPresenceReporter* reporter : m_vPresenceReporters) + reporter->DestroyPresence(&m_ServerPresence); +} + +void ServerPresenceManager::RunFrame(double flCurrentTime) +{ + if (!m_bHasPresence || !Cvar_ns_report_server_to_masterserver->GetBool()) // don't run until we actually have server presence + return; + + // don't run if we're sp and don't want to report sp + if (m_ServerPresence.m_bIsSingleplayerServer && !Cvar_ns_report_sp_server_to_masterserver->GetBool()) + return; + + // Call RunFrame() so that reporters can, for example, handle std::future results as soon as they arrive. + for (ServerPresenceReporter* reporter : m_vPresenceReporters) + reporter->RunFrame(flCurrentTime, &m_ServerPresence); + + // run on a specified delay + if ((flCurrentTime - m_flLastPresenceUpdate) * 1000 < Cvar_ns_server_presence_update_rate->GetFloat()) + return; + + // is this the first frame we're updating this presence? + if (m_bFirstPresenceUpdate) + { + // let reporters setup/clear any state + for (ServerPresenceReporter* reporter : m_vPresenceReporters) + reporter->CreatePresence(&m_ServerPresence); + + m_bFirstPresenceUpdate = false; + } + + m_flLastPresenceUpdate = flCurrentTime; + + for (ServerPresenceReporter* reporter : m_vPresenceReporters) + reporter->ReportPresence(&m_ServerPresence); +} + +void ServerPresenceManager::SetPort(const int iPort) +{ + // update port + m_ServerPresence.m_iPort = iPort; +} + +void ServerPresenceManager::SetAuthPort(const int iAuthPort) +{ + // update authport + m_ServerPresence.m_iAuthPort = iAuthPort; +} + +void ServerPresenceManager::SetName(const std::string sServerNameUnicode) +{ + // update name + m_ServerPresence.m_sServerName = sServerNameUnicode; +} + +void ServerPresenceManager::SetDescription(const std::string sServerDescUnicode) +{ + // update desc + m_ServerPresence.m_sServerDesc = sServerDescUnicode; +} + +void ServerPresenceManager::SetPassword(const char* pPassword) +{ + // update password + strncpy_s(m_ServerPresence.m_Password, sizeof(m_ServerPresence.m_Password), pPassword, sizeof(m_ServerPresence.m_Password) - 1); +} + +void ServerPresenceManager::SetMap(const char* pMapName, bool isInitialising) +{ + // if the server is initialising (i.e. this is first map) on sp, set the server to sp + if (isInitialising) + m_ServerPresence.m_bIsSingleplayerServer = !strncmp(pMapName, "sp_", 3); + + // update map + strncpy_s(m_ServerPresence.m_MapName, sizeof(m_ServerPresence.m_MapName), pMapName, sizeof(m_ServerPresence.m_MapName) - 1); +} + +void ServerPresenceManager::SetPlaylist(const char* pPlaylistName) +{ + // update playlist + strncpy_s( + m_ServerPresence.m_PlaylistName, + sizeof(m_ServerPresence.m_PlaylistName), + pPlaylistName, + sizeof(m_ServerPresence.m_PlaylistName) - 1); + + // update maxplayers + const char* pMaxPlayers = R2::GetCurrentPlaylistVar("max_players", true); + + // can be null in some situations, so default 6 + if (pMaxPlayers) + m_ServerPresence.m_iMaxPlayers = std::stoi(pMaxPlayers); + else + m_ServerPresence.m_iMaxPlayers = 6; +} + +void ServerPresenceManager::SetPlayerCount(const int iPlayerCount) +{ + m_ServerPresence.m_iPlayerCount = iPlayerCount; +} + +ON_DLL_LOAD_RELIESON("engine.dll", ServerPresence, ConVar, (CModule module)) +{ + g_pServerPresence = new ServerPresenceManager; + + Cvar_hostname = module.Offset(0x1315BAE8).Deref().As(); +} diff --git a/NorthstarDLL/server/serverpresence.h b/NorthstarDLL/server/serverpresence.h new file mode 100644 index 00000000..125b2e68 --- /dev/null +++ b/NorthstarDLL/server/serverpresence.h @@ -0,0 +1,92 @@ +#pragma once +#include "core/convar/convar.h" + +struct ServerPresence +{ + int m_iPort; + int m_iAuthPort; + + std::string m_sServerName; + std::string m_sServerDesc; + char m_Password[256]; // probably bigger than will ever be used in practice, lol + + char m_MapName[32]; + char m_PlaylistName[64]; + bool m_bIsSingleplayerServer; // whether the server started in sp + + int m_iPlayerCount; + int m_iMaxPlayers; + + ServerPresence() + { + memset(this, 0, sizeof(this)); + } + + ServerPresence(const ServerPresence* obj) + { + m_iPort = obj->m_iPort; + m_iAuthPort = obj->m_iAuthPort; + + m_sServerName = obj->m_sServerName; + m_sServerDesc = obj->m_sServerDesc; + memcpy(m_Password, obj->m_Password, sizeof(m_Password)); + + memcpy(m_MapName, obj->m_MapName, sizeof(m_MapName)); + memcpy(m_PlaylistName, obj->m_PlaylistName, sizeof(m_PlaylistName)); + + m_iPlayerCount = obj->m_iPlayerCount; + m_iMaxPlayers = obj->m_iMaxPlayers; + } +}; + +class ServerPresenceReporter +{ + public: + virtual void CreatePresence(const ServerPresence* pServerPresence) {} + virtual void ReportPresence(const ServerPresence* pServerPresence) {} + virtual void DestroyPresence(const ServerPresence* pServerPresence) {} + virtual void RunFrame(double flCurrentTime, const ServerPresence* pServerPresence) {} +}; + +class ServerPresenceManager +{ + private: + ServerPresence m_ServerPresence; + + bool m_bHasPresence = false; + bool m_bFirstPresenceUpdate = false; + + std::vector m_vPresenceReporters; + + double m_flLastPresenceUpdate = 0; + ConVar* Cvar_ns_server_presence_update_rate; + + ConVar* Cvar_ns_server_name; + ConVar* Cvar_ns_server_desc; + ConVar* Cvar_ns_server_password; + + ConVar* Cvar_ns_report_server_to_masterserver; + ConVar* Cvar_ns_report_sp_server_to_masterserver; + + public: + ServerPresenceManager(); + + void AddPresenceReporter(ServerPresenceReporter* reporter); + + void CreatePresence(); + void DestroyPresence(); + void RunFrame(double flCurrentTime); + + void SetPort(const int iPort); + void SetAuthPort(const int iPort); + + void SetName(const std::string sServerNameUnicode); + void SetDescription(const std::string sServerDescUnicode); + void SetPassword(const char* pPassword); + + void SetMap(const char* pMapName, bool isInitialising = false); + void SetPlaylist(const char* pPlaylistName); + void SetPlayerCount(const int iPlayerCount); +}; + +extern ServerPresenceManager* g_pServerPresence; diff --git a/NorthstarDLL/serverauthentication.cpp b/NorthstarDLL/serverauthentication.cpp deleted file mode 100644 index 350d8521..00000000 --- a/NorthstarDLL/serverauthentication.cpp +++ /dev/null @@ -1,462 +0,0 @@ -#include "pch.h" -#include "serverauthentication.h" -#include "limits.h" -#include "cvar.h" -#include "convar.h" -#include "masterserver.h" -#include "serverpresence.h" -#include "hoststate.h" -#include "maxplayers.h" -#include "bansystem.h" -#include "concommand.h" -#include "dedicated.h" -#include "nsprefix.h" -#include "tier0.h" -#include "r2engine.h" -#include "r2client.h" -#include "r2server.h" - -#include "httplib.h" - -#include -#include -#include - -AUTOHOOK_INIT() - -const char* AUTHSERVER_VERIFY_STRING = "I am a northstar server!"; - -// global vars -ServerAuthenticationManager* g_pServerAuthentication; -CBaseServer__RejectConnectionType CBaseServer__RejectConnection; - -void ServerAuthenticationManager::StartPlayerAuthServer() -{ - if (m_bRunningPlayerAuthThread) - { - spdlog::warn("ServerAuthenticationManager::StartPlayerAuthServer was called while m_bRunningPlayerAuthThread is true"); - return; - } - - g_pServerPresence->SetAuthPort(Cvar_ns_player_auth_port->GetInt()); // set auth port for presence - m_bRunningPlayerAuthThread = true; - - // listen is a blocking call so thread this - std::thread serverThread( - [this] - { - // this is just a super basic way to verify that servers have ports open, masterserver will try to read this before ensuring - // server is legit - m_PlayerAuthServer.Get( - "/verify", - [](const httplib::Request& request, httplib::Response& response) - { response.set_content(AUTHSERVER_VERIFY_STRING, "text/plain"); }); - - m_PlayerAuthServer.Post( - "/authenticate_incoming_player", - [this](const httplib::Request& request, httplib::Response& response) - { - if (!request.has_param("id") || !request.has_param("authToken") || request.body.size() >= R2::PERSISTENCE_MAX_SIZE || - !request.has_param("serverAuthToken") || - strcmp(g_pMasterServerManager->m_sOwnServerAuthToken, request.get_param_value("serverAuthToken").c_str())) - { - response.set_content("{\"success\":false}", "application/json"); - return; - } - - RemoteAuthData newAuthData {}; - strncpy_s(newAuthData.uid, sizeof(newAuthData.uid), request.get_param_value("id").c_str(), sizeof(newAuthData.uid) - 1); - strncpy_s( - newAuthData.username, - sizeof(newAuthData.username), - request.get_param_value("username").c_str(), - sizeof(newAuthData.username) - 1); - - newAuthData.pdataSize = request.body.size(); - newAuthData.pdata = new char[newAuthData.pdataSize]; - memcpy(newAuthData.pdata, request.body.c_str(), newAuthData.pdataSize); - - std::lock_guard guard(m_AuthDataMutex); - m_RemoteAuthenticationData.insert(std::make_pair(request.get_param_value("authToken"), newAuthData)); - - response.set_content("{\"success\":true}", "application/json"); - }); - - m_PlayerAuthServer.listen("0.0.0.0", Cvar_ns_player_auth_port->GetInt()); - }); - - serverThread.detach(); -} - -void ServerAuthenticationManager::StopPlayerAuthServer() -{ - if (!m_bRunningPlayerAuthThread) - { - spdlog::warn("ServerAuthenticationManager::StopPlayerAuthServer was called while m_bRunningPlayerAuthThread is false"); - return; - } - - m_bRunningPlayerAuthThread = false; - m_PlayerAuthServer.stop(); -} - -void ServerAuthenticationManager::AddPlayer(R2::CBaseClient* player, const char* pToken) -{ - PlayerAuthenticationData additionalData; - additionalData.pdataSize = m_RemoteAuthenticationData[pToken].pdataSize; - additionalData.usingLocalPdata = player->m_iPersistenceReady == R2::ePersistenceReady::READY_INSECURE; - - m_PlayerAuthenticationData.insert(std::make_pair(player, additionalData)); -} - -void ServerAuthenticationManager::RemovePlayer(R2::CBaseClient* player) -{ - if (m_PlayerAuthenticationData.count(player)) - m_PlayerAuthenticationData.erase(player); -} - -bool checkIsPlayerNameValid(const char* name) -{ - int len = strlen(name); - // Restricts name to max 63 characters - if (len >= 64) - return false; - for (int i = 0; i < len; i++) - { - // Restricts the characters in the name to a restricted range in ASCII - if (static_cast(name[i]) < 32 || static_cast(name[i]) > 126) - { - return false; - } - } - return true; -} - -bool ServerAuthenticationManager::VerifyPlayerName(const char* authToken, const char* name) -{ - std::lock_guard guard(m_AuthDataMutex); - - if (CVar_ns_auth_allow_insecure->GetInt()) - { - spdlog::info("Allowing player with name '{}' because ns_auth_allow_insecure is enabled", name); - return true; - } - - if (!checkIsPlayerNameValid(name)) - { - spdlog::info("Rejecting player with name '{}' because the name contains forbidden characters", name); - return false; - } - // TODO: We should really have a better way of doing this for singleplayer - // Best way of doing this would be to check if server is actually in singleplayer mode, or just running a SP map in multiplayer - // Currently there's not an easy way of checking this, so we just disable this check if mapname starts with `sp_` - // This means that player names are not checked on singleplayer - if ((m_RemoteAuthenticationData.empty() || m_RemoteAuthenticationData.count(std::string(authToken)) == 0) && - strncmp(R2::g_pHostState->m_levelName, "sp_", 3) != 0) - { - spdlog::info("Rejecting player with name '{}' because authToken '{}' was not found", name, authToken); - return false; - } - - const RemoteAuthData& authData = m_RemoteAuthenticationData[authToken]; - - if (*authData.username && strncmp(name, authData.username, 64) != 0) - { - spdlog::info("Rejecting player with name '{}' because name does not match expected name '{}'", name, authData.username); - return false; - } - - return true; -} - -bool ServerAuthenticationManager::CheckDuplicateAccounts(R2::CBaseClient* player) -{ - if (m_bAllowDuplicateAccounts) - return true; - - bool bHasUidPlayer = false; - for (int i = 0; i < R2::GetMaxPlayers(); i++) - if (&R2::g_pClientArray[i] != player && !strcmp(R2::g_pClientArray[i].m_UID, player->m_UID)) - return false; - - return true; -} - -bool ServerAuthenticationManager::AuthenticatePlayer(R2::CBaseClient* player, uint64_t uid, char* authToken) -{ - std::string strUid = std::to_string(uid); - std::lock_guard guard(m_AuthDataMutex); - - if (!strncmp(R2::g_pHostState->m_levelName, "sp_", 3)) - return true; - - // copy uuid - strcpy(player->m_UID, strUid.c_str()); - - bool authFail = true; - if (!m_RemoteAuthenticationData.empty() && m_RemoteAuthenticationData.count(std::string(authToken))) - { - if (!CheckDuplicateAccounts(player)) - return false; - - // use stored auth data - RemoteAuthData authData = m_RemoteAuthenticationData[authToken]; - - if (!strcmp(strUid.c_str(), authData.uid)) // connecting client's uid is the same as auth's uid - { - // if we're resetting let script handle the reset - if (!m_bForceResetLocalPlayerPersistence || strcmp(authData.uid, R2::g_pLocalPlayerUserID)) - { - // copy pdata into buffer - memcpy(player->m_PersistenceBuffer, authData.pdata, authData.pdataSize); - } - - // set persistent data as ready - player->m_iPersistenceReady = R2::ePersistenceReady::READY_REMOTE; - authFail = false; - } - } - - if (authFail) - { - if (CVar_ns_auth_allow_insecure->GetBool()) - { - // set persistent data as ready - // note: actual placeholder persistent data is populated in script with InitPersistentData() - player->m_iPersistenceReady = R2::ePersistenceReady::READY_INSECURE; - return true; - } - else - return false; - } - - return true; // auth successful, client stays on -} - -bool ServerAuthenticationManager::RemovePlayerAuthData(R2::CBaseClient* player) -{ - if (!Cvar_ns_erase_auth_info->GetBool()) // keep auth data forever - return false; - - // hack for special case where we're on a local server, so we erase our own newly created auth data on disconnect - if (m_bNeedLocalAuthForNewgame && !strcmp(player->m_UID, R2::g_pLocalPlayerUserID)) - return false; - - // we don't have our auth token at this point, so lookup authdata by uid - for (auto& auth : m_RemoteAuthenticationData) - { - if (!strcmp(player->m_UID, auth.second.uid)) - { - // pretty sure this is fine, since we don't iterate after the erase - // i think if we iterated after it'd be undefined behaviour tho - std::lock_guard guard(m_AuthDataMutex); - - delete[] auth.second.pdata; - m_RemoteAuthenticationData.erase(auth.first); - return true; - } - } - - return false; -} - -void ServerAuthenticationManager::WritePersistentData(R2::CBaseClient* player) -{ - if (player->m_iPersistenceReady == R2::ePersistenceReady::READY_REMOTE) - { - g_pMasterServerManager->WritePlayerPersistentData( - player->m_UID, (const char*)player->m_PersistenceBuffer, m_PlayerAuthenticationData[player].pdataSize); - } - else if (CVar_ns_auth_allow_insecure_write->GetBool()) - { - // todo: write pdata to disk here - } -} - -// auth hooks - -// store these in vars so we can use them in CBaseClient::Connect -// this is fine because ptrs won't decay by the time we use this, just don't use it outside of calls from cbaseclient::connectclient -char* pNextPlayerToken; -uint64_t iNextPlayerUid; - -// clang-format off -AUTOHOOK(CBaseServer__ConnectClient, engine.dll + 0x114430, -void*,, ( - void* self, - void* addr, - void* a3, - uint32_t a4, - uint32_t a5, - int32_t a6, - void* a7, - char* playerName, - char* serverFilter, - void* a10, - char a11, - void* a12, - char a13, - char a14, - int64_t uid, - uint32_t a16, - uint32_t a17)) -// clang-format on -{ - // auth tokens are sent with serverfilter, can't be accessed from player struct to my knowledge, so have to do this here - pNextPlayerToken = serverFilter; - iNextPlayerUid = uid; - - spdlog::info( - "CBaseServer__ClientConnect attempted connection with uid {}, playerName '{}', serverFilter '{}'", uid, playerName, serverFilter); - - if (!g_pServerAuthentication->VerifyPlayerName(pNextPlayerToken, playerName)) - { - CBaseServer__RejectConnection(self, *((int*)self + 3), addr, "Invalid Name.\n"); - return nullptr; - } - if (!g_pBanSystem->IsUIDAllowed(uid)) - { - CBaseServer__RejectConnection(self, *((int*)self + 3), addr, "Banned From server.\n"); - return nullptr; - } - - return CBaseServer__ConnectClient(self, addr, a3, a4, a5, a6, a7, playerName, serverFilter, a10, a11, a12, a13, a14, uid, a16, a17); -} - -// clang-format off -AUTOHOOK(CBaseClient__Connect, engine.dll + 0x101740, -bool,, (R2::CBaseClient* self, char* name, void* netchan_ptr_arg, char b_fake_player_arg, void* a5, char* Buffer, void* a7)) -// clang-format on -{ - // try to auth player, dc if it fails - // we connect regardless of auth, because returning bad from this function can fuck client state p bad - bool ret = CBaseClient__Connect(self, name, netchan_ptr_arg, b_fake_player_arg, a5, Buffer, a7); - if (!ret) - return ret; - - if (!g_pServerAuthentication->VerifyPlayerName(pNextPlayerToken, name)) - { - R2::CBaseClient__Disconnect(self, 1, "Invalid Name.\n"); - return false; - } - if (!g_pBanSystem->IsUIDAllowed(iNextPlayerUid)) - { - R2::CBaseClient__Disconnect(self, 1, "Banned From server.\n"); - return false; - } - if (!g_pServerAuthentication->AuthenticatePlayer(self, iNextPlayerUid, pNextPlayerToken)) - { - R2::CBaseClient__Disconnect(self, 1, "Authentication Failed.\n"); - return false; - } - - g_pServerAuthentication->AddPlayer(self, pNextPlayerToken); - g_pServerLimits->AddPlayer(self); - - return ret; -} - -// clang-format off -AUTOHOOK(CBaseClient__ActivatePlayer, engine.dll + 0x100F80, -void,, (R2::CBaseClient* self)) -// clang-format on -{ - // if we're authed, write our persistent data - // RemovePlayerAuthData returns true if it removed successfully, i.e. on first call only, and we only want to write on >= second call - // (since this func is called on map loads) - if (self->m_iPersistenceReady >= R2::ePersistenceReady::READY && !g_pServerAuthentication->RemovePlayerAuthData(self)) - { - g_pServerAuthentication->m_bForceResetLocalPlayerPersistence = false; - g_pServerAuthentication->WritePersistentData(self); - g_pServerPresence->SetPlayerCount(g_pServerAuthentication->m_PlayerAuthenticationData.size()); - } - - CBaseClient__ActivatePlayer(self); -} - -// clang-format off -AUTOHOOK(_CBaseClient__Disconnect, engine.dll + 0x1012C0, -void,, (R2::CBaseClient* self, uint32_t unknownButAlways1, const char* pReason, ...)) -// clang-format on -{ - // have to manually format message because can't pass varargs to original func - char buf[1024]; - - va_list va; - va_start(va, pReason); - vsprintf(buf, pReason, va); - va_end(va); - - // this reason is used while connecting to a local server, hacky, but just ignore it - if (strcmp(pReason, "Connection closing")) - { - spdlog::info("Player {} disconnected: \"{}\"", self->m_Name, buf); - - // dcing, write persistent data - if (g_pServerAuthentication->m_PlayerAuthenticationData[self].needPersistenceWriteOnLeave) - g_pServerAuthentication->WritePersistentData(self); - g_pServerAuthentication->RemovePlayerAuthData(self); // won't do anything 99% of the time, but just in case - - g_pServerAuthentication->RemovePlayer(self); - g_pServerLimits->RemovePlayer(self); - } - - else if (g_pServerAuthentication->m_RemoteAuthenticationData.count(self->m_Name)) - { - g_pServerAuthentication->RemovePlayer(self); - g_pServerLimits->RemovePlayer(self); - } - - g_pServerPresence->SetPlayerCount(g_pServerAuthentication->m_PlayerAuthenticationData.size()); - - _CBaseClient__Disconnect(self, unknownButAlways1, buf); -} - -void ConCommand_ns_resetpersistence(const CCommand& args) -{ - if (*R2::g_pServerState == R2::server_state_t::ss_active) - { - spdlog::error("ns_resetpersistence must be entered from the main menu"); - return; - } - - spdlog::info("resetting persistence on next lobby load..."); - g_pServerAuthentication->m_bForceResetLocalPlayerPersistence = true; -} - -ON_DLL_LOAD_RELIESON("engine.dll", ServerAuthentication, (ConCommand, ConVar), (CModule module)) -{ - AUTOHOOK_DISPATCH() - - g_pServerAuthentication = new ServerAuthenticationManager; - - g_pServerAuthentication->Cvar_ns_player_auth_port = new ConVar("ns_player_auth_port", "8081", FCVAR_GAMEDLL, ""); - g_pServerAuthentication->Cvar_ns_erase_auth_info = - new ConVar("ns_erase_auth_info", "1", FCVAR_GAMEDLL, "Whether auth info should be erased from this server on disconnect or crash"); - g_pServerAuthentication->CVar_ns_auth_allow_insecure = - new ConVar("ns_auth_allow_insecure", "0", FCVAR_GAMEDLL, "Whether this server will allow unauthenicated players to connect"); - g_pServerAuthentication->CVar_ns_auth_allow_insecure_write = new ConVar( - "ns_auth_allow_insecure_write", - "0", - FCVAR_GAMEDLL, - "Whether the pdata of unauthenticated clients will be written to disk when changed"); - - RegisterConCommand( - "ns_resetpersistence", ConCommand_ns_resetpersistence, "resets your pdata when you next enter the lobby", FCVAR_NONE); - - // patch to disable kicking based on incorrect serverfilter in connectclient, since we repurpose it for use as an auth token - module.Offset(0x114655).Patch("EB"); - - // patch to disable fairfight marking players as cheaters and kicking them - module.Offset(0x101012).Patch("E9 90 00"); - - CBaseServer__RejectConnection = module.Offset(0x1182E0).As(); - - if (Tier0::CommandLine()->CheckParm("-allowdupeaccounts")) - { - // patch to allow same of multiple account - module.Offset(0x114510).Patch("EB"); - - g_pServerAuthentication->m_bAllowDuplicateAccounts = true; - } -} diff --git a/NorthstarDLL/serverauthentication.h b/NorthstarDLL/serverauthentication.h deleted file mode 100644 index 65050d56..00000000 --- a/NorthstarDLL/serverauthentication.h +++ /dev/null @@ -1,59 +0,0 @@ -#pragma once -#include "convar.h" -#include "httplib.h" -#include "r2engine.h" -#include -#include - -struct RemoteAuthData -{ - char uid[33]; - char username[64]; - - // pdata - char* pdata; - size_t pdataSize; -}; - -struct PlayerAuthenticationData -{ - bool usingLocalPdata; - size_t pdataSize; - bool needPersistenceWriteOnLeave = true; -}; - -typedef int64_t (*CBaseServer__RejectConnectionType)(void* a1, unsigned int a2, void* a3, const char* a4, ...); -extern CBaseServer__RejectConnectionType CBaseServer__RejectConnection; - -class ServerAuthenticationManager -{ - private: - httplib::Server m_PlayerAuthServer; - - public: - ConVar* Cvar_ns_player_auth_port; - ConVar* Cvar_ns_erase_auth_info; - ConVar* CVar_ns_auth_allow_insecure; - ConVar* CVar_ns_auth_allow_insecure_write; - - std::mutex m_AuthDataMutex; - std::unordered_map m_RemoteAuthenticationData; - std::unordered_map m_PlayerAuthenticationData; - bool m_bAllowDuplicateAccounts = false; - bool m_bRunningPlayerAuthThread = false; - bool m_bNeedLocalAuthForNewgame = false; - bool m_bForceResetLocalPlayerPersistence = false; - - public: - void StartPlayerAuthServer(); - void StopPlayerAuthServer(); - void AddPlayer(R2::CBaseClient* player, const char* pToken); - void RemovePlayer(R2::CBaseClient* player); - bool CheckDuplicateAccounts(R2::CBaseClient* player); - bool AuthenticatePlayer(R2::CBaseClient* player, uint64_t uid, char* authToken); - bool VerifyPlayerName(const char* authToken, const char* name); - bool RemovePlayerAuthData(R2::CBaseClient* player); - void WritePersistentData(R2::CBaseClient* player); -}; - -extern ServerAuthenticationManager* g_pServerAuthentication; diff --git a/NorthstarDLL/serverchathooks.cpp b/NorthstarDLL/serverchathooks.cpp deleted file mode 100644 index e4e67e05..00000000 --- a/NorthstarDLL/serverchathooks.cpp +++ /dev/null @@ -1,172 +0,0 @@ -#include "pch.h" -#include "serverchathooks.h" -#include "limits.h" -#include "squirrel.h" -#include "r2server.h" - -#include -#include -#include - -AUTOHOOK_INIT() - -class CServerGameDLL; - -class CRecipientFilter -{ - char unknown[58]; -}; - -CServerGameDLL* g_pServerGameDLL; - -void(__fastcall* CServerGameDLL__OnReceivedSayTextMessage)( - CServerGameDLL* self, unsigned int senderPlayerId, const char* text, int channelId); - -void(__fastcall* CRecipientFilter__Construct)(CRecipientFilter* self); -void(__fastcall* CRecipientFilter__Destruct)(CRecipientFilter* self); -void(__fastcall* CRecipientFilter__AddAllPlayers)(CRecipientFilter* self); -void(__fastcall* CRecipientFilter__AddRecipient)(CRecipientFilter* self, const R2::CBasePlayer* player); -void(__fastcall* CRecipientFilter__MakeReliable)(CRecipientFilter* self); - -void(__fastcall* UserMessageBegin)(CRecipientFilter* filter, const char* messagename); -void(__fastcall* MessageEnd)(); -void(__fastcall* MessageWriteByte)(int iValue); -void(__fastcall* MessageWriteString)(const char* sz); -void(__fastcall* MessageWriteBool)(bool bValue); - -bool bShouldCallSayTextHook = false; -// clang-format off -AUTOHOOK(_CServerGameDLL__OnReceivedSayTextMessage, server.dll + 0x1595C0, -void, __fastcall, (CServerGameDLL* self, unsigned int senderPlayerId, const char* text, bool isTeam)) -// clang-format on -{ - // MiniHook doesn't allow calling the base function outside of anywhere but the hook function. - // To allow bypassing the hook, isSkippingHook can be set. - if (bShouldCallSayTextHook) - { - bShouldCallSayTextHook = false; - _CServerGameDLL__OnReceivedSayTextMessage(self, senderPlayerId, text, isTeam); - return; - } - - // check chat ratelimits - if (!g_pServerLimits->CheckChatLimits(&R2::g_pClientArray[senderPlayerId - 1])) - return; - - SQRESULT result = g_pSquirrel->Call( - "CServerGameDLL_ProcessMessageStartThread", static_cast(senderPlayerId) - 1, text, isTeam); - - if (result == SQRESULT_ERROR) - _CServerGameDLL__OnReceivedSayTextMessage(self, senderPlayerId, text, isTeam); -} - -void ChatSendMessage(unsigned int playerIndex, const char* text, bool isTeam) -{ - bShouldCallSayTextHook = true; - CServerGameDLL__OnReceivedSayTextMessage( - g_pServerGameDLL, - // Ensure the first bit isn't set, since this indicates a custom message - (playerIndex + 1) & CUSTOM_MESSAGE_INDEX_MASK, - text, - isTeam); -} - -void ChatBroadcastMessage(int fromPlayerIndex, int toPlayerIndex, const char* text, bool isTeam, bool isDead, CustomMessageType messageType) -{ - R2::CBasePlayer* toPlayer = NULL; - if (toPlayerIndex >= 0) - { - toPlayer = R2::UTIL_PlayerByIndex(toPlayerIndex + 1); - if (toPlayer == NULL) - return; - } - - // Build a new string where the first byte is the message type - char sendText[256]; - sendText[0] = (char)messageType; - strncpy_s(sendText + 1, 255, text, 254); - - // Anonymous custom messages use playerId=0, non-anonymous ones use a player ID with the first bit set - unsigned int fromPlayerId = fromPlayerIndex < 0 ? 0 : ((fromPlayerIndex + 1) | CUSTOM_MESSAGE_INDEX_BIT); - - CRecipientFilter filter; - CRecipientFilter__Construct(&filter); - if (toPlayer == NULL) - { - CRecipientFilter__AddAllPlayers(&filter); - } - else - { - CRecipientFilter__AddRecipient(&filter, toPlayer); - } - CRecipientFilter__MakeReliable(&filter); - - UserMessageBegin(&filter, "SayText"); - MessageWriteByte(fromPlayerId); - MessageWriteString(sendText); - MessageWriteBool(isTeam); - MessageWriteBool(isDead); - MessageEnd(); - - CRecipientFilter__Destruct(&filter); -} - -ADD_SQFUNC("void", NSSendMessage, "int playerIndex, string text, bool isTeam", "", ScriptContext::SERVER) -{ - int playerIndex = g_pSquirrel->getinteger(sqvm, 1); - const char* text = g_pSquirrel->getstring(sqvm, 2); - bool isTeam = g_pSquirrel->getbool(sqvm, 3); - - ChatSendMessage(playerIndex, text, isTeam); - - return SQRESULT_NULL; -} - -ADD_SQFUNC( - "void", - NSBroadcastMessage, - "int fromPlayerIndex, int toPlayerIndex, string text, bool isTeam, bool isDead, int messageType", - "", - ScriptContext::SERVER) -{ - int fromPlayerIndex = g_pSquirrel->getinteger(sqvm, 1); - int toPlayerIndex = g_pSquirrel->getinteger(sqvm, 2); - const char* text = g_pSquirrel->getstring(sqvm, 3); - bool isTeam = g_pSquirrel->getbool(sqvm, 4); - bool isDead = g_pSquirrel->getbool(sqvm, 5); - int messageType = g_pSquirrel->getinteger(sqvm, 6); - - if (messageType < 1) - { - g_pSquirrel->raiseerror(sqvm, fmt::format("Invalid message type {}", messageType).c_str()); - return SQRESULT_ERROR; - } - - ChatBroadcastMessage(fromPlayerIndex, toPlayerIndex, text, isTeam, isDead, (CustomMessageType)messageType); - - return SQRESULT_NULL; -} - -ON_DLL_LOAD("engine.dll", EngineServerChatHooks, (CModule module)) -{ - g_pServerGameDLL = module.Offset(0x13F0AA98).As(); -} - -ON_DLL_LOAD_RELIESON("server.dll", ServerChatHooks, ServerSquirrel, (CModule module)) -{ - AUTOHOOK_DISPATCH_MODULE(server.dll) - - CServerGameDLL__OnReceivedSayTextMessage = - module.Offset(0x1595C0).As(); - CRecipientFilter__Construct = module.Offset(0x1E9440).As(); - CRecipientFilter__Destruct = module.Offset(0x1E9700).As(); - CRecipientFilter__AddAllPlayers = module.Offset(0x1E9940).As(); - CRecipientFilter__AddRecipient = module.Offset(0x1E9B30).As(); - CRecipientFilter__MakeReliable = module.Offset(0x1EA4E0).As(); - - UserMessageBegin = module.Offset(0x15C520).As(); - MessageEnd = module.Offset(0x158880).As(); - MessageWriteByte = module.Offset(0x158A90).As(); - MessageWriteString = module.Offset(0x158D00).As(); - MessageWriteBool = module.Offset(0x158A00).As(); -} diff --git a/NorthstarDLL/serverchathooks.h b/NorthstarDLL/serverchathooks.h deleted file mode 100644 index 1d8a806a..00000000 --- a/NorthstarDLL/serverchathooks.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once -#include "pch.h" -#include -#include - -enum class CustomMessageType : char -{ - Chat = 1, - Whisper = 2 -}; - -constexpr unsigned char CUSTOM_MESSAGE_INDEX_BIT = 0b10000000; -constexpr unsigned char CUSTOM_MESSAGE_INDEX_MASK = ~CUSTOM_MESSAGE_INDEX_BIT; - -// Send a vanilla chat message as if it was from the player. -void ChatSendMessage(unsigned int playerIndex, const char* text, bool isteam); - -// Send a custom message. -// fromPlayerIndex: set to -1 for a [SERVER] message, or another value to send from a specific player -// toPlayerIndex: set to -1 to send to all players, or another value to send to a single player -// isTeam: display a [TEAM] badge -// isDead: display a [DEAD] badge -// messageType: send a specific message type -void ChatBroadcastMessage( - int fromPlayerIndex, int toPlayerIndex, const char* text, bool isTeam, bool isDead, CustomMessageType messageType); diff --git a/NorthstarDLL/serverpresence.cpp b/NorthstarDLL/serverpresence.cpp deleted file mode 100644 index fb8cf624..00000000 --- a/NorthstarDLL/serverpresence.cpp +++ /dev/null @@ -1,237 +0,0 @@ -#include "pch.h" -#include "serverpresence.h" -#include "playlist.h" -#include "tier0.h" -#include "convar.h" - -#include - -ServerPresenceManager* g_pServerPresence; - -ConVar* Cvar_hostname; - -// Convert a hex digit char to integer. -inline int hctod(char c) -{ - if (c >= 'A' && c <= 'F') - { - return c - 'A' + 10; - } - else if (c >= 'a' && c <= 'f') - { - return c - 'a' + 10; - } - else - { - return c - '0'; - } -} - -// This function interprets all 4-hexadecimal-digit unicode codepoint characters like \u4E2D to UTF-8 encoding. -std::string UnescapeUnicode(const std::string& str) -{ - std::string result; - - std::regex r("\\\\u([a-f\\d]{4})", std::regex::icase); - auto matches_begin = std::sregex_iterator(str.begin(), str.end(), r); - auto matches_end = std::sregex_iterator(); - std::smatch last_match; - - for (std::sregex_iterator i = matches_begin; i != matches_end; ++i) - { - last_match = *i; - result.append(last_match.prefix()); - unsigned int cp = 0; - for (int i = 2; i <= 5; ++i) - { - cp *= 16; - cp += hctod(last_match.str()[i]); - } - if (cp <= 0x7F) - { - result.push_back(cp); - } - else if (cp <= 0x7FF) - { - result.push_back((cp >> 6) | 0b11000000 & (~(1 << 5))); - result.push_back(cp & ((1 << 6) - 1) | 0b10000000 & (~(1 << 6))); - } - else if (cp <= 0xFFFF) - { - result.push_back((cp >> 12) | 0b11100000 & (~(1 << 4))); - result.push_back((cp >> 6) & ((1 << 6) - 1) | 0b10000000 & (~(1 << 6))); - result.push_back(cp & ((1 << 6) - 1) | 0b10000000 & (~(1 << 6))); - } - } - - if (!last_match.ready()) - return str; - else - result.append(last_match.suffix()); - - return result; -} - -ServerPresenceManager::ServerPresenceManager() -{ - // clang-format off - // register convars - Cvar_ns_server_presence_update_rate = new ConVar( - "ns_server_presence_update_rate", "5000", FCVAR_GAMEDLL, "How often we update our server's presence on server lists in ms"); - - Cvar_ns_server_name = new ConVar("ns_server_name", "Unnamed Northstar Server", FCVAR_GAMEDLL, "This server's description", false, 0, false, 0, [](ConVar* cvar, const char* pOldValue, float flOldValue) { - g_pServerPresence->SetName(UnescapeUnicode(g_pServerPresence->Cvar_ns_server_name->GetString())); - - // update engine hostname cvar - Cvar_hostname->SetValue(g_pServerPresence->Cvar_ns_server_name->GetString()); - }); - - Cvar_ns_server_desc = new ConVar("ns_server_desc", "Default server description", FCVAR_GAMEDLL, "This server's name", false, 0, false, 0, [](ConVar* cvar, const char* pOldValue, float flOldValue) { - g_pServerPresence->SetDescription(UnescapeUnicode(g_pServerPresence->Cvar_ns_server_desc->GetString())); - }); - - Cvar_ns_server_password = new ConVar("ns_server_password", "", FCVAR_GAMEDLL, "This server's password", false, 0, false, 0, [](ConVar* cvar, const char* pOldValue, float flOldValue) { - g_pServerPresence->SetPassword(g_pServerPresence->Cvar_ns_server_password->GetString()); - }); - - Cvar_ns_report_server_to_masterserver = new ConVar("ns_report_server_to_masterserver", "1", FCVAR_GAMEDLL, "Whether we should report this server to the masterserver"); - Cvar_ns_report_sp_server_to_masterserver = new ConVar("ns_report_sp_server_to_masterserver", "0", FCVAR_GAMEDLL, "Whether we should report this server to the masterserver, when started in singleplayer"); - // clang-format on -} - -void ServerPresenceManager::AddPresenceReporter(ServerPresenceReporter* reporter) -{ - m_vPresenceReporters.push_back(reporter); -} - -void ServerPresenceManager::CreatePresence() -{ - // reset presence fields that rely on runtime server state - // these being: port/auth port, map/playlist name, and playercount/maxplayers - m_ServerPresence.m_iPort = 0; - m_ServerPresence.m_iAuthPort = 0; - - m_ServerPresence.m_iPlayerCount = 0; // this should actually be 0 at this point, so shouldn't need updating later - m_ServerPresence.m_iMaxPlayers = 0; - - memset(m_ServerPresence.m_MapName, 0, sizeof(m_ServerPresence.m_MapName)); - memset(m_ServerPresence.m_PlaylistName, 0, sizeof(m_ServerPresence.m_PlaylistName)); - m_ServerPresence.m_bIsSingleplayerServer = false; - - m_bHasPresence = true; - m_bFirstPresenceUpdate = true; - - // code that's calling this should set up the reset fields at this point -} - -void ServerPresenceManager::DestroyPresence() -{ - m_bHasPresence = false; - - for (ServerPresenceReporter* reporter : m_vPresenceReporters) - reporter->DestroyPresence(&m_ServerPresence); -} - -void ServerPresenceManager::RunFrame(double flCurrentTime) -{ - if (!m_bHasPresence || !Cvar_ns_report_server_to_masterserver->GetBool()) // don't run until we actually have server presence - return; - - // don't run if we're sp and don't want to report sp - if (m_ServerPresence.m_bIsSingleplayerServer && !Cvar_ns_report_sp_server_to_masterserver->GetBool()) - return; - - // Call RunFrame() so that reporters can, for example, handle std::future results as soon as they arrive. - for (ServerPresenceReporter* reporter : m_vPresenceReporters) - reporter->RunFrame(flCurrentTime, &m_ServerPresence); - - // run on a specified delay - if ((flCurrentTime - m_flLastPresenceUpdate) * 1000 < Cvar_ns_server_presence_update_rate->GetFloat()) - return; - - // is this the first frame we're updating this presence? - if (m_bFirstPresenceUpdate) - { - // let reporters setup/clear any state - for (ServerPresenceReporter* reporter : m_vPresenceReporters) - reporter->CreatePresence(&m_ServerPresence); - - m_bFirstPresenceUpdate = false; - } - - m_flLastPresenceUpdate = flCurrentTime; - - for (ServerPresenceReporter* reporter : m_vPresenceReporters) - reporter->ReportPresence(&m_ServerPresence); -} - -void ServerPresenceManager::SetPort(const int iPort) -{ - // update port - m_ServerPresence.m_iPort = iPort; -} - -void ServerPresenceManager::SetAuthPort(const int iAuthPort) -{ - // update authport - m_ServerPresence.m_iAuthPort = iAuthPort; -} - -void ServerPresenceManager::SetName(const std::string sServerNameUnicode) -{ - // update name - m_ServerPresence.m_sServerName = sServerNameUnicode; -} - -void ServerPresenceManager::SetDescription(const std::string sServerDescUnicode) -{ - // update desc - m_ServerPresence.m_sServerDesc = sServerDescUnicode; -} - -void ServerPresenceManager::SetPassword(const char* pPassword) -{ - // update password - strncpy_s(m_ServerPresence.m_Password, sizeof(m_ServerPresence.m_Password), pPassword, sizeof(m_ServerPresence.m_Password) - 1); -} - -void ServerPresenceManager::SetMap(const char* pMapName, bool isInitialising) -{ - // if the server is initialising (i.e. this is first map) on sp, set the server to sp - if (isInitialising) - m_ServerPresence.m_bIsSingleplayerServer = !strncmp(pMapName, "sp_", 3); - - // update map - strncpy_s(m_ServerPresence.m_MapName, sizeof(m_ServerPresence.m_MapName), pMapName, sizeof(m_ServerPresence.m_MapName) - 1); -} - -void ServerPresenceManager::SetPlaylist(const char* pPlaylistName) -{ - // update playlist - strncpy_s( - m_ServerPresence.m_PlaylistName, - sizeof(m_ServerPresence.m_PlaylistName), - pPlaylistName, - sizeof(m_ServerPresence.m_PlaylistName) - 1); - - // update maxplayers - const char* pMaxPlayers = R2::GetCurrentPlaylistVar("max_players", true); - - // can be null in some situations, so default 6 - if (pMaxPlayers) - m_ServerPresence.m_iMaxPlayers = std::stoi(pMaxPlayers); - else - m_ServerPresence.m_iMaxPlayers = 6; -} - -void ServerPresenceManager::SetPlayerCount(const int iPlayerCount) -{ - m_ServerPresence.m_iPlayerCount = iPlayerCount; -} - -ON_DLL_LOAD_RELIESON("engine.dll", ServerPresence, ConVar, (CModule module)) -{ - g_pServerPresence = new ServerPresenceManager; - - Cvar_hostname = module.Offset(0x1315BAE8).Deref().As(); -} diff --git a/NorthstarDLL/serverpresence.h b/NorthstarDLL/serverpresence.h deleted file mode 100644 index 97b4654c..00000000 --- a/NorthstarDLL/serverpresence.h +++ /dev/null @@ -1,92 +0,0 @@ -#pragma once -#include "convar.h" - -struct ServerPresence -{ - int m_iPort; - int m_iAuthPort; - - std::string m_sServerName; - std::string m_sServerDesc; - char m_Password[256]; // probably bigger than will ever be used in practice, lol - - char m_MapName[32]; - char m_PlaylistName[64]; - bool m_bIsSingleplayerServer; // whether the server started in sp - - int m_iPlayerCount; - int m_iMaxPlayers; - - ServerPresence() - { - memset(this, 0, sizeof(this)); - } - - ServerPresence(const ServerPresence* obj) - { - m_iPort = obj->m_iPort; - m_iAuthPort = obj->m_iAuthPort; - - m_sServerName = obj->m_sServerName; - m_sServerDesc = obj->m_sServerDesc; - memcpy(m_Password, obj->m_Password, sizeof(m_Password)); - - memcpy(m_MapName, obj->m_MapName, sizeof(m_MapName)); - memcpy(m_PlaylistName, obj->m_PlaylistName, sizeof(m_PlaylistName)); - - m_iPlayerCount = obj->m_iPlayerCount; - m_iMaxPlayers = obj->m_iMaxPlayers; - } -}; - -class ServerPresenceReporter -{ - public: - virtual void CreatePresence(const ServerPresence* pServerPresence) {} - virtual void ReportPresence(const ServerPresence* pServerPresence) {} - virtual void DestroyPresence(const ServerPresence* pServerPresence) {} - virtual void RunFrame(double flCurrentTime, const ServerPresence* pServerPresence) {} -}; - -class ServerPresenceManager -{ - private: - ServerPresence m_ServerPresence; - - bool m_bHasPresence = false; - bool m_bFirstPresenceUpdate = false; - - std::vector m_vPresenceReporters; - - double m_flLastPresenceUpdate = 0; - ConVar* Cvar_ns_server_presence_update_rate; - - ConVar* Cvar_ns_server_name; - ConVar* Cvar_ns_server_desc; - ConVar* Cvar_ns_server_password; - - ConVar* Cvar_ns_report_server_to_masterserver; - ConVar* Cvar_ns_report_sp_server_to_masterserver; - - public: - ServerPresenceManager(); - - void AddPresenceReporter(ServerPresenceReporter* reporter); - - void CreatePresence(); - void DestroyPresence(); - void RunFrame(double flCurrentTime); - - void SetPort(const int iPort); - void SetAuthPort(const int iPort); - - void SetName(const std::string sServerNameUnicode); - void SetDescription(const std::string sServerDescUnicode); - void SetPassword(const char* pPassword); - - void SetMap(const char* pMapName, bool isInitialising = false); - void SetPlaylist(const char* pPlaylistName); - void SetPlayerCount(const int iPlayerCount); -}; - -extern ServerPresenceManager* g_pServerPresence; diff --git a/NorthstarDLL/shared/exploit_fixes/exploitfixes.cpp b/NorthstarDLL/shared/exploit_fixes/exploitfixes.cpp new file mode 100644 index 00000000..e4430fd4 --- /dev/null +++ b/NorthstarDLL/shared/exploit_fixes/exploitfixes.cpp @@ -0,0 +1,458 @@ +#include "pch.h" +#include "core/convar/cvar.h" +#include "ns_limits.h" +#include "dedicated/dedicated.h" +#include "core/tier0.h" +#include "engine/r2engine.h" +#include "client/r2client.h" +#include "core/math/vector.h" + +AUTOHOOK_INIT() + +ConVar* Cvar_ns_exploitfixes_log; +ConVar* Cvar_ns_should_log_all_clientcommands; + +ConVar* Cvar_sv_cheats; + +#define BLOCKED_INFO(s) \ + ( \ + [=]() -> bool \ + { \ + if (Cvar_ns_exploitfixes_log->GetBool()) \ + { \ + std::stringstream stream; \ + stream << "ExploitFixes.cpp: " << BLOCK_PREFIX << s; \ + spdlog::error(stream.str()); \ + } \ + return false; \ + }()) + +// block bad netmessages +// Servers can literally request a screenshot from any client, yeah no +// 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; +} + +// 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; +} + +// 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; + struct SetConVarEntry + { + char name[ENTRY_STR_LEN]; + char val[ENTRY_STR_LEN]; + }; + + struct NET_SetConVar + { + void* vtable; + void* unk1; + void* unk2; + void* m_pMessageHandler; + SetConVarEntry* m_ConVars; // convar entry array + void* unk5; // these 2 unks are just vector capacity or whatever + void* unk6; + int m_ConVars_count; // amount of cvar entries in array (this will not be out of bounds) + }; + + auto msg = (NET_SetConVar*)pMsg; + bool bIsServerFrame = Tier0::ThreadInServerFrameThread(); + + std::string BLOCK_PREFIX = + std::string {"NET_SetConVar ("} + (bIsServerFrame ? "server" : "client") + "): Blocked dangerous/invalid msg: "; + + if (bIsServerFrame) + { + constexpr int SETCONVAR_SANITY_AMOUNT_LIMIT = 69; + if (msg->m_ConVars_count < 1 || msg->m_ConVars_count > SETCONVAR_SANITY_AMOUNT_LIMIT) + { + return BLOCKED_INFO("Invalid m_ConVars_count (" << msg->m_ConVars_count << ")"); + } + } + + for (int i = 0; i < msg->m_ConVars_count; i++) + { + auto entry = msg->m_ConVars + i; + + // Safety check for memory access + if (MemoryAddress(entry).IsMemoryReadable(sizeof(*entry))) + { + // Find null terminators + bool nameValid = false, valValid = false; + for (int i = 0; i < ENTRY_STR_LEN; i++) + { + if (!entry->name[i]) + nameValid = true; + if (!entry->val[i]) + valValid = true; + } + + if (!nameValid || !valValid) + return BLOCKED_INFO("Missing null terminators"); + + ConVar* pVar = R2::g_pCVar->FindVar(entry->name); + + if (pVar) + { + memcpy( + entry->name, + pVar->m_ConCommandBase.m_pszName, + strlen(pVar->m_ConCommandBase.m_pszName) + 1); // Force name to match case + + int iFlags = bIsServerFrame ? FCVAR_USERINFO : FCVAR_REPLICATED; + if (!pVar->IsFlagSet(iFlags)) + return BLOCKED_INFO( + "Invalid flags (" << std::hex << "0x" << pVar->m_ConCommandBase.m_nFlags << "), var is " << entry->name); + } + } + else + { + return BLOCKED_INFO("Unreadable memory at " << (void*)entry); // Not risking that one, they all gotta be readable + } + } + + return CClient_ProcessSetConVar(msg); +} + +// 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 + { + BYTE gap0[24]; + void* m_pMessageHandler; + int m_nBackupCommands; + int m_nNewCommands; + int m_nLength; + // bf_read m_DataIn; + // bf_write m_DataOut; + }; + + auto msg = (CLC_Move*)pMsg; + + const char* BLOCK_PREFIX = "ProcessUserCmds: "; + + if (msg->m_nBackupCommands < 0) + { + return BLOCKED_INFO("Invalid m_nBackupCommands (" << msg->m_nBackupCommands << ")"); + } + + if (msg->m_nNewCommands < 0) + { + return BLOCKED_INFO("Invalid m_nNewCommands (" << msg->m_nNewCommands << ")"); + } + + if (msg->m_nLength <= 0) + return BLOCKED_INFO("Invalid message length (" << msg->m_nLength << ")"); + + return CClient_ProcessUsercmds(thisptr, pMsg); +} + +// 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 + 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 + { + 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; + }; + + auto cmd = (SV_CUserCmd*)pCmd_move; + auto fromCmd = (SV_CUserCmd*)pCmd_from; + + std::string BLOCK_PREFIX = + "ReadUsercmd (command_number delta: " + std::to_string(cmd->command_number - fromCmd->command_number) + "): "; + + // fix invalid player angles + cmd->worldViewAngles.MakeValid(); + cmd->attackangles.MakeValid(); + cmd->localViewAngles.MakeValid(); + + // Fix invalid camera angles + cmd->cameraPos.MakeValid(); + cmd->cameraAngles.MakeValid(); + + // Fix invaid movement vector + cmd->move.MakeValid(); + + 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 + << ", commandTime : " << cmd->command_time << ")"); + goto INVALID_CMD; // No simulation of bogus-timed cmds + } + + 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}; + cmd->tick_count = cmd->frameTime = 0; + cmd->move = cmd->cameraPos = {0, 0, 0}; + cmd->buttons = 0; + cmd->meleetarget = 0; +} + +// 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 +{ + // 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"); +} + +// 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); + + if (!g_pServerLimits->CheckStringCommandLimits(self)) + { + 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; + } + + // 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()) + { + 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() + }; + + int iCmdLength = strlen(tempCommand.Arg(0)); + + bool bIsBadCommand = false; + for (auto& blockedCommand : blockedCommands) + { + if (iCmdLength != strlen(blockedCommand)) + continue; + + 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 + + // this is a command we need to block + return false; + NEXT_COMMAND:; + } + } + + return CGameClient__ExecuteStringCommand(self, unknown, pCommandString); +} + +// 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"); +} + +// 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 (i >= MAX_ENT_IDX) + { + spdlog::warn("GetEntByIndex {} is out of bounds (max {})", i, MAX_ENT_IDX); + return nullptr; + } + + 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; + }; + + CEntityReadInfo* pReadInfo = (CEntityReadInfo*)a1; + if (pReadInfo->nNewEntity >= 0x1000 || pReadInfo->nNewEntity < 0) + { + // 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; + } + + return CL_CopyExistingEntity(a1); +} + +ON_DLL_LOAD("engine.dll", EngineExploitFixes, (CModule module)) +{ + AUTOHOOK_DISPATCH_MODULE(engine.dll) + + CCommand__Tokenize = module.Offset(0x418380).As(); + + // 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) + { + // Just return, none of them have any args or are userpurge + exportAddr.Patch("C3"); + spdlog::info("Patched AntiTamper function export \"{}\"", exportName); + } + } + + 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"); +} diff --git a/NorthstarDLL/shared/exploit_fixes/exploitfixes_lzss.cpp b/NorthstarDLL/shared/exploit_fixes/exploitfixes_lzss.cpp new file mode 100644 index 00000000..4205133a --- /dev/null +++ b/NorthstarDLL/shared/exploit_fixes/exploitfixes_lzss.cpp @@ -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() +} diff --git a/NorthstarDLL/shared/exploit_fixes/exploitfixes_utf8parser.cpp b/NorthstarDLL/shared/exploit_fixes/exploitfixes_utf8parser.cpp new file mode 100644 index 00000000..e2510765 --- /dev/null +++ b/NorthstarDLL/shared/exploit_fixes/exploitfixes_utf8parser.cpp @@ -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(); +} diff --git a/NorthstarDLL/shared/exploit_fixes/ns_limits.cpp b/NorthstarDLL/shared/exploit_fixes/ns_limits.cpp new file mode 100644 index 00000000..49f80bab --- /dev/null +++ b/NorthstarDLL/shared/exploit_fixes/ns_limits.cpp @@ -0,0 +1,298 @@ +#include "pch.h" +#include "ns_limits.h" +#include "engine/hoststate.h" +#include "client/r2client.h" +#include "engine/r2engine.h" +#include "server/r2server.h" +#include "shared/maxplayers.h" +#include "core/tier0.h" +#include "core/math/vector.h" +#include "server/auth/serverauthentication.h" + +AUTOHOOK_INIT() + +ServerLimitsManager* g_pServerLimits; + +// 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 +{ + static const ConVar* Cvar_net_data_block_enabled = R2::g_pCVar->FindVar("net_data_block_enabled"); + + // don't ratelimit datablock packets as long as datablock is enabled + if (packet->adr.type == R2::NA_IP && + (!(packet->data[4] == 'N' && Cvar_net_data_block_enabled->GetBool()) || !Cvar_net_data_block_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"); +} + +ON_DLL_LOAD("server.dll", ServerLimitsServer, (CModule module)) +{ + AUTOHOOK_DISPATCH_MODULE(server.dll) +} diff --git a/NorthstarDLL/shared/exploit_fixes/ns_limits.h b/NorthstarDLL/shared/exploit_fixes/ns_limits.h new file mode 100644 index 00000000..bbc0a85f --- /dev/null +++ b/NorthstarDLL/shared/exploit_fixes/ns_limits.h @@ -0,0 +1,51 @@ +#pragma once +#include "engine/r2engine.h" +#include "core/convar/convar.h" +#include + +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 m_PlayerLimitData; + std::vector 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; diff --git a/NorthstarDLL/shared/keyvalues.cpp b/NorthstarDLL/shared/keyvalues.cpp new file mode 100644 index 00000000..fe7d6299 --- /dev/null +++ b/NorthstarDLL/shared/keyvalues.cpp @@ -0,0 +1,1316 @@ +#include "pch.h" +#include "keyvalues.h" +#include + +// implementation of the ConVar class +// heavily based on https://github.com/Mauler125/r5sdk/blob/master/r5dev/vpc/keyvalues.cpp + +typedef int HKeySymbol; +#define INVALID_KEY_SYMBOL (-1) + +#define MAKE_3_BYTES_FROM_1_AND_2(x1, x2) ((((uint16_t)x2) << 8) | (uint8_t)(x1)) +#define SPLIT_3_BYTES_INTO_1_AND_2(x1, x2, x3) \ + do \ + { \ + x1 = (uint8_t)(x3); \ + x2 = (uint16_t)((x3) >> 8); \ + } while (0) + +struct CKeyValuesSystem +{ + public: + struct __VTable + { + char pad0[8 * 3]; // 2 methods + HKeySymbol (*GetSymbolForString)(CKeyValuesSystem* self, const char* name, bool bCreate); + const char* (*GetStringForSymbol)(CKeyValuesSystem* self, HKeySymbol symbol); + char pad1[8 * 5]; + HKeySymbol (*GetSymbolForStringCaseSensitive)( + CKeyValuesSystem* self, HKeySymbol& hCaseInsensitiveSymbol, const char* name, bool bCreate); + }; + + const __VTable* m_pVtable; +}; + +int (*V_UTF8ToUnicode)(const char* pUTF8, wchar_t* pwchDest, int cubDestSizeInBytes); +int (*V_UnicodeToUTF8)(const wchar_t* pUnicode, char* pUTF8, int cubDestSizeInBytes); +CKeyValuesSystem* (*KeyValuesSystem)(); + +KeyValues::KeyValues() {} // default constructor for copying and such + +//----------------------------------------------------------------------------- +// Purpose: Constructor +// Input : *pszSetName - +//----------------------------------------------------------------------------- +KeyValues::KeyValues(const char* pszSetName) +{ + Init(); + SetName(pszSetName); +} + +//----------------------------------------------------------------------------- +// Purpose: Constructor +// Input : *pszSetName - +// *pszFirstKey - +// *pszFirstValue - +//----------------------------------------------------------------------------- +KeyValues::KeyValues(const char* pszSsetName, const char* pszFirstKey, const char* pszFirstValue) +{ + Init(); + SetName(pszSsetName); + SetString(pszFirstKey, pszFirstValue); +} + +//----------------------------------------------------------------------------- +// Purpose: Constructor +// Input : *pszSetName - +// *pszFirstKey - +// *pwszFirstValue - +//----------------------------------------------------------------------------- +KeyValues::KeyValues(const char* pszSetName, const char* pszFirstKey, const wchar_t* pwszFirstValue) +{ + Init(); + SetName(pszSetName); + SetWString(pszFirstKey, pwszFirstValue); +} + +//----------------------------------------------------------------------------- +// Purpose: Constructor +// Input : *pszSetName - +// *pszFirstKey - +// iFirstValue - +//----------------------------------------------------------------------------- +KeyValues::KeyValues(const char* pszSetName, const char* pszFirstKey, int iFirstValue) +{ + Init(); + SetName(pszSetName); + SetInt(pszFirstKey, iFirstValue); +} + +//----------------------------------------------------------------------------- +// Purpose: Constructor +// Input : *pszSetName - +// *pszFirstKey - +// *pszFirstValue - +// *pszSecondKey - +// *pszSecondValue - +//----------------------------------------------------------------------------- +KeyValues::KeyValues( + const char* pszSetName, const char* pszFirstKey, const char* pszFirstValue, const char* pszSecondKey, const char* pszSecondValue) +{ + Init(); + SetName(pszSetName); + SetString(pszFirstKey, pszFirstValue); + SetString(pszSecondKey, pszSecondValue); +} + +//----------------------------------------------------------------------------- +// Purpose: Constructor +// Input : *pszSetName - +// *pszFirstKey - +// iFirstValue - +// *pszSecondKey - +// iSecondValue - +//----------------------------------------------------------------------------- +KeyValues::KeyValues(const char* pszSetName, const char* pszFirstKey, int iFirstValue, const char* pszSecondKey, int iSecondValue) +{ + Init(); + SetName(pszSetName); + SetInt(pszFirstKey, iFirstValue); + SetInt(pszSecondKey, iSecondValue); +} + +//----------------------------------------------------------------------------- +// Purpose: Destructor +//----------------------------------------------------------------------------- +KeyValues::~KeyValues(void) +{ + RemoveEverything(); +} + +//----------------------------------------------------------------------------- +// Purpose: Initialize member variables +//----------------------------------------------------------------------------- +void KeyValues::Init(void) +{ + m_iKeyName = 0; + m_iKeyNameCaseSensitive1 = 0; + m_iKeyNameCaseSensitive2 = 0; + m_iDataType = TYPE_NONE; + + m_pSub = nullptr; + m_pPeer = nullptr; + m_pChain = nullptr; + + m_sValue = nullptr; + m_wsValue = nullptr; + m_pValue = nullptr; + + m_bHasEscapeSequences = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: Clear out all subkeys, and the current value +//----------------------------------------------------------------------------- +void KeyValues::Clear(void) +{ + delete m_pSub; + m_pSub = nullptr; + m_iDataType = TYPE_NONE; +} + +//----------------------------------------------------------------------------- +// for backwards compat - we used to need this to force the free to run from the same DLL +// as the alloc +//----------------------------------------------------------------------------- +void KeyValues::DeleteThis(void) +{ + delete this; +} + +//----------------------------------------------------------------------------- +// Purpose: remove everything +//----------------------------------------------------------------------------- +void KeyValues::RemoveEverything(void) +{ + KeyValues* dat; + KeyValues* datNext = nullptr; + for (dat = m_pSub; dat != nullptr; dat = datNext) + { + datNext = dat->m_pPeer; + dat->m_pPeer = nullptr; + delete dat; + } + + for (dat = m_pPeer; dat && dat != this; dat = datNext) + { + datNext = dat->m_pPeer; + dat->m_pPeer = nullptr; + delete dat; + } + + delete[] m_sValue; + m_sValue = nullptr; + delete[] m_wsValue; + m_wsValue = nullptr; +} + +//----------------------------------------------------------------------------- +// Purpose: Find a keyValue, create it if it is not found. +// Set bCreate to true to create the key if it doesn't already exist +// (which ensures a valid pointer will be returned) +// Input : *pszKeyName - +// bCreate - +// Output : *KeyValues +//----------------------------------------------------------------------------- +KeyValues* KeyValues::FindKey(const char* pszKeyName, bool bCreate) +{ + assert_msg(this, "Member function called on NULL KeyValues"); + + if (!pszKeyName || !*pszKeyName) + return this; + + const char* pSubStr = strchr(pszKeyName, '/'); + + HKeySymbol iSearchStr = KeyValuesSystem()->m_pVtable->GetSymbolForString(KeyValuesSystem(), pszKeyName, bCreate); + if (iSearchStr == INVALID_KEY_SYMBOL) + { + // not found, couldn't possibly be in key value list + return nullptr; + } + + KeyValues* pLastKVs = nullptr; + KeyValues* pCurrentKVs; + // find the searchStr in the current peer list + for (pCurrentKVs = m_pSub; pCurrentKVs != NULL; pCurrentKVs = pCurrentKVs->m_pPeer) + { + pLastKVs = pCurrentKVs; // record the last item looked at (for if we need to append to the end of the list) + + // symbol compare + if (pLastKVs->m_iKeyName == (uint32_t)iSearchStr) + break; + } + + if (!pCurrentKVs && m_pChain) + pCurrentKVs = m_pChain->FindKey(pszKeyName, false); + + // make sure a key was found + if (!pCurrentKVs) + { + if (bCreate) + { + // we need to create a new key + pCurrentKVs = new KeyValues(pszKeyName); + // Assert(dat != NULL); + + // insert new key at end of list + if (pLastKVs) + pLastKVs->m_pPeer = pCurrentKVs; + else + m_pSub = pCurrentKVs; + + pCurrentKVs->m_pPeer = NULL; + + // a key graduates to be a submsg as soon as it's m_pSub is set + // this should be the only place m_pSub is set + m_iDataType = TYPE_NONE; + } + else + { + return NULL; + } + } + + // if we've still got a subStr we need to keep looking deeper in the tree + if (pSubStr) + { + // recursively chain down through the paths in the string + return pCurrentKVs->FindKey(pSubStr + 1, bCreate); + } + + return pCurrentKVs; +} + +//----------------------------------------------------------------------------- +// Purpose: Locate last child. Returns NULL if we have no children +// Output : *KeyValues +//----------------------------------------------------------------------------- +KeyValues* KeyValues::FindLastSubKey(void) const +{ + // No children? + if (m_pSub == nullptr) + return nullptr; + + // Scan for the last one + KeyValues* pLastChild = m_pSub; + while (pLastChild->m_pPeer) + pLastChild = pLastChild->m_pPeer; + return pLastChild; +} + +//----------------------------------------------------------------------------- +// Purpose: Adds a subkey. Make sure the subkey isn't a child of some other keyvalues +// Input : *pSubKey - +//----------------------------------------------------------------------------- +void KeyValues::AddSubKey(KeyValues* pSubkey) +{ + // Make sure the subkey isn't a child of some other keyvalues + assert(pSubkey != nullptr); + assert(pSubkey->m_pPeer == nullptr); + + // add into subkey list + if (m_pSub == nullptr) + { + m_pSub = pSubkey; + } + else + { + KeyValues* pTempDat = m_pSub; + while (pTempDat->GetNextKey() != nullptr) + { + pTempDat = pTempDat->GetNextKey(); + } + + pTempDat->SetNextKey(pSubkey); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Remove a subkey from the list +// Input : *pSubKey - +//----------------------------------------------------------------------------- +void KeyValues::RemoveSubKey(KeyValues* pSubKey) +{ + if (!pSubKey) + return; + + // check the list pointer + if (m_pSub == pSubKey) + { + m_pSub = pSubKey->m_pPeer; + } + else + { + // look through the list + KeyValues* kv = m_pSub; + while (kv->m_pPeer) + { + if (kv->m_pPeer == pSubKey) + { + kv->m_pPeer = pSubKey->m_pPeer; + break; + } + + kv = kv->m_pPeer; + } + } + + pSubKey->m_pPeer = nullptr; +} + +//----------------------------------------------------------------------------- +// Purpose: Insert a subkey at index +// Input : nIndex - +// *pSubKey - +//----------------------------------------------------------------------------- +void KeyValues::InsertSubKey(int nIndex, KeyValues* pSubKey) +{ + // Sub key must be valid and not part of another chain + assert(pSubKey && pSubKey->m_pPeer == nullptr); + + if (nIndex == 0) + { + pSubKey->m_pPeer = m_pSub; + m_pSub = pSubKey; + return; + } + else + { + int nCurrentIndex = 0; + for (KeyValues* pIter = GetFirstSubKey(); pIter != nullptr; pIter = pIter->GetNextKey()) + { + ++nCurrentIndex; + if (nCurrentIndex == nIndex) + { + pSubKey->m_pPeer = pIter->m_pPeer; + pIter->m_pPeer = pSubKey; + return; + } + } + // Index is out of range if we get here + assert(0); + return; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Checks if key contains a subkey +// Input : *pSubKey - +// Output : true if contains, false otherwise +//----------------------------------------------------------------------------- +bool KeyValues::ContainsSubKey(KeyValues* pSubKey) +{ + for (KeyValues* pIter = GetFirstSubKey(); pIter != nullptr; pIter = pIter->GetNextKey()) + { + if (pSubKey == pIter) + { + return true; + } + } + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Swaps existing subkey with another +// Input : *pExistingSubkey - +// *pNewSubKey - +//----------------------------------------------------------------------------- +void KeyValues::SwapSubKey(KeyValues* pExistingSubkey, KeyValues* pNewSubKey) +{ + assert(pExistingSubkey != nullptr && pNewSubKey != nullptr); + + // Make sure the new sub key isn't a child of some other keyvalues + assert(pNewSubKey->m_pPeer == nullptr); + + // Check the list pointer + if (m_pSub == pExistingSubkey) + { + pNewSubKey->m_pPeer = pExistingSubkey->m_pPeer; + pExistingSubkey->m_pPeer = nullptr; + m_pSub = pNewSubKey; + } + else + { + // Look through the list + KeyValues* kv = m_pSub; + while (kv->m_pPeer) + { + if (kv->m_pPeer == pExistingSubkey) + { + pNewSubKey->m_pPeer = pExistingSubkey->m_pPeer; + pExistingSubkey->m_pPeer = nullptr; + kv->m_pPeer = pNewSubKey; + break; + } + + kv = kv->m_pPeer; + } + // Existing sub key should always be found, otherwise it's a bug in the calling code. + assert(kv->m_pPeer != nullptr); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Elides subkey +// Input : *pSubKey - +//----------------------------------------------------------------------------- +void KeyValues::ElideSubKey(KeyValues* pSubKey) +{ + // This pointer's "next" pointer needs to be fixed up when we elide the key + KeyValues** ppPointerToFix = &m_pSub; + for (KeyValues* pKeyIter = m_pSub; pKeyIter != nullptr; ppPointerToFix = &pKeyIter->m_pPeer, pKeyIter = pKeyIter->GetNextKey()) + { + if (pKeyIter == pSubKey) + { + if (pSubKey->m_pSub == nullptr) + { + // No children, simply remove the key + *ppPointerToFix = pSubKey->m_pPeer; + delete pSubKey; + } + else + { + *ppPointerToFix = pSubKey->m_pSub; + // Attach the remainder of this chain to the last child of pSubKey + KeyValues* pChildIter = pSubKey->m_pSub; + while (pChildIter->m_pPeer != nullptr) + { + pChildIter = pChildIter->m_pPeer; + } + // Now points to the last child of pSubKey + pChildIter->m_pPeer = pSubKey->m_pPeer; + // Detach the node to be elided + pSubKey->m_pSub = nullptr; + pSubKey->m_pPeer = nullptr; + delete pSubKey; + } + return; + } + } + // Key not found; that's caller error. + assert(0); +} + +//----------------------------------------------------------------------------- +// Purpose: Check if a keyName has no value assigned to it. +// Input : *pszKeyName - +// Output : true on success, false otherwise +//----------------------------------------------------------------------------- +bool KeyValues::IsEmpty(const char* pszKeyName) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (!pKey) + return true; + + if (pKey->m_iDataType == TYPE_NONE && pKey->m_pSub == nullptr) + return true; + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: gets the first true sub key +// Output : *KeyValues +//----------------------------------------------------------------------------- +KeyValues* KeyValues::GetFirstTrueSubKey(void) const +{ + assert_msg(this, "Member function called on NULL KeyValues"); + KeyValues* pRet = this ? m_pSub : nullptr; + while (pRet && pRet->m_iDataType != TYPE_NONE) + pRet = pRet->m_pPeer; + + return pRet; +} + +//----------------------------------------------------------------------------- +// Purpose: gets the next true sub key +// Output : *KeyValues +//----------------------------------------------------------------------------- +KeyValues* KeyValues::GetNextTrueSubKey(void) const +{ + assert_msg(this, "Member function called on NULL KeyValues"); + KeyValues* pRet = this ? m_pPeer : nullptr; + while (pRet && pRet->m_iDataType != TYPE_NONE) + pRet = pRet->m_pPeer; + + return pRet; +} + +//----------------------------------------------------------------------------- +// Purpose: gets the first value +// Output : *KeyValues +//----------------------------------------------------------------------------- +KeyValues* KeyValues::GetFirstValue(void) const +{ + assert_msg(this, "Member function called on NULL KeyValues"); + KeyValues* pRet = this ? m_pSub : nullptr; + while (pRet && pRet->m_iDataType == TYPE_NONE) + pRet = pRet->m_pPeer; + + return pRet; +} + +//----------------------------------------------------------------------------- +// Purpose: gets the next value +// Output : *KeyValues +//----------------------------------------------------------------------------- +KeyValues* KeyValues::GetNextValue(void) const +{ + assert_msg(this, "Member function called on NULL KeyValues"); + KeyValues* pRet = this ? m_pPeer : nullptr; + while (pRet && pRet->m_iDataType == TYPE_NONE) + pRet = pRet->m_pPeer; + + return pRet; +} + +//----------------------------------------------------------------------------- +// Purpose: Return the first subkey in the list +//----------------------------------------------------------------------------- +KeyValues* KeyValues::GetFirstSubKey() const +{ + assert_msg(this, "Member function called on NULL KeyValues"); + return this ? m_pSub : nullptr; +} + +//----------------------------------------------------------------------------- +// Purpose: Return the next subkey +//----------------------------------------------------------------------------- +KeyValues* KeyValues::GetNextKey() const +{ + assert_msg(this, "Member function called on NULL KeyValues"); + return this ? m_pPeer : nullptr; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the name of the current key section +// Output : const char* +//----------------------------------------------------------------------------- +const char* KeyValues::GetName(void) const +{ + return KeyValuesSystem()->m_pVtable->GetStringForSymbol( + KeyValuesSystem(), MAKE_3_BYTES_FROM_1_AND_2(m_iKeyNameCaseSensitive1, m_iKeyNameCaseSensitive2)); +} + +//----------------------------------------------------------------------------- +// Purpose: Get the integer value of a keyName. Default value is returned +// if the keyName can't be found. +// Input : *pszKeyName - +// nDefaultValue - +// Output : int +//----------------------------------------------------------------------------- +int KeyValues::GetInt(const char* pszKeyName, int iDefaultValue) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + { + switch (pKey->m_iDataType) + { + case TYPE_STRING: + return atoi(pKey->m_sValue); + case TYPE_WSTRING: + return _wtoi(pKey->m_wsValue); + case TYPE_FLOAT: + return static_cast(pKey->m_flValue); + case TYPE_UINT64: + // can't convert, since it would lose data + assert(0); + return 0; + case TYPE_INT: + case TYPE_PTR: + default: + return pKey->m_iValue; + }; + } + return iDefaultValue; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the integer value of a keyName. Default value is returned +// if the keyName can't be found. +// Input : *pszKeyName - +// nDefaultValue - +// Output : uint64_t +//----------------------------------------------------------------------------- +uint64_t KeyValues::GetUint64(const char* pszKeyName, uint64_t nDefaultValue) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + { + switch (pKey->m_iDataType) + { + case TYPE_STRING: + { + uint64_t uiResult = 0ull; + sscanf(pKey->m_sValue, "%lld", &uiResult); + return uiResult; + } + case TYPE_WSTRING: + { + uint64_t uiResult = 0ull; + swscanf(pKey->m_wsValue, L"%lld", &uiResult); + return uiResult; + } + case TYPE_FLOAT: + return static_cast(pKey->m_flValue); + case TYPE_UINT64: + return *reinterpret_cast(pKey->m_sValue); + case TYPE_PTR: + return static_cast(reinterpret_cast(pKey->m_pValue)); + case TYPE_INT: + default: + return pKey->m_iValue; + }; + } + return nDefaultValue; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the pointer value of a keyName. Default value is returned +// if the keyName can't be found. +// Input : *pszKeyName - +// pDefaultValue - +// Output : void* +//----------------------------------------------------------------------------- +void* KeyValues::GetPtr(const char* pszKeyName, void* pDefaultValue) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + { + switch (pKey->m_iDataType) + { + case TYPE_PTR: + return pKey->m_pValue; + + case TYPE_WSTRING: + case TYPE_STRING: + case TYPE_FLOAT: + case TYPE_INT: + case TYPE_UINT64: + default: + return nullptr; + }; + } + return pDefaultValue; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the float value of a keyName. Default value is returned +// if the keyName can't be found. +// Input : *pszKeyName - +// flDefaultValue - +// Output : float +//----------------------------------------------------------------------------- +float KeyValues::GetFloat(const char* pszKeyName, float flDefaultValue) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + { + switch (pKey->m_iDataType) + { + case TYPE_STRING: + return static_cast(atof(pKey->m_sValue)); + case TYPE_WSTRING: + return static_cast(_wtof(pKey->m_wsValue)); // no wtof + case TYPE_FLOAT: + return pKey->m_flValue; + case TYPE_INT: + return static_cast(pKey->m_iValue); + case TYPE_UINT64: + return static_cast((*(reinterpret_cast(pKey->m_sValue)))); + case TYPE_PTR: + default: + return 0.0f; + }; + } + return flDefaultValue; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the string pointer of a keyName. Default value is returned +// if the keyName can't be found. +// // Input : *pszKeyName - +// pszDefaultValue - +// Output : const char* +//----------------------------------------------------------------------------- +const char* KeyValues::GetString(const char* pszKeyName, const char* pszDefaultValue) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + { + // convert the data to string form then return it + char buf[64]; + switch (pKey->m_iDataType) + { + case TYPE_FLOAT: + snprintf(buf, sizeof(buf), "%f", pKey->m_flValue); + SetString(pszKeyName, buf); + break; + case TYPE_PTR: + snprintf(buf, sizeof(buf), "%lld", reinterpret_cast(pKey->m_pValue)); + SetString(pszKeyName, buf); + break; + case TYPE_INT: + snprintf(buf, sizeof(buf), "%d", pKey->m_iValue); + SetString(pszKeyName, buf); + break; + case TYPE_UINT64: + snprintf(buf, sizeof(buf), "%lld", *(reinterpret_cast(pKey->m_sValue))); + SetString(pszKeyName, buf); + break; + case TYPE_COLOR: + snprintf(buf, sizeof(buf), "%d %d %d %d", pKey->m_Color[0], pKey->m_Color[1], pKey->m_Color[2], pKey->m_Color[3]); + SetString(pszKeyName, buf); + break; + + case TYPE_WSTRING: + { + // convert the string to char *, set it for future use, and return it + char wideBuf[512]; + int result = V_UnicodeToUTF8(pKey->m_wsValue, wideBuf, 512); + if (result) + { + // note: this will copy wideBuf + SetString(pszKeyName, wideBuf); + } + else + { + return pszDefaultValue; + } + break; + } + case TYPE_STRING: + break; + default: + return pszDefaultValue; + }; + + return pKey->m_sValue; + } + return pszDefaultValue; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the wide string pointer of a keyName. Default value is returned +// if the keyName can't be found. +// // Input : *pszKeyName - +// pwszDefaultValue - +// Output : const wchar_t* +//----------------------------------------------------------------------------- +const wchar_t* KeyValues::GetWString(const char* pszKeyName, const wchar_t* pwszDefaultValue) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + { + wchar_t wbuf[64]; + switch (pKey->m_iDataType) + { + case TYPE_FLOAT: + swprintf(wbuf, ARRAYSIZE(wbuf), L"%f", pKey->m_flValue); + SetWString(pszKeyName, wbuf); + break; + case TYPE_PTR: + swprintf(wbuf, ARRAYSIZE(wbuf), L"%lld", static_cast(reinterpret_cast(pKey->m_pValue))); + SetWString(pszKeyName, wbuf); + break; + case TYPE_INT: + swprintf(wbuf, ARRAYSIZE(wbuf), L"%d", pKey->m_iValue); + SetWString(pszKeyName, wbuf); + break; + case TYPE_UINT64: + { + swprintf(wbuf, ARRAYSIZE(wbuf), L"%lld", *(reinterpret_cast(pKey->m_sValue))); + SetWString(pszKeyName, wbuf); + } + break; + case TYPE_COLOR: + swprintf(wbuf, ARRAYSIZE(wbuf), L"%d %d %d %d", pKey->m_Color[0], pKey->m_Color[1], pKey->m_Color[2], pKey->m_Color[3]); + SetWString(pszKeyName, wbuf); + break; + + case TYPE_WSTRING: + break; + case TYPE_STRING: + { + size_t bufSize = strlen(pKey->m_sValue) + 1; + wchar_t* pWBuf = new wchar_t[bufSize]; + int result = V_UTF8ToUnicode(pKey->m_sValue, pWBuf, static_cast(bufSize * sizeof(wchar_t))); + if (result >= 0) // may be a zero length string + { + SetWString(pszKeyName, pWBuf); + delete[] pWBuf; + } + else + { + delete[] pWBuf; + return pwszDefaultValue; + } + + break; + } + default: + return pwszDefaultValue; + }; + + return reinterpret_cast(pKey->m_wsValue); + } + return pwszDefaultValue; +} + +//----------------------------------------------------------------------------- +// Purpose: Gets a color +// Input : *pszKeyName - +// &defaultColor - +// Output : Color +//----------------------------------------------------------------------------- +Color KeyValues::GetColor(const char* pszKeyName, const Color& defaultColor) +{ + Color color = defaultColor; + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + { + if (pKey->m_iDataType == TYPE_COLOR) + { + color[0] = pKey->m_Color[0]; + color[1] = pKey->m_Color[1]; + color[2] = pKey->m_Color[2]; + color[3] = pKey->m_Color[3]; + } + else if (pKey->m_iDataType == TYPE_FLOAT) + { + color[0] = static_cast(pKey->m_flValue); + } + else if (pKey->m_iDataType == TYPE_INT) + { + color[0] = static_cast(pKey->m_iValue); + } + else if (pKey->m_iDataType == TYPE_STRING) + { + // parse the colors out of the string + float a = 0, b = 0, c = 0, d = 0; + sscanf(pKey->m_sValue, "%f %f %f %f", &a, &b, &c, &d); + color[0] = static_cast(a); + color[1] = static_cast(b); + color[2] = static_cast(c); + color[3] = static_cast(d); + } + } + return color; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the data type of the value stored in a keyName +// Input : *pszKeyName - +//----------------------------------------------------------------------------- +KeyValuesTypes_t KeyValues::GetDataType(const char* pszKeyName) +{ + KeyValues* pKey = FindKey(pszKeyName, false); + if (pKey) + return static_cast(pKey->m_iDataType); + + return TYPE_NONE; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the data type of the value stored in this keyName +//----------------------------------------------------------------------------- +KeyValuesTypes_t KeyValues::GetDataType(void) const +{ + return static_cast(m_iDataType); +} + +//----------------------------------------------------------------------------- +// Purpose: Set the integer value of a keyName. +// Input : *pszKeyName - +// iValue - +//----------------------------------------------------------------------------- +void KeyValues::SetInt(const char* pszKeyName, int iValue) +{ + KeyValues* pKey = FindKey(pszKeyName, true); + if (pKey) + { + pKey->m_iValue = iValue; + pKey->m_iDataType = TYPE_INT; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Set the integer value of a keyName. +//----------------------------------------------------------------------------- +void KeyValues::SetUint64(const char* pszKeyName, uint64_t nValue) +{ + KeyValues* pKey = FindKey(pszKeyName, true); + + if (pKey) + { + // delete the old value + delete[] pKey->m_sValue; + // make sure we're not storing the WSTRING - as we're converting over to STRING + delete[] pKey->m_wsValue; + pKey->m_wsValue = nullptr; + + pKey->m_sValue = new char[sizeof(uint64_t)]; + *(reinterpret_cast(pKey->m_sValue)) = nValue; + pKey->m_iDataType = TYPE_UINT64; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Set the float value of a keyName. +// Input : *pszKeyName - +// flValue - +//----------------------------------------------------------------------------- +void KeyValues::SetFloat(const char* pszKeyName, float flValue) +{ + KeyValues* pKey = FindKey(pszKeyName, true); + if (pKey) + { + pKey->m_flValue = flValue; + pKey->m_iDataType = TYPE_FLOAT; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Set the name value of a keyName. +// Input : *pszSetName - +//----------------------------------------------------------------------------- +void KeyValues::SetName(const char* pszSetName) +{ + HKeySymbol hCaseSensitiveKeyName = INVALID_KEY_SYMBOL, hCaseInsensitiveKeyName = INVALID_KEY_SYMBOL; + hCaseSensitiveKeyName = + KeyValuesSystem()->m_pVtable->GetSymbolForStringCaseSensitive(KeyValuesSystem(), hCaseInsensitiveKeyName, pszSetName, false); + + m_iKeyName = hCaseInsensitiveKeyName; + SPLIT_3_BYTES_INTO_1_AND_2(m_iKeyNameCaseSensitive1, m_iKeyNameCaseSensitive2, hCaseSensitiveKeyName); +} + +//----------------------------------------------------------------------------- +// Purpose: Set the pointer value of a keyName. +// Input : *pszKeyName - +// *pValue - +//----------------------------------------------------------------------------- +void KeyValues::SetPtr(const char* pszKeyName, void* pValue) +{ + KeyValues* pKey = FindKey(pszKeyName, true); + + if (pKey) + { + pKey->m_pValue = pValue; + pKey->m_iDataType = TYPE_PTR; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Set the string value (internal) +// Input : *pszValue - +//----------------------------------------------------------------------------- +void KeyValues::SetStringValue(char const* pszValue) +{ + // delete the old value + delete[] m_sValue; + // make sure we're not storing the WSTRING - as we're converting over to STRING + delete[] m_wsValue; + m_wsValue = nullptr; + + if (!pszValue) + { + // ensure a valid value + pszValue = ""; + } + + // allocate memory for the new value and copy it in + size_t len = strlen(pszValue); + m_sValue = new char[len + 1]; + memcpy(m_sValue, pszValue, len + 1); + + m_iDataType = TYPE_STRING; +} + +//----------------------------------------------------------------------------- +// Purpose: Sets this key's peer to the KeyValues passed in +// Input : *pDat - +//----------------------------------------------------------------------------- +void KeyValues::SetNextKey(KeyValues* pDat) +{ + m_pPeer = pDat; +} + +//----------------------------------------------------------------------------- +// Purpose: Set the string value of a keyName. +// Input : *pszKeyName - +// *pszValue - +//----------------------------------------------------------------------------- +void KeyValues::SetString(const char* pszKeyName, const char* pszValue) +{ + if (KeyValues* pKey = FindKey(pszKeyName, true)) + { + pKey->SetStringValue(pszValue); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Set the string value of a keyName. +// Input : *pszKeyName - +// *pwszValue - +//----------------------------------------------------------------------------- +void KeyValues::SetWString(const char* pszKeyName, const wchar_t* pwszValue) +{ + KeyValues* pKey = FindKey(pszKeyName, true); + if (pKey) + { + // delete the old value + delete[] pKey->m_wsValue; + // make sure we're not storing the STRING - as we're converting over to WSTRING + delete[] pKey->m_sValue; + pKey->m_sValue = nullptr; + + if (!pwszValue) + { + // ensure a valid value + pwszValue = L""; + } + + // allocate memory for the new value and copy it in + size_t len = wcslen(pwszValue); + pKey->m_wsValue = new wchar_t[len + 1]; + memcpy(pKey->m_wsValue, pwszValue, (len + 1) * sizeof(wchar_t)); + + pKey->m_iDataType = TYPE_WSTRING; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Sets a color +// Input : *pszKeyName - +// color - +//----------------------------------------------------------------------------- +void KeyValues::SetColor(const char* pszKeyName, Color color) +{ + KeyValues* pKey = FindKey(pszKeyName, true); + + if (pKey) + { + pKey->m_iDataType = TYPE_COLOR; + pKey->m_Color[0] = color[0]; + pKey->m_Color[1] = color[1]; + pKey->m_Color[2] = color[2]; + pKey->m_Color[3] = color[3]; + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &src - +//----------------------------------------------------------------------------- +void KeyValues::RecursiveCopyKeyValues(KeyValues& src) +{ + // garymcthack - need to check this code for possible buffer overruns. + + m_iKeyName = src.m_iKeyName; + m_iKeyNameCaseSensitive1 = src.m_iKeyNameCaseSensitive1; + m_iKeyNameCaseSensitive2 = src.m_iKeyNameCaseSensitive2; + + if (!src.m_pSub) + { + m_iDataType = src.m_iDataType; + char buf[256]; + switch (src.m_iDataType) + { + case TYPE_NONE: + break; + case TYPE_STRING: + if (src.m_sValue) + { + size_t len = strlen(src.m_sValue) + 1; + m_sValue = new char[len]; + strncpy(m_sValue, src.m_sValue, len); + } + break; + case TYPE_INT: + { + m_iValue = src.m_iValue; + snprintf(buf, sizeof(buf), "%d", m_iValue); + size_t len = strlen(buf) + 1; + m_sValue = new char[len]; + strncpy(m_sValue, buf, len); + } + break; + case TYPE_FLOAT: + { + m_flValue = src.m_flValue; + snprintf(buf, sizeof(buf), "%f", m_flValue); + size_t len = strlen(buf) + 1; + m_sValue = new char[len]; + strncpy(m_sValue, buf, len); + } + break; + case TYPE_PTR: + { + m_pValue = src.m_pValue; + } + break; + case TYPE_UINT64: + { + m_sValue = new char[sizeof(uint64_t)]; + memcpy(m_sValue, src.m_sValue, sizeof(uint64_t)); + } + break; + case TYPE_COLOR: + { + m_Color[0] = src.m_Color[0]; + m_Color[1] = src.m_Color[1]; + m_Color[2] = src.m_Color[2]; + m_Color[3] = src.m_Color[3]; + } + break; + + default: + { + // do nothing . .what the heck is this? + assert(0); + } + break; + } + } + + // Handle the immediate child + if (src.m_pSub) + { + m_pSub = new KeyValues; + + m_pSub->Init(); + m_pSub->SetName(nullptr); + + m_pSub->RecursiveCopyKeyValues(*src.m_pSub); + } + + // Handle the immediate peer + if (src.m_pPeer) + { + m_pPeer = new KeyValues; + + m_pPeer->Init(); + m_pPeer->SetName(nullptr); + + m_pPeer->RecursiveCopyKeyValues(*src.m_pPeer); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Make a new copy of all subkeys, add them all to the passed-in keyvalues +// Input : *pParent - +//----------------------------------------------------------------------------- +void KeyValues::CopySubkeys(KeyValues* pParent) const +{ + // recursively copy subkeys + // Also maintain ordering.... + KeyValues* pPrev = nullptr; + for (KeyValues* pSub = m_pSub; pSub != nullptr; pSub = pSub->m_pPeer) + { + // take a copy of the subkey + KeyValues* pKey = pSub->MakeCopy(); + + // add into subkey list + if (pPrev) + { + pPrev->m_pPeer = pKey; + } + else + { + pParent->m_pSub = pKey; + } + pKey->m_pPeer = nullptr; + pPrev = pKey; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Makes a copy of the whole key-value pair set +// Output : KeyValues* +//----------------------------------------------------------------------------- +KeyValues* KeyValues::MakeCopy(void) const +{ + KeyValues* pNewKeyValue = new KeyValues; + + pNewKeyValue->Init(); + pNewKeyValue->SetName(GetName()); + + // copy data + pNewKeyValue->m_iDataType = m_iDataType; + switch (m_iDataType) + { + case TYPE_STRING: + { + if (m_sValue) + { + size_t len = strlen(m_sValue); + assert(!pNewKeyValue->m_sValue); + pNewKeyValue->m_sValue = new char[len + 1]; + memcpy(pNewKeyValue->m_sValue, m_sValue, len + 1); + } + } + break; + case TYPE_WSTRING: + { + if (m_wsValue) + { + size_t len = wcslen(m_wsValue); + pNewKeyValue->m_wsValue = new wchar_t[len + 1]; + memcpy(pNewKeyValue->m_wsValue, m_wsValue, len + 1 * sizeof(wchar_t)); + } + } + break; + + case TYPE_INT: + pNewKeyValue->m_iValue = m_iValue; + break; + + case TYPE_FLOAT: + pNewKeyValue->m_flValue = m_flValue; + break; + + case TYPE_PTR: + pNewKeyValue->m_pValue = m_pValue; + break; + + case TYPE_COLOR: + pNewKeyValue->m_Color[0] = m_Color[0]; + pNewKeyValue->m_Color[1] = m_Color[1]; + pNewKeyValue->m_Color[2] = m_Color[2]; + pNewKeyValue->m_Color[3] = m_Color[3]; + break; + + case TYPE_UINT64: + pNewKeyValue->m_sValue = new char[sizeof(uint64_t)]; + memcpy(pNewKeyValue->m_sValue, m_sValue, sizeof(uint64_t)); + break; + }; + + // recursively copy subkeys + CopySubkeys(pNewKeyValue); + return pNewKeyValue; +} + +ON_DLL_LOAD("vstdlib.dll", KeyValues, (CModule module)) +{ + V_UTF8ToUnicode = module.GetExport("V_UTF8ToUnicode").As(); + V_UnicodeToUTF8 = module.GetExport("V_UnicodeToUTF8").As(); + KeyValuesSystem = module.GetExport("KeyValuesSystem").As(); +} + +AUTOHOOK_INIT() + +// clang-format off +AUTOHOOK(KeyValues__LoadFromBuffer, engine.dll + 0x426C30, +char, __fastcall, (KeyValues* self, const char* pResourceName, 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(pResourceName, "playlists")) + pFileSystem = pSavedFilesystemPtr; + + return KeyValues__LoadFromBuffer(self, pResourceName, pBuffer, pFileSystem, a5, a6, a7); +} + +ON_DLL_LOAD("engine.dll", EngineKeyValues, (CModule module)) +{ + AUTOHOOK_DISPATCH() +} diff --git a/NorthstarDLL/shared/keyvalues.h b/NorthstarDLL/shared/keyvalues.h new file mode 100644 index 00000000..64ca0cc7 --- /dev/null +++ b/NorthstarDLL/shared/keyvalues.h @@ -0,0 +1,134 @@ +#pragma once +#include "core/math/color.h" + +enum KeyValuesTypes_t : char +{ + TYPE_NONE = 0x0, + TYPE_STRING = 0x1, + TYPE_INT = 0x2, + TYPE_FLOAT = 0x3, + TYPE_PTR = 0x4, + TYPE_WSTRING = 0x5, + TYPE_COLOR = 0x6, + TYPE_UINT64 = 0x7, + TYPE_COMPILED_INT_BYTE = 0x8, + TYPE_COMPILED_INT_0 = 0x9, + TYPE_COMPILED_INT_1 = 0xA, + TYPE_NUMTYPES = 0xB, +}; + +enum MergeKeyValuesOp_t +{ + MERGE_KV_ALL, + MERGE_KV_UPDATE, // update values are copied into storage, adding new keys to storage or updating existing ones + MERGE_KV_DELETE, // update values specify keys that get deleted from storage + MERGE_KV_BORROW, // update values only update existing keys in storage, keys in update that do not exist in storage are discarded +}; + +//----------------------------------------------------------------------------- +// Purpose: Simple recursive data access class +// Used in vgui for message parameters and resource files +// Destructor deletes all child KeyValues nodes +// Data is stored in key (string names) - (string/int/float)value pairs called nodes. +// +// About KeyValues Text File Format: + +// It has 3 control characters '{', '}' and '"'. Names and values may be quoted or +// not. The quote '"' character must not be used within name or values, only for +// quoting whole tokens. You may use escape sequences wile parsing and add within a +// quoted token a \" to add quotes within your name or token. When using Escape +// Sequence the parser must now that by setting KeyValues::UsesEscapeSequences( true ), +// which it's off by default. Non-quoted tokens ends with a whitespace, '{', '}' and '"'. +// So you may use '{' and '}' within quoted tokens, but not for non-quoted tokens. +// An open bracket '{' after a key name indicates a list of subkeys which is finished +// with a closing bracket '}'. Subkeys use the same definitions recursively. +// Whitespaces are space, return, newline and tabulator. Allowed Escape sequences +// are \n, \t, \\, \n and \". The number character '#' is used for macro purposes +// (eg #include), don't use it as first character in key names. +//----------------------------------------------------------------------------- +class KeyValues +{ + private: + KeyValues(); // for internal use only + + public: + // Constructors/destructors + KeyValues(const char* pszSetName); + KeyValues(const char* pszSetName, const char* pszFirstKey, const char* pszFirstValue); + KeyValues(const char* pszSetName, const char* pszFirstKey, const wchar_t* pwszFirstValue); + KeyValues(const char* pszSetName, const char* pszFirstKey, int iFirstValue); + KeyValues( + const char* pszSetName, const char* pszFirstKey, const char* pszFirstValue, const char* pszSecondKey, const char* pszSecondValue); + KeyValues(const char* pszSetName, const char* pszFirstKey, int iFirstValue, const char* pszSecondKey, int iSecondValue); + ~KeyValues(void); + + void Init(void); + void Clear(void); + void DeleteThis(void); + void RemoveEverything(); + + KeyValues* FindKey(const char* pKeyName, bool bCreate = false); + KeyValues* FindLastSubKey(void) const; + + void AddSubKey(KeyValues* pSubkey); + void RemoveSubKey(KeyValues* pSubKey); + void InsertSubKey(int nIndex, KeyValues* pSubKey); + bool ContainsSubKey(KeyValues* pSubKey); + void SwapSubKey(KeyValues* pExistingSubkey, KeyValues* pNewSubKey); + void ElideSubKey(KeyValues* pSubKey); + + // Data access + bool IsEmpty(const char* pszKeyName); + KeyValues* GetFirstTrueSubKey(void) const; + KeyValues* GetNextTrueSubKey(void) const; + KeyValues* GetFirstValue(void) const; + KeyValues* GetNextValue(void) const; + KeyValues* GetFirstSubKey() const; + KeyValues* GetNextKey() const; + const char* GetName(void) const; + int GetInt(const char* pszKeyName, int iDefaultValue); + uint64_t GetUint64(const char* pszKeyName, uint64_t nDefaultValue); + void* GetPtr(const char* pszKeyName, void* pDefaultValue); + float GetFloat(const char* pszKeyName, float flDefaultValue); + const char* GetString(const char* pszKeyName = nullptr, const char* pszDefaultValue = ""); + const wchar_t* GetWString(const char* pszKeyName = nullptr, const wchar_t* pwszDefaultValue = L""); + Color GetColor(const char* pszKeyName, const Color& defaultColor); + KeyValuesTypes_t GetDataType(const char* pszKeyName); + KeyValuesTypes_t GetDataType(void) const; + + // Key writing + void SetInt(const char* pszKeyName, int iValue); + void SetUint64(const char* pszKeyName, uint64_t nValue); + void SetPtr(const char* pszKeyName, void* pValue); + void SetNextKey(KeyValues* pDat); + void SetName(const char* pszName); + void SetString(const char* pszKeyName, const char* pszValue); + void SetWString(const char* pszKeyName, const wchar_t* pwszValue); + void SetStringValue(char const* pszValue); + void SetColor(const char* pszKeyName, Color color); + void SetFloat(const char* pszKeyName, float flValue); + + void RecursiveCopyKeyValues(KeyValues& src); + void CopySubkeys(KeyValues* pParent) const; + KeyValues* MakeCopy(void) const; + + public: + uint32_t m_iKeyName : 24; // 0x0000 + uint32_t m_iKeyNameCaseSensitive1 : 8; // 0x0003 + char* m_sValue; // 0x0008 + wchar_t* m_wsValue; // 0x0010 + union // 0x0018 + { + int m_iValue; + float m_flValue; + void* m_pValue; + unsigned char m_Color[4]; + }; + char m_szShortName[8]; // 0x0020 + char m_iDataType; // 0x0028 + char m_bHasEscapeSequences; // 0x0029 + uint16_t m_iKeyNameCaseSensitive2; // 0x002A + KeyValues* m_pPeer; // 0x0030 + KeyValues* m_pSub; // 0x0038 + KeyValues* m_pChain; // 0x0040 +}; diff --git a/NorthstarDLL/shared/maxplayers.cpp b/NorthstarDLL/shared/maxplayers.cpp new file mode 100644 index 00000000..ebb44341 --- /dev/null +++ b/NorthstarDLL/shared/maxplayers.cpp @@ -0,0 +1,645 @@ +#include "pch.h" +#include "core/tier0.h" +#include "maxplayers.h" + +AUTOHOOK_INIT() + +// never set this to anything below 32 +#define NEW_MAX_PLAYERS 64 +// dg note: the theoretical limit is actually 100, 76 works without entity issues, and 64 works without clientside prediction issues. + +#define PAD_NUMBER(number, boundary) (((number) + ((boundary)-1)) / (boundary)) * (boundary) + +// this is horrible +constexpr int PlayerResource_Name_Start = 0; // Start of modded allocated space. +constexpr int PlayerResource_Name_Size = ((NEW_MAX_PLAYERS + 1) * 8); // const char* m_szName[MAX_PLAYERS + 1]; + +constexpr int PlayerResource_Ping_Start = PlayerResource_Name_Start + PlayerResource_Name_Size; +constexpr int PlayerResource_Ping_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int m_iPing[MAX_PLAYERS + 1]; + +constexpr int PlayerResource_Team_Start = PlayerResource_Ping_Start + PlayerResource_Ping_Size; +constexpr int PlayerResource_Team_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int m_iTeam[MAX_PLAYERS + 1]; + +constexpr int PlayerResource_PRHealth_Start = PlayerResource_Team_Start + PlayerResource_Team_Size; +constexpr int PlayerResource_PRHealth_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int m_iPRHealth[MAX_PLAYERS + 1]; + +constexpr int PlayerResource_Connected_Start = PlayerResource_PRHealth_Start + PlayerResource_PRHealth_Size; +constexpr int PlayerResource_Connected_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int (used as a bool) m_bConnected[MAX_PLAYERS + 1]; + +constexpr int PlayerResource_Alive_Start = PlayerResource_Connected_Start + PlayerResource_Connected_Size; +constexpr int PlayerResource_Alive_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int (used as a bool) m_bAlive[MAX_PLAYERS + 1]; + +constexpr int PlayerResource_BoolStats_Start = PlayerResource_Alive_Start + PlayerResource_Alive_Size; +constexpr int PlayerResource_BoolStats_Size = ((NEW_MAX_PLAYERS + 1) * 4); // int (used as a bool idk) m_boolStats[MAX_PLAYERS + 1]; + +constexpr int PlayerResource_KillStats_Start = PlayerResource_BoolStats_Start + PlayerResource_BoolStats_Size; +constexpr int PlayerResource_KillStats_Length = PAD_NUMBER((NEW_MAX_PLAYERS + 1) * 6, 4); +constexpr int PlayerResource_KillStats_Size = (PlayerResource_KillStats_Length * 6); // int m_killStats[MAX_PLAYERS + 1][6]; + +constexpr int PlayerResource_ScoreStats_Start = PlayerResource_KillStats_Start + PlayerResource_KillStats_Size; +constexpr int PlayerResource_ScoreStats_Length = PAD_NUMBER((NEW_MAX_PLAYERS + 1) * 5, 4); +constexpr int PlayerResource_ScoreStats_Size = (PlayerResource_ScoreStats_Length * 4); // int m_scoreStats[MAX_PLAYERS + 1][5]; + +// must be the usage of the last field to account for any possible paddings +constexpr int PlayerResource_TotalSize = PlayerResource_ScoreStats_Start + PlayerResource_ScoreStats_Size; + +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; + +bool MaxPlayersIncreaseEnabled() +{ + static bool bMaxPlayersIncreaseEnabled = Tier0::CommandLine()->CheckParm("-experimentalmaxplayersincrease"); + return bMaxPlayersIncreaseEnabled; +} + +// should we use R2 for this? not sure +namespace R2 // use R2 namespace for game funcs +{ + int GetMaxPlayers() + { + if (MaxPlayersIncreaseEnabled()) + return NEW_MAX_PLAYERS; + + return 32; + } +} // namespace R2 + +template void ChangeOffset(MemoryAddress addr, unsigned int offset) +{ + addr.Patch((BYTE*)&offset, sizeof(T)); +} + +// 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")) + { + int maxPlayersPowerOf2 = 1; + while (maxPlayersPowerOf2 < NEW_MAX_PLAYERS) + maxPlayersPowerOf2 <<= 1; + + maxentries = maxPlayersPowerOf2; + } + + return StringTables_CreateStringTable(thisptr, name, maxentries, userdatafixedsize, userdatanetworkbits, flags); +} + +ON_DLL_LOAD("engine.dll", MaxPlayersOverride_Engine, (CModule module)) +{ + if (!MaxPlayersIncreaseEnabled()) + return; + + AUTOHOOK_DISPATCH_MODULE(engine.dll) + + // patch GetPlayerLimits to ignore the boundary limit + module.Offset(0x116458).Patch("0xEB"); // jle => jmp + + // patch ED_Alloc to change nFirstIndex + ChangeOffset(module.Offset(0x18F46C + 1), NEW_MAX_PLAYERS + 8 + 1); // original: 41 (sv.GetMaxClients() + 1) + + // patch CGameServer::SpawnServer to change GetMaxClients inline + ChangeOffset(module.Offset(0x119543 + 2), NEW_MAX_PLAYERS + 8 + 1); // original: 41 (sv.GetMaxClients() + 1) + + // patch CGameServer::SpawnServer to change for loop + ChangeOffset(module.Offset(0x11957F + 2), NEW_MAX_PLAYERS); // original: 32 + + // patch CGameServer::SpawnServer to change for loop (there are two) + ChangeOffset(module.Offset(0x119586 + 2), NEW_MAX_PLAYERS + 1); // original: 33 (32 + 1) + + // patch max players somewhere in CClientState + ChangeOffset(module.Offset(0x1A162C + 2), NEW_MAX_PLAYERS - 1); // original: 31 (32 - 1) + + // patch max players in userinfo stringtable creation + /*{ + int maxPlayersPowerOf2 = 1; + while (maxPlayersPowerOf2 < NEW_MAX_PLAYERS) + maxPlayersPowerOf2 <<= 1; + ChangeOffset((char*)baseAddress + 0x114B79 + 3, maxPlayersPowerOf2); // original: 32 + }*/ + // this is not supposed to work at all but it does on 64 players (how) + // proper fix below + + // patch max players in userinfo stringtable creation loop + ChangeOffset(module.Offset(0x114C48 + 2), NEW_MAX_PLAYERS); // original: 32 + + // do not load prebaked SendTable message list + module.Offset(0x75859).Patch("EB"); // jnz -> jmp +} + +typedef void (*RunUserCmds_Type)(bool a1, float a2); +RunUserCmds_Type RunUserCmds_Original; + +HMODULE serverBase = 0; +auto RandomIntZeroMax = (__int64(__fastcall*)())0; + +// lazy rebuild +// clang-format off +AUTOHOOK(RunUserCmds, server.dll + 0x483D10, +void,, (bool a1, float a2)) +// clang-format on +{ + unsigned char v3; // bl + int v5; // er14 + int i; // edi + __int64 v7; // rax + DWORD* v8; // rbx + int v9; // edi + __int64* v10; // rsi + __int64 v11; // rax + int v12; // er12 + __int64 v13; // rdi + int v14; // ebx + int v15; // eax + __int64 v16; // r8 + int v17; // edx + char v18; // r15 + char v19; // bp + int v20; // esi + __int64* v21; // rdi + __int64 v22; // rcx + bool v23; // al + __int64 v24; // rax + __int64 v25[NEW_MAX_PLAYERS]; // [rsp+20h] [rbp-138h] BYREF + + uintptr_t base = (__int64)serverBase; + auto g_pGlobals = *(__int64*)(base + 0xBFBE08); + __int64 globals = g_pGlobals; + + auto g_pEngineServer = *(__int64*)(base + 0xBFBD98); + + auto qword_1814D9648 = *(__int64*)(base + 0x14D9648); + auto qword_1814DA408 = *(__int64*)(base + 0x14DA408); + auto qword_1812107E8 = *(__int64*)(base + 0x12107E8); + auto qword_1812105A8 = *(__int64*)(base + 0x12105A8); + + auto UTIL_PlayerByIndex = (__int64(__fastcall*)(int index))(base + 0x26AA10); + auto sub_180485590 = (void(__fastcall*)(__int64))(base + 0x485590); + auto sub_18058CD80 = (void(__fastcall*)(__int64))(base + 0x58CD80); + auto sub_1805A6D90 = (void(__fastcall*)(__int64))(base + 0x5A6D90); + auto sub_1805A6E50 = (bool(__fastcall*)(__int64, int, char))(base + 0x5A6E50); + auto sub_1805A6C20 = (void(__fastcall*)(__int64))(base + 0x5A6C20); + + v3 = *(unsigned char*)(g_pGlobals + 73); + if (*(DWORD*)(qword_1814D9648 + 92) && + ((*(unsigned __int8(__fastcall**)(__int64))(*(__int64*)g_pEngineServer + 32))(g_pEngineServer) || + !*(DWORD*)(qword_1814DA408 + 92)) && + v3) + { + globals = g_pGlobals; + v5 = 1; + for (i = 1; i <= *(DWORD*)(g_pGlobals + 52); ++i) + { + v7 = UTIL_PlayerByIndex(i); + v8 = (DWORD*)v7; + if (v7) + { + *(__int64*)(base + 0x1210420) = v7; + *(float*)(g_pGlobals + 16) = a2; + if (!a1) + sub_18058CD80(v7); + sub_1805A6D90((__int64)v8); + } + globals = g_pGlobals; + } + memset(v25, 0, sizeof(v25)); + v9 = 0; + if (*(int*)(globals + 52) > 0) + { + v10 = v25; + do + { + v11 = UTIL_PlayerByIndex(++v9); + globals = g_pGlobals; + *v10++ = v11; + } while (v9 < *(DWORD*)(globals + 52)); + } + v12 = *(DWORD*)(qword_1812107E8 + 92); + if (*(DWORD*)(qword_1812105A8 + 92)) + { + v13 = *(DWORD*)(globals + 52) - 1; + if (v13 >= 1) + { + v14 = *(DWORD*)(globals + 52); + do + { + v15 = RandomIntZeroMax(); + v16 = v25[v13--]; + v17 = v15 % v14--; + v25[v13 + 1] = v25[v17]; + v25[v17] = v16; + } while (v13 >= 1); + globals = g_pGlobals; + } + } + v18 = 1; + do + { + v19 = 0; + v20 = 0; + if (*(int*)(globals + 52) > 0) + { + v21 = v25; + do + { + v22 = *v21; + if (*v21) + { + *(__int64*)(base + 0x1210420) = *v21; + *(float*)(globals + 16) = a2; + v23 = sub_1805A6E50(v22, v12, v18); + globals = g_pGlobals; + if (v23) + v19 = 1; + else + *v21 = 0; + } + ++v20; + ++v21; + } while (v20 < *(DWORD*)(globals + 52)); + } + v18 = 0; + } while (v19); + if (*(int*)(globals + 52) >= 1) + { + do + { + v24 = UTIL_PlayerByIndex(v5); + if (v24) + { + *(__int64*)(base + 0x1210420) = v24; + *(float*)(g_pGlobals + 16) = a2; + sub_1805A6C20(v24); + } + ++v5; + } while (v5 <= *(DWORD*)(g_pGlobals + 52)); + } + sub_180485590(*(__int64*)(base + 0xB7B2D8)); + } +} + +// 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(recvProp, elements, flags, name, proxyFn, unk1); +} + +ON_DLL_LOAD("server.dll", MaxPlayersOverride_Server, (CModule module)) +{ + if (!MaxPlayersIncreaseEnabled()) + return; + + AUTOHOOK_DISPATCH_MODULE(server.dll) + + // get required data + serverBase = (HMODULE)module.m_nAddress; + RandomIntZeroMax = (decltype(RandomIntZeroMax))(GetProcAddress(GetModuleHandleA("vstdlib.dll"), "RandomIntZeroMax")); + + // patch max players amount + ChangeOffset(module.Offset(0x9A44D + 3), NEW_MAX_PLAYERS); // 0x20 (32) => 0x80 (128) + + // patch SpawnGlobalNonRewinding to change forced edict index + ChangeOffset(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(module.Offset(0x5C560A + 1), CPlayerResource_ModifiedSize); + + // DT_PlayerResource::m_iPing SendProp + ChangeOffset(module.Offset(0x5C5059 + 2), CPlayerResource_OriginalSize + PlayerResource_Ping_Start); + ChangeOffset(module.Offset(0x5C50A8 + 2), CPlayerResource_OriginalSize + PlayerResource_Ping_Start); + ChangeOffset(module.Offset(0x5C50E2 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_iPing DataMap + ChangeOffset(module.Offset(0xB94598), CPlayerResource_OriginalSize + PlayerResource_Ping_Start); + ChangeOffset(module.Offset(0xB9459C), NEW_MAX_PLAYERS + 1); + ChangeOffset(module.Offset(0xB945C0), PlayerResource_Ping_Size); + + // DT_PlayerResource::m_iTeam SendProp + ChangeOffset(module.Offset(0x5C5110 + 2), CPlayerResource_OriginalSize + PlayerResource_Team_Start); + ChangeOffset(module.Offset(0x5C519C + 2), CPlayerResource_OriginalSize + PlayerResource_Team_Start); + ChangeOffset(module.Offset(0x5C517E + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_iTeam DataMap + ChangeOffset(module.Offset(0xB94600), CPlayerResource_OriginalSize + PlayerResource_Team_Start); + ChangeOffset(module.Offset(0xB94604), NEW_MAX_PLAYERS + 1); + ChangeOffset(module.Offset(0xB94628), PlayerResource_Team_Size); + + // DT_PlayerResource::m_iPRHealth SendProp + ChangeOffset(module.Offset(0x5C51C0 + 2), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + ChangeOffset(module.Offset(0x5C5204 + 2), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + ChangeOffset(module.Offset(0x5C523E + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_iPRHealth DataMap + ChangeOffset(module.Offset(0xB94668), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + ChangeOffset(module.Offset(0xB9466C), NEW_MAX_PLAYERS + 1); + ChangeOffset(module.Offset(0xB94690), PlayerResource_PRHealth_Size); + + // DT_PlayerResource::m_bConnected SendProp + ChangeOffset(module.Offset(0x5C526C + 2), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset(module.Offset(0x5C52B4 + 2), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset(module.Offset(0x5C52EE + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_bConnected DataMap + ChangeOffset(module.Offset(0xB946D0), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset(module.Offset(0xB946D4), NEW_MAX_PLAYERS + 1); + ChangeOffset(module.Offset(0xB946F8), PlayerResource_Connected_Size); + + // DT_PlayerResource::m_bAlive SendProp + ChangeOffset(module.Offset(0x5C5321 + 2), CPlayerResource_OriginalSize + PlayerResource_Alive_Start); + ChangeOffset(module.Offset(0x5C5364 + 2), CPlayerResource_OriginalSize + PlayerResource_Alive_Start); + ChangeOffset(module.Offset(0x5C539E + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_bAlive DataMap + ChangeOffset(module.Offset(0xB94738), CPlayerResource_OriginalSize + PlayerResource_Alive_Start); + ChangeOffset(module.Offset(0xB9473C), NEW_MAX_PLAYERS + 1); + ChangeOffset(module.Offset(0xB94760), PlayerResource_Alive_Size); + + // DT_PlayerResource::m_boolStats SendProp + ChangeOffset(module.Offset(0x5C53CC + 2), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + ChangeOffset(module.Offset(0x5C5414 + 2), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + ChangeOffset(module.Offset(0x5C544E + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_boolStats DataMap + ChangeOffset(module.Offset(0xB947A0), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + ChangeOffset(module.Offset(0xB947A4), NEW_MAX_PLAYERS + 1); + ChangeOffset(module.Offset(0xB947C8), PlayerResource_BoolStats_Size); + + // DT_PlayerResource::m_killStats SendProp + ChangeOffset(module.Offset(0x5C547C + 2), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start); + ChangeOffset(module.Offset(0x5C54E2 + 2), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start); + ChangeOffset(module.Offset(0x5C54FE + 4), PlayerResource_KillStats_Length); + + // DT_PlayerResource::m_killStats DataMap + ChangeOffset(module.Offset(0xB94808), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start); + ChangeOffset(module.Offset(0xB9480C), PlayerResource_KillStats_Length); + ChangeOffset(module.Offset(0xB94830), PlayerResource_KillStats_Size); + + // DT_PlayerResource::m_scoreStats SendProp + ChangeOffset(module.Offset(0x5C5528 + 2), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + ChangeOffset(module.Offset(0x5C5576 + 2), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + ChangeOffset(module.Offset(0x5C5584 + 4), PlayerResource_ScoreStats_Length); + + // DT_PlayerResource::m_scoreStats DataMap + ChangeOffset(module.Offset(0xB94870), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + ChangeOffset(module.Offset(0xB94874), PlayerResource_ScoreStats_Length); + ChangeOffset(module.Offset(0xB94898), PlayerResource_ScoreStats_Size); + + // CPlayerResource::UpdatePlayerData - m_bConnected + ChangeOffset(module.Offset(0x5C66EE + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset(module.Offset(0x5C672E + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); + + // CPlayerResource::UpdatePlayerData - m_iPing + ChangeOffset(module.Offset(0x5C6394 + 4), CPlayerResource_OriginalSize + PlayerResource_Ping_Start); + ChangeOffset(module.Offset(0x5C63DB + 4), CPlayerResource_OriginalSize + PlayerResource_Ping_Start); + + // CPlayerResource::UpdatePlayerData - m_iTeam + ChangeOffset(module.Offset(0x5C63FD + 4), CPlayerResource_OriginalSize + PlayerResource_Team_Start); + ChangeOffset(module.Offset(0x5C6442 + 4), CPlayerResource_OriginalSize + PlayerResource_Team_Start); + + // CPlayerResource::UpdatePlayerData - m_iPRHealth + ChangeOffset(module.Offset(0x5C645B + 4), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + ChangeOffset(module.Offset(0x5C64A0 + 4), CPlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + + // CPlayerResource::UpdatePlayerData - m_bConnected + ChangeOffset(module.Offset(0x5C64AA + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset(module.Offset(0x5C64F0 + 4), CPlayerResource_OriginalSize + PlayerResource_Connected_Start); + + // CPlayerResource::UpdatePlayerData - m_bAlive + ChangeOffset(module.Offset(0x5C650A + 4), CPlayerResource_OriginalSize + PlayerResource_Alive_Start); + ChangeOffset(module.Offset(0x5C654F + 4), CPlayerResource_OriginalSize + PlayerResource_Alive_Start); + + // CPlayerResource::UpdatePlayerData - m_boolStats + ChangeOffset(module.Offset(0x5C6557 + 4), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + ChangeOffset(module.Offset(0x5C65A5 + 4), CPlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + + // CPlayerResource::UpdatePlayerData - m_scoreStats + ChangeOffset(module.Offset(0x5C65C2 + 3), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + ChangeOffset(module.Offset(0x5C65E3 + 4), CPlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + + // CPlayerResource::UpdatePlayerData - m_killStats + ChangeOffset(module.Offset(0x5C6654 + 3), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start); + ChangeOffset(module.Offset(0x5C665B + 3), CPlayerResource_OriginalSize + PlayerResource_KillStats_Start); + + *module.Offset(0x14E7390).As() = 0; + auto DT_PlayerResource_Construct = module.Offset(0x5C4FE0).As<__int64(__fastcall*)()>(); + DT_PlayerResource_Construct(); + + constexpr int CTeam_OriginalSize = 3336; + constexpr int CTeam_AddedSize = Team_AddedSize; + constexpr int CTeam_ModifiedSize = CTeam_OriginalSize + CTeam_AddedSize; + + // CTeam class allocation function - allocate a bigger amount to fit all new team player data + ChangeOffset(module.Offset(0x23924A + 1), CTeam_ModifiedSize); + + // CTeam::CTeam - increase memset length to clean newly allocated data + ChangeOffset(module.Offset(0x2395AE + 2), 256 + CTeam_AddedSize); + + *module.Offset(0xC945A0).As() = 0; + auto DT_Team_Construct = module.Offset(0x238F50).As<__int64(__fastcall*)()>(); + DT_Team_Construct(); +} + +// 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(recvProp, elements, flags, name, proxyFn); +} + +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(module.Offset(0x164C41 + 1), C_PlayerResource_ModifiedSize); + + // C_PlayerResource::C_PlayerResource - change loop end value + ChangeOffset(module.Offset(0x1640C4 + 2), NEW_MAX_PLAYERS - 32); + + // C_PlayerResource::C_PlayerResource - change m_szName address + ChangeOffset( + 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( + 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(module.Offset(0x1640D0 + 3), 2244 + C_PlayerResource_AddedSize); + + // C_PlayerResource::UpdatePlayerName - change m_szName address + ChangeOffset(module.Offset(0x16431F + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName - change m_szName address 1 + ChangeOffset(module.Offset(0x1645B1 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName - change m_szName address 2 + ChangeOffset(module.Offset(0x1645C0 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName - change m_szName address 3 + ChangeOffset(module.Offset(0x1645DD + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName internal func - change m_szName address 1 + ChangeOffset(module.Offset(0x164B71 + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName internal func - change m_szName address 2 + ChangeOffset(module.Offset(0x164B9B + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName2 (?) - change m_szName address 1 + ChangeOffset(module.Offset(0x164641 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName2 (?) - change m_szName address 2 + ChangeOffset(module.Offset(0x164650 + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName2 (?) - change m_szName address 3 + ChangeOffset(module.Offset(0x16466D + 3), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName internal func - change m_szName2 (?) address 1 + ChangeOffset(module.Offset(0x164BA3 + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName internal func - change m_szName2 (?) address 2 + ChangeOffset(module.Offset(0x164BCE + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::GetPlayerName internal func - change m_szName2 (?) address 3 + ChangeOffset(module.Offset(0x164BE7 + 4), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + + // C_PlayerResource::m_szName + ChangeOffset(module.Offset(0xc350f8), C_PlayerResource_OriginalSize + PlayerResource_Name_Start); + ChangeOffset(module.Offset(0xc350f8 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource size + ChangeOffset(module.Offset(0x163415 + 6), C_PlayerResource_ModifiedSize); + + // DT_PlayerResource::m_iPing RecvProp + ChangeOffset(module.Offset(0x163492 + 2), C_PlayerResource_OriginalSize + PlayerResource_Ping_Start); + ChangeOffset(module.Offset(0x1634D6 + 2), C_PlayerResource_OriginalSize + PlayerResource_Ping_Start); + ChangeOffset(module.Offset(0x163515 + 5), NEW_MAX_PLAYERS + 1); + + // C_PlayerResource::m_iPing + ChangeOffset(module.Offset(0xc35170), C_PlayerResource_OriginalSize + PlayerResource_Ping_Start); + ChangeOffset(module.Offset(0xc35170 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_iTeam RecvProp + ChangeOffset(module.Offset(0x163549 + 2), C_PlayerResource_OriginalSize + PlayerResource_Team_Start); + ChangeOffset(module.Offset(0x1635C8 + 2), C_PlayerResource_OriginalSize + PlayerResource_Team_Start); + ChangeOffset(module.Offset(0x1635AD + 5), NEW_MAX_PLAYERS + 1); + + // C_PlayerResource::m_iTeam + ChangeOffset(module.Offset(0xc351e8), C_PlayerResource_OriginalSize + PlayerResource_Team_Start); + ChangeOffset(module.Offset(0xc351e8 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_iPRHealth RecvProp + ChangeOffset(module.Offset(0x1635F9 + 2), C_PlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + ChangeOffset(module.Offset(0x163625 + 2), C_PlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + ChangeOffset(module.Offset(0x163675 + 5), NEW_MAX_PLAYERS + 1); + + // C_PlayerResource::m_iPRHealth + ChangeOffset(module.Offset(0xc35260), C_PlayerResource_OriginalSize + PlayerResource_PRHealth_Start); + ChangeOffset(module.Offset(0xc35260 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_bConnected RecvProp + ChangeOffset(module.Offset(0x1636A9 + 2), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset(module.Offset(0x1636D5 + 2), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset(module.Offset(0x163725 + 5), NEW_MAX_PLAYERS + 1); + + // C_PlayerResource::m_bConnected + ChangeOffset(module.Offset(0xc352d8), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); + ChangeOffset(module.Offset(0xc352d8 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_bAlive RecvProp + ChangeOffset(module.Offset(0x163759 + 2), C_PlayerResource_OriginalSize + PlayerResource_Alive_Start); + ChangeOffset(module.Offset(0x163785 + 2), C_PlayerResource_OriginalSize + PlayerResource_Alive_Start); + ChangeOffset(module.Offset(0x1637D5 + 5), NEW_MAX_PLAYERS + 1); + + // C_PlayerResource::m_bAlive + ChangeOffset(module.Offset(0xc35350), C_PlayerResource_OriginalSize + PlayerResource_Alive_Start); + ChangeOffset(module.Offset(0xc35350 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_boolStats RecvProp + ChangeOffset(module.Offset(0x163809 + 2), C_PlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + ChangeOffset(module.Offset(0x163835 + 2), C_PlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + ChangeOffset(module.Offset(0x163885 + 5), NEW_MAX_PLAYERS + 1); + + // C_PlayerResource::m_boolStats + ChangeOffset(module.Offset(0xc353c8), C_PlayerResource_OriginalSize + PlayerResource_BoolStats_Start); + ChangeOffset(module.Offset(0xc353c8 + 4), NEW_MAX_PLAYERS + 1); + + // DT_PlayerResource::m_killStats RecvProp + ChangeOffset(module.Offset(0x1638B3 + 2), C_PlayerResource_OriginalSize + PlayerResource_KillStats_Start); + ChangeOffset(module.Offset(0x1638E5 + 2), C_PlayerResource_OriginalSize + PlayerResource_KillStats_Start); + ChangeOffset(module.Offset(0x163935 + 5), PlayerResource_KillStats_Length); + + // C_PlayerResource::m_killStats + ChangeOffset(module.Offset(0xc35440), C_PlayerResource_OriginalSize + PlayerResource_KillStats_Start); + ChangeOffset(module.Offset(0xc35440 + 4), PlayerResource_KillStats_Length); + + // DT_PlayerResource::m_scoreStats RecvProp + ChangeOffset(module.Offset(0x163969 + 2), C_PlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + ChangeOffset(module.Offset(0x163995 + 2), C_PlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + ChangeOffset(module.Offset(0x1639E5 + 5), PlayerResource_ScoreStats_Length); + + // C_PlayerResource::m_scoreStats + ChangeOffset(module.Offset(0xc354b8), C_PlayerResource_OriginalSize + PlayerResource_ScoreStats_Start); + ChangeOffset(module.Offset(0xc354b8 + 4), PlayerResource_ScoreStats_Length); + + // C_PlayerResource::GetPlayerName - change m_bConnected address + ChangeOffset(module.Offset(0x164599 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); + + // C_PlayerResource::GetPlayerName2 (?) - change m_bConnected address + ChangeOffset(module.Offset(0x164629 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); + + // C_PlayerResource::GetPlayerName internal func - change m_bConnected address + ChangeOffset(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(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(module.Offset(0x164834 + 3), C_PlayerResource_OriginalSize + PlayerResource_Connected_Start); + + *module.Offset(0xC35068).As() = 0; + auto DT_PlayerResource_Construct = module.Offset(0x163400).As<__int64(__fastcall*)()>(); + DT_PlayerResource_Construct(); + + constexpr int C_Team_OriginalSize = 3200; + constexpr int C_Team_AddedSize = Team_AddedSize; + 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(module.Offset(0x182321 + 1), C_Team_ModifiedSize); + + // C_Team::C_Team - increase memset length to clean newly allocated data + ChangeOffset(module.Offset(0x1804A2 + 2), 256 + C_Team_AddedSize); + + // DT_Team size + ChangeOffset(module.Offset(0xC3AA0C), C_Team_ModifiedSize); + + *module.Offset(0xC3AFF8).As() = 0; + auto DT_Team_Construct = module.Offset(0x17F950).As<__int64(__fastcall*)()>(); + DT_Team_Construct(); +} diff --git a/NorthstarDLL/shared/maxplayers.h b/NorthstarDLL/shared/maxplayers.h new file mode 100644 index 00000000..b251f6a6 --- /dev/null +++ b/NorthstarDLL/shared/maxplayers.h @@ -0,0 +1,7 @@ +#pragma once + +// should we use R2 for this? not sure +namespace R2 // use R2 namespace for game funcs +{ + int GetMaxPlayers(); +} // namespace R2 diff --git a/NorthstarDLL/shared/misccommands.cpp b/NorthstarDLL/shared/misccommands.cpp new file mode 100644 index 00000000..ad8b2a32 --- /dev/null +++ b/NorthstarDLL/shared/misccommands.cpp @@ -0,0 +1,314 @@ +#include "pch.h" +#include "misccommands.h" +#include "core/convar/concommand.h" +#include "shared/playlist.h" +#include "engine/r2engine.h" +#include "client/r2client.h" +#include "core/tier0.h" +#include "engine/hoststate.h" +#include "masterserver/masterserver.h" +#include "mods/modmanager.h" +#include "server/auth/serverauthentication.h" +#include "squirrel/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->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() +{ + RegisterConCommand( + "force_newgame", + ConCommand_force_newgame, + "forces a map load through directly setting g_pHostState->m_iNextState to HS_NEW_GAME", + FCVAR_NONE); + + 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); + + // this is a concommand because we make a deferred call to it from another thread + 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(); + + 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> 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}, + + // fcvars that should be cheats + {"net_ignoreAllSnapshots", FCVAR_CHEAT}, + {"highlight_draw", FCVAR_CHEAT}, + // these should potentially be replicated rather than cheat, like sv_footsteps is + // however they're defined on client, so can't make replicated atm sadly + {"cl_footstep_event_max_dist", FCVAR_CHEAT}, + {"cl_footstep_event_max_dist_titan", FCVAR_CHEAT}, + }; + + // array of cvars and the flags we want to remove from them + const std::vector> 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}, + + // these are devonly by default but should be modifyable + // NOTE: not all of these may actually do anything or work properly in practice + // network settings + {"cl_updaterate_mp", FCVAR_DEVELOPMENTONLY}, + {"cl_updaterate_sp", FCVAR_DEVELOPMENTONLY}, + {"clock_bias_sp", FCVAR_DEVELOPMENTONLY}, + {"clock_bias_mp", FCVAR_DEVELOPMENTONLY}, + {"cl_interpolate", FCVAR_DEVELOPMENTONLY}, // super duper ultra fucks anims if changed + {"cl_interpolateSoAllAnimsLoop", FCVAR_DEVELOPMENTONLY}, + {"cl_cmdrate", FCVAR_DEVELOPMENTONLY}, + {"cl_cmdbackup", FCVAR_DEVELOPMENTONLY}, + {"rate", FCVAR_DEVELOPMENTONLY}, + {"net_minroutable", FCVAR_DEVELOPMENTONLY}, + {"net_maxroutable", FCVAR_DEVELOPMENTONLY}, + {"net_lerpFields", FCVAR_DEVELOPMENTONLY}, + {"net_ignoreAllSnapshots", FCVAR_DEVELOPMENTONLY}, + {"net_chokeloop", FCVAR_DEVELOPMENTONLY}, + {"sv_unlag", FCVAR_DEVELOPMENTONLY}, + {"sv_maxunlag", FCVAR_DEVELOPMENTONLY}, + {"sv_lagpushticks", FCVAR_DEVELOPMENTONLY}, + {"sv_instancebaselines", FCVAR_DEVELOPMENTONLY}, + {"sv_voiceEcho", FCVAR_DEVELOPMENTONLY}, + {"net_compresspackets", FCVAR_DEVELOPMENTONLY}, + {"net_compresspackets_minsize", FCVAR_DEVELOPMENTONLY}, + {"net_verifyEncryption", FCVAR_DEVELOPMENTONLY}, // unsure if functional in retail + + // gameplay settings + {"vel_samples", FCVAR_DEVELOPMENTONLY}, + {"vel_sampleFrequency", FCVAR_DEVELOPMENTONLY}, + {"sv_friction", FCVAR_DEVELOPMENTONLY}, + {"sv_stopspeed", FCVAR_DEVELOPMENTONLY}, + {"sv_airaccelerate", FCVAR_DEVELOPMENTONLY}, + {"sv_forceGrapplesToFail", FCVAR_DEVELOPMENTONLY}, + {"sv_maxvelocity", FCVAR_DEVELOPMENTONLY}, + {"sv_footsteps", FCVAR_DEVELOPMENTONLY}, + // these 2 are flagged as CHEAT above, could be made REPLICATED later potentially + {"cl_footstep_event_max_dist", FCVAR_DEVELOPMENTONLY}, + {"cl_footstep_event_max_dist_titan", FCVAR_DEVELOPMENTONLY}, + {"sv_balanceTeams", FCVAR_DEVELOPMENTONLY}, + {"rodeo_enable", FCVAR_DEVELOPMENTONLY}, + {"sv_forceRodeoToFail", FCVAR_DEVELOPMENTONLY}, + {"player_find_rodeo_target_per_cmd", FCVAR_DEVELOPMENTONLY}, // todo test before merge + {"hud_takesshots", FCVAR_DEVELOPMENTONLY}, // very likely does not work but would be cool if it did + + {"cam_collision", FCVAR_DEVELOPMENTONLY}, + {"cam_idealdelta", FCVAR_DEVELOPMENTONLY}, + {"cam_ideallag", FCVAR_DEVELOPMENTONLY}, + + // graphics/visual settings + {"mat_colorcorrection", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoRadius", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoDepthMax", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoBlurSharpness", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoIntensity", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoBias", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoDistanceLerp", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoBlurRadius", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoExponent", FCVAR_DEVELOPMENTONLY}, + {"r_hbaoDepthFadePctDefault", FCVAR_DEVELOPMENTONLY}, + {"r_drawscreenspaceparticles", FCVAR_DEVELOPMENTONLY}, + {"ui_loadingscreen_fadeout_time", FCVAR_DEVELOPMENTONLY}, + {"ui_loadingscreen_fadein_time", FCVAR_DEVELOPMENTONLY}, + {"ui_loadingscreen_transition_time", FCVAR_DEVELOPMENTONLY}, + {"ui_loadingscreen_mintransition_time", FCVAR_DEVELOPMENTONLY}, + // these 2 could be FCVAR_CHEAT, i guess? + {"cl_draw_player_model", FCVAR_DEVELOPMENTONLY}, + {"cl_always_draw_3p_player", FCVAR_DEVELOPMENTONLY}, + {"idcolor_neutral", FCVAR_DEVELOPMENTONLY}, + {"idcolor_ally", FCVAR_DEVELOPMENTONLY}, + {"idcolor_ally_cb1", FCVAR_DEVELOPMENTONLY}, + {"idcolor_ally_cb2", FCVAR_DEVELOPMENTONLY}, + {"idcolor_ally_cb3", FCVAR_DEVELOPMENTONLY}, + {"idcolor_enemy", FCVAR_DEVELOPMENTONLY}, + {"idcolor_enemy_cb1", FCVAR_DEVELOPMENTONLY}, + {"idcolor_enemy_cb2", FCVAR_DEVELOPMENTONLY}, + {"idcolor_enemy_cb3", FCVAR_DEVELOPMENTONLY}, + {"playerListPartyColorR", FCVAR_DEVELOPMENTONLY}, + {"playerListPartyColorG", FCVAR_DEVELOPMENTONLY}, + {"playerListPartyColorB", FCVAR_DEVELOPMENTONLY}, + {"playerListUseFriendColor", FCVAR_DEVELOPMENTONLY}, + {"fx_impact_neutral", FCVAR_DEVELOPMENTONLY}, + {"fx_impact_ally", FCVAR_DEVELOPMENTONLY}, + {"fx_impact_enemy", FCVAR_DEVELOPMENTONLY}, + {"hitch_alert_color", FCVAR_DEVELOPMENTONLY}, + {"particles_cull_all", FCVAR_DEVELOPMENTONLY}, + {"particles_cull_dlights", FCVAR_DEVELOPMENTONLY}, + {"map_settings_override", FCVAR_DEVELOPMENTONLY}, + {"highlight_draw", FCVAR_DEVELOPMENTONLY}, + + // sys/engine settings + {"sleep_when_meeting_framerate", FCVAR_DEVELOPMENTONLY}, + {"sleep_when_meeting_framerate_headroom_ms", FCVAR_DEVELOPMENTONLY}, + {"not_focus_sleep", FCVAR_DEVELOPMENTONLY}, + {"sp_not_focus_pause", FCVAR_DEVELOPMENTONLY}, + {"joy_requireFocus", FCVAR_DEVELOPMENTONLY}, + + {"host_thread_mode", FCVAR_DEVELOPMENTONLY}, + {"phys_enable_simd_optimizations", FCVAR_DEVELOPMENTONLY}, + {"phys_enable_experimental_optimizations", FCVAR_DEVELOPMENTONLY}, + + {"community_frame_run", FCVAR_DEVELOPMENTONLY}, + {"sv_single_core_dedi", FCVAR_DEVELOPMENTONLY}, + {"sv_stressbots", FCVAR_DEVELOPMENTONLY}, + + {"fatal_script_errors", FCVAR_DEVELOPMENTONLY}, + {"fatal_script_errors_client", FCVAR_DEVELOPMENTONLY}, + {"fatal_script_errors_server", FCVAR_DEVELOPMENTONLY}, + {"script_error_on_midgame_load", FCVAR_DEVELOPMENTONLY}, // idk what this is + + {"ai_ainRebuildOnMapStart", FCVAR_DEVELOPMENTONLY}, + + {"save_enable", FCVAR_DEVELOPMENTONLY}, + + // cheat commands + {"switchclass", FCVAR_DEVELOPMENTONLY}, + {"set", FCVAR_DEVELOPMENTONLY}, + {"_setClassVarServer", FCVAR_DEVELOPMENTONLY}, + + // reparse commands + {"aisettings_reparse", FCVAR_DEVELOPMENTONLY}, + {"aisettings_reparse_client", FCVAR_DEVELOPMENTONLY}, + {"damagedefs_reparse", FCVAR_DEVELOPMENTONLY}, + {"damagedefs_reparse_client", FCVAR_DEVELOPMENTONLY}, + {"playerSettings_reparse", FCVAR_DEVELOPMENTONLY}, + {"_playerSettings_reparse_Server", FCVAR_DEVELOPMENTONLY}, + + }; + + const std::vector> CVAR_FIXUP_DEFAULT_VALUES = { + {"sv_stressbots", "0"}, // not currently used but this is probably a bad default if we get bots working + {"cl_pred_optimize", "0"} // fixes issues with animation prediction in thirdperson + }; + + 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); + } + + for (auto& fixup : CVAR_FIXUP_DEFAULT_VALUES) + { + ConVar* cvar = R2::g_pCVar->FindVar(std::get<0>(fixup)); + if (cvar && !strcmp(cvar->GetString(), cvar->m_pszDefaultValue)) + { + cvar->SetValue(std::get<1>(fixup)); + cvar->m_pszDefaultValue = std::get<1>(fixup); + } + } +} diff --git a/NorthstarDLL/shared/misccommands.h b/NorthstarDLL/shared/misccommands.h new file mode 100644 index 00000000..07a07fb3 --- /dev/null +++ b/NorthstarDLL/shared/misccommands.h @@ -0,0 +1,3 @@ +#pragma once +void AddMiscConCommands(); +void FixupCvarFlags(); diff --git a/NorthstarDLL/shared/playlist.cpp b/NorthstarDLL/shared/playlist.cpp new file mode 100644 index 00000000..2fb856b3 --- /dev/null +++ b/NorthstarDLL/shared/playlist.cpp @@ -0,0 +1,130 @@ +#include "pch.h" +#include "playlist.h" +#include "core/convar/concommand.h" +#include "core/convar/convar.h" +#include "squirrel/squirrel.h" +#include "engine/hoststate.h" +#include "server/serverpresence.h" + +AUTOHOOK_INIT() + +// 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; + + R2::SetCurrentPlaylist(args.Arg(1)); +} + +void ConCommand_setplaylistvaroverride(const CCommand& args) +{ + if (args.ArgC() < 3) + return; + + for (int i = 1; i < args.ArgC(); i += 2) + R2::SetPlaylistVarOverride(args.Arg(i), args.Arg(i + 1)); +} + +ON_DLL_LOAD_RELIESON("engine.dll", PlaylistHooks, (ConCommand, ConVar), (CModule module)) +{ + AUTOHOOK_DISPATCH() + + R2::GetCurrentPlaylistName = module.Offset(0x18C640).As(); + R2::SetCurrentPlaylist = module.Offset(0x18EB20).As(); + R2::SetPlaylistVarOverride = module.Offset(0x18ED00).As(); + R2::GetCurrentPlaylistVar = module.Offset(0x18C680).As(); + + // 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 + // 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"); + + // 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 + module.Offset(0x18ED8D).Patch("C3"); + + // patch to allow setplaylistvaroverride to be called before map init on dedicated and private match launched through the game + module.Offset(0x18ED17).NOP(6); +} diff --git a/NorthstarDLL/shared/playlist.h b/NorthstarDLL/shared/playlist.h new file mode 100644 index 00000000..c77b37d9 --- /dev/null +++ b/NorthstarDLL/shared/playlist.h @@ -0,0 +1,10 @@ +#pragma once + +// 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 diff --git a/NorthstarDLL/shared/sourceinterface.cpp b/NorthstarDLL/shared/sourceinterface.cpp new file mode 100644 index 00000000..ed95de50 --- /dev/null +++ b/NorthstarDLL/shared/sourceinterface.cpp @@ -0,0 +1,49 @@ +#include "pch.h" +#include "sourceinterface.h" +#include "console/sourceconsole.h" + +AUTOHOOK_INIT() + +// really wanted to do a modular callback system here but honestly couldn't be bothered so hardcoding stuff for now: todo later + +// clang-format off +AUTOHOOK_PROCADDRESS(ClientCreateInterface, client.dll, CreateInterface, +void*, __fastcall, (const char* pName, const int* pReturnCode)) +// clang-format on +{ + void* ret = ClientCreateInterface(pName, pReturnCode); + spdlog::info("CreateInterface CLIENT {}", pName); + + if (!strcmp(pName, "GameClientExports001")) + InitialiseConsoleOnInterfaceCreation(); + + return ret; +} + +// clang-format off +AUTOHOOK_PROCADDRESS(ServerCreateInterface, server.dll, CreateInterface, +void*, __fastcall, (const char* pName, const int* pReturnCode)) +// clang-format on +{ + void* ret = ServerCreateInterface(pName, pReturnCode); + spdlog::info("CreateInterface SERVER {}", pName); + + return ret; +} + +// clang-format off +AUTOHOOK_PROCADDRESS(EngineCreateInterface, engine.dll, CreateInterface, +void*, __fastcall, (const char* pName, const int* pReturnCode)) +// clang-format on +{ + void* ret = EngineCreateInterface(pName, pReturnCode); + spdlog::info("CreateInterface ENGINE {}", pName); + + return ret; +} + +// clang-format off +ON_DLL_LOAD("client.dll", ClientInterface, (CModule module)) {AUTOHOOK_DISPATCH_MODULE(client.dll)} +ON_DLL_LOAD("server.dll", ServerInterface, (CModule module)) {AUTOHOOK_DISPATCH_MODULE(server.dll)} +ON_DLL_LOAD("engine.dll", EngineInterface, (CModule module)) {AUTOHOOK_DISPATCH_MODULE(engine.dll)} +// clang-format on diff --git a/NorthstarDLL/shared/sourceinterface.h b/NorthstarDLL/shared/sourceinterface.h new file mode 100644 index 00000000..474e961b --- /dev/null +++ b/NorthstarDLL/shared/sourceinterface.h @@ -0,0 +1,31 @@ +#pragma once +#include + +// literally just copied from ttf2sdk definition +typedef void* (*CreateInterfaceFn)(const char* pName, int* pReturnCode); + +template class SourceInterface +{ + private: + T* m_interface; + + public: + SourceInterface(const std::string& moduleName, const std::string& interfaceName) + { + HMODULE handle = GetModuleHandleA(moduleName.c_str()); + CreateInterfaceFn createInterface = (CreateInterfaceFn)GetProcAddress(handle, "CreateInterface"); + m_interface = (T*)createInterface(interfaceName.c_str(), NULL); + if (m_interface == nullptr) + spdlog::error("Failed to call CreateInterface for %s in %s", interfaceName, moduleName); + } + + T* operator->() const + { + return m_interface; + } + + operator T*() const + { + return m_interface; + } +}; diff --git a/NorthstarDLL/sourceconsole.cpp b/NorthstarDLL/sourceconsole.cpp deleted file mode 100644 index 0048ac4c..00000000 --- a/NorthstarDLL/sourceconsole.cpp +++ /dev/null @@ -1,92 +0,0 @@ -#include "pch.h" -#include "convar.h" -#include "sourceconsole.h" -#include "sourceinterface.h" -#include "concommand.h" -#include "printcommand.h" - -SourceInterface* g_pSourceGameConsole; - -void ConCommand_toggleconsole(const CCommand& arg) -{ - if ((*g_pSourceGameConsole)->IsConsoleVisible()) - (*g_pSourceGameConsole)->Hide(); - else - (*g_pSourceGameConsole)->Activate(); -} - -void ConCommand_showconsole(const CCommand& arg) -{ - (*g_pSourceGameConsole)->Activate(); -} - -void ConCommand_hideconsole(const CCommand& arg) -{ - (*g_pSourceGameConsole)->Hide(); -} - -void SourceConsoleSink::custom_sink_it_(const custom_log_msg& msg) -{ - if (!(*g_pSourceGameConsole)->m_bInitialized) - return; - - spdlog::memory_buf_t formatted; - spdlog::sinks::base_sink::formatter_->format(msg, formatted); - - // get message string - std::string str = fmt::to_string(formatted); - - SourceColor levelColor = m_LogColours[msg.level]; - std::string name {msg.logger_name.begin(), msg.logger_name.end()}; - - (*g_pSourceGameConsole)->m_pConsole->m_pConsolePanel->ColorPrint(msg.origin->SRCColor, ("[" + name + "]").c_str()); - (*g_pSourceGameConsole)->m_pConsole->m_pConsolePanel->Print(" "); - (*g_pSourceGameConsole)->m_pConsole->m_pConsolePanel->ColorPrint(levelColor, ("[" + std::string(level_names[msg.level]) + "]").c_str()); - (*g_pSourceGameConsole)->m_pConsole->m_pConsolePanel->Print(" "); - (*g_pSourceGameConsole)->m_pConsole->m_pConsolePanel->Print(fmt::to_string(formatted).c_str()); -} - -void SourceConsoleSink::sink_it_(const spdlog::details::log_msg& msg) -{ - throw std::runtime_error("sink_it_ called on SourceConsoleSink with pure log_msg. This is an error!"); -} - -void SourceConsoleSink::flush_() {} - -// clang-format off -HOOK(OnCommandSubmittedHook, OnCommandSubmitted, -void, __fastcall, (CConsoleDialog* consoleDialog, const char* pCommand)) -// clang-format on -{ - consoleDialog->m_pConsolePanel->Print("] "); - consoleDialog->m_pConsolePanel->Print(pCommand); - consoleDialog->m_pConsolePanel->Print("\n"); - - TryPrintCvarHelpForCommand(pCommand); - - OnCommandSubmitted(consoleDialog, pCommand); -} - -// called from sourceinterface.cpp in client createinterface hooks, on GameClientExports001 -void InitialiseConsoleOnInterfaceCreation() -{ - (*g_pSourceGameConsole)->Initialize(); - // hook OnCommandSubmitted so we print inputted commands - OnCommandSubmittedHook.Dispatch((*g_pSourceGameConsole)->m_pConsole->m_vtable->OnCommandSubmitted); - - auto consoleSink = std::make_shared(); - if (g_bSpdLog_UseAnsiColor) - consoleSink->set_pattern("%v"); // no need to include the level in the game console, the text colour signifies it anyway - else - consoleSink->set_pattern("[%n] [%l] %v"); // no colour, so we should show the level for colourblind people - RegisterCustomSink(consoleSink); -} - -ON_DLL_LOAD_CLIENT_RELIESON("client.dll", SourceConsole, ConCommand, (CModule module)) -{ - g_pSourceGameConsole = new SourceInterface("client.dll", "GameConsole004"); - - RegisterConCommand("toggleconsole", ConCommand_toggleconsole, "Show/hide the console.", FCVAR_DONTRECORD); - RegisterConCommand("showconsole", ConCommand_showconsole, "Show the console.", FCVAR_DONTRECORD); - RegisterConCommand("hideconsole", ConCommand_hideconsole, "Hide the console.", FCVAR_DONTRECORD); -} diff --git a/NorthstarDLL/sourceconsole.h b/NorthstarDLL/sourceconsole.h deleted file mode 100644 index b22ef2d3..00000000 --- a/NorthstarDLL/sourceconsole.h +++ /dev/null @@ -1,86 +0,0 @@ -#pragma once -#include "pch.h" -#include "sourceinterface.h" -#include "spdlog/sinks/base_sink.h" -#include - -class EditablePanel -{ - public: - virtual ~EditablePanel() = 0; - unsigned char unknown[0x2B0]; -}; - -class IConsoleDisplayFunc -{ - public: - virtual void ColorPrint(const SourceColor& clr, const char* pMessage) = 0; - virtual void Print(const char* pMessage) = 0; - virtual void DPrint(const char* pMessage) = 0; -}; - -class CConsolePanel : public EditablePanel, public IConsoleDisplayFunc -{ -}; - -class CConsoleDialog -{ - public: - struct VTable - { - void* unknown[298]; - void (*OnCommandSubmitted)(CConsoleDialog* consoleDialog, const char* pCommand); - }; - - VTable* m_vtable; - unsigned char unknown[0x398]; - CConsolePanel* m_pConsolePanel; -}; - -class CGameConsole -{ - public: - virtual ~CGameConsole() = 0; - - // activates the console, makes it visible and brings it to the foreground - virtual void Activate() = 0; - - virtual void Initialize() = 0; - - // hides the console - virtual void Hide() = 0; - - // clears the console - virtual void Clear() = 0; - - // return true if the console has focus - virtual bool IsConsoleVisible() = 0; - - virtual void SetParent(int parent) = 0; - - bool m_bInitialized; - CConsoleDialog* m_pConsole; -}; - -extern SourceInterface* g_pSourceGameConsole; - -// spdlog logger -class SourceConsoleSink : public CustomSink -{ - private: - std::map m_LogColours = { - {spdlog::level::trace, NS::Colors::TRACE.ToSourceColor()}, - {spdlog::level::debug, NS::Colors::DEBUG.ToSourceColor()}, - {spdlog::level::info, NS::Colors::INFO.ToSourceColor()}, - {spdlog::level::warn, NS::Colors::WARN.ToSourceColor()}, - {spdlog::level::err, NS::Colors::ERR.ToSourceColor()}, - {spdlog::level::critical, NS::Colors::CRIT.ToSourceColor()}, - {spdlog::level::off, NS::Colors::OFF.ToSourceColor()}}; - - protected: - void custom_sink_it_(const custom_log_msg& msg); - void sink_it_(const spdlog::details::log_msg& msg) override; - void flush_() override; -}; - -void InitialiseConsoleOnInterfaceCreation(); diff --git a/NorthstarDLL/sourceinterface.cpp b/NorthstarDLL/sourceinterface.cpp deleted file mode 100644 index d5f7b7cd..00000000 --- a/NorthstarDLL/sourceinterface.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include "pch.h" -#include "sourceinterface.h" -#include "sourceconsole.h" - -AUTOHOOK_INIT() - -// really wanted to do a modular callback system here but honestly couldn't be bothered so hardcoding stuff for now: todo later - -// clang-format off -AUTOHOOK_PROCADDRESS(ClientCreateInterface, client.dll, CreateInterface, -void*, __fastcall, (const char* pName, const int* pReturnCode)) -// clang-format on -{ - void* ret = ClientCreateInterface(pName, pReturnCode); - spdlog::info("CreateInterface CLIENT {}", pName); - - if (!strcmp(pName, "GameClientExports001")) - InitialiseConsoleOnInterfaceCreation(); - - return ret; -} - -// clang-format off -AUTOHOOK_PROCADDRESS(ServerCreateInterface, server.dll, CreateInterface, -void*, __fastcall, (const char* pName, const int* pReturnCode)) -// clang-format on -{ - void* ret = ServerCreateInterface(pName, pReturnCode); - spdlog::info("CreateInterface SERVER {}", pName); - - return ret; -} - -// clang-format off -AUTOHOOK_PROCADDRESS(EngineCreateInterface, engine.dll, CreateInterface, -void*, __fastcall, (const char* pName, const int* pReturnCode)) -// clang-format on -{ - void* ret = EngineCreateInterface(pName, pReturnCode); - spdlog::info("CreateInterface ENGINE {}", pName); - - return ret; -} - -// clang-format off -ON_DLL_LOAD("client.dll", ClientInterface, (CModule module)) {AUTOHOOK_DISPATCH_MODULE(client.dll)} -ON_DLL_LOAD("server.dll", ServerInterface, (CModule module)) {AUTOHOOK_DISPATCH_MODULE(server.dll)} -ON_DLL_LOAD("engine.dll", EngineInterface, (CModule module)) {AUTOHOOK_DISPATCH_MODULE(engine.dll)} -// clang-format on diff --git a/NorthstarDLL/sourceinterface.h b/NorthstarDLL/sourceinterface.h deleted file mode 100644 index 474e961b..00000000 --- a/NorthstarDLL/sourceinterface.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once -#include - -// literally just copied from ttf2sdk definition -typedef void* (*CreateInterfaceFn)(const char* pName, int* pReturnCode); - -template class SourceInterface -{ - private: - T* m_interface; - - public: - SourceInterface(const std::string& moduleName, const std::string& interfaceName) - { - HMODULE handle = GetModuleHandleA(moduleName.c_str()); - CreateInterfaceFn createInterface = (CreateInterfaceFn)GetProcAddress(handle, "CreateInterface"); - m_interface = (T*)createInterface(interfaceName.c_str(), NULL); - if (m_interface == nullptr) - spdlog::error("Failed to call CreateInterface for %s in %s", interfaceName, moduleName); - } - - T* operator->() const - { - return m_interface; - } - - operator T*() const - { - return m_interface; - } -}; diff --git a/NorthstarDLL/squirrel.cpp b/NorthstarDLL/squirrel.cpp deleted file mode 100644 index 3d62ac62..00000000 --- a/NorthstarDLL/squirrel.cpp +++ /dev/null @@ -1,739 +0,0 @@ -#include "pch.h" -#include "squirrel.h" -#include "concommand.h" -#include "modmanager.h" -#include "dedicated.h" -#include "r2engine.h" -#include "tier0.h" - -#include - -AUTOHOOK_INIT() - -std::shared_ptr getSquirrelLoggerByContext(ScriptContext context) -{ - switch (context) - { - case ScriptContext::UI: - return NS::log::SCRIPT_UI; - case ScriptContext::CLIENT: - return NS::log::SCRIPT_CL; - case ScriptContext::SERVER: - return NS::log::SCRIPT_SV; - default: - throw std::runtime_error("getSquirrelLoggerByContext called with invalid context"); - return nullptr; - } -} - -namespace NS::log -{ - template std::shared_ptr squirrel_logger() - { - // Switch statements can't be constexpr afaik - // clang-format off - if constexpr (context == ScriptContext::UI) { return SCRIPT_UI; } - if constexpr (context == ScriptContext::CLIENT) { return SCRIPT_CL; } - if constexpr (context == ScriptContext::SERVER) { return SCRIPT_SV; } - // clang-format on - } -}; // namespace NS::log - -const char* GetContextName(ScriptContext context) -{ - switch (context) - { - case ScriptContext::CLIENT: - return "CLIENT"; - case ScriptContext::SERVER: - return "SERVER"; - case ScriptContext::UI: - return "UI"; - default: - return "UNKNOWN"; - } -} - -const char* GetContextName_Short(ScriptContext context) -{ - switch (context) - { - case ScriptContext::CLIENT: - return "CL"; - case ScriptContext::SERVER: - return "SV"; - case ScriptContext::UI: - return "UI"; - default: - return "??"; - } -} - -eSQReturnType SQReturnTypeFromString(const char* pReturnType) -{ - static const std::map sqReturnTypeNameToString = { - {"bool", eSQReturnType::Boolean}, - {"float", eSQReturnType::Float}, - {"vector", eSQReturnType::Vector}, - {"int", eSQReturnType::Integer}, - {"entity", eSQReturnType::Entity}, - {"string", eSQReturnType::String}, - {"array", eSQReturnType::Arrays}, - {"asset", eSQReturnType::Asset}, - {"table", eSQReturnType::Table}}; - - if (sqReturnTypeNameToString.find(pReturnType) != sqReturnTypeNameToString.end()) - return sqReturnTypeNameToString.at(pReturnType); - else - return eSQReturnType::Default; // previous default value -} - -const char* SQTypeNameFromID(int type) -{ - switch (type) - { - case OT_ASSET: - return "asset"; - case OT_INTEGER: - return "int"; - case OT_BOOL: - return "bool"; - case SQOBJECT_NUMERIC: - return "float or int"; - case OT_NULL: - return "null"; - case OT_VECTOR: - return "vector"; - case 0: - return "var"; - case OT_USERDATA: - return "userdata"; - case OT_FLOAT: - return "float"; - case OT_STRING: - return "string"; - case OT_ARRAY: - return "array"; - case 0x8000200: - return "function"; - case 0x8100000: - return "structdef"; - case OT_THREAD: - return "thread"; - case OT_FUNCPROTO: - return "function"; - case OT_CLAAS: - return "class"; - case OT_WEAKREF: - return "weakref"; - case 0x8080000: - return "unimplemented function"; - case 0x8200000: - return "struct instance"; - case OT_TABLE: - return "table"; - case 0xA008000: - return "instance"; - case OT_ENTITY: - return "entity"; - } - return ""; -} - -// Allows for generating squirrelmessages from plugins. -// Not used in this version, but will be used later -void AsyncCall_External(ScriptContext context, const char* func_name, SquirrelMessage_External_Pop function) -{ - SquirrelMessage message {}; - message.functionName = func_name; - message.isExternal = true; - message.externalFunc = function; - switch (context) - { - case ScriptContext::CLIENT: - g_pSquirrel->messageBuffer->push(message); - case ScriptContext::SERVER: - g_pSquirrel->messageBuffer->push(message); - case ScriptContext::UI: - g_pSquirrel->messageBuffer->push(message); - } -} - -// needed to define implementations for squirrelmanager outside of squirrel.h without compiler errors -template class SquirrelManager; -template class SquirrelManager; -template class SquirrelManager; - -template void SquirrelManager::VMCreated(CSquirrelVM* newSqvm) -{ - m_pSQVM = newSqvm; - - for (SQFuncRegistration* funcReg : m_funcRegistrations) - { - spdlog::info("Registering {} function {}", GetContextName(context), funcReg->squirrelFuncName); - RegisterSquirrelFunc(m_pSQVM, funcReg, 1); - } - - for (auto& pair : g_pModManager->m_DependencyConstants) - { - bool bWasFound = false; - - for (Mod& dependency : g_pModManager->m_LoadedMods) - { - if (!dependency.m_bEnabled) - continue; - - if (dependency.Name == pair.second) - { - bWasFound = true; - break; - } - } - - defconst(m_pSQVM, pair.first.c_str(), bWasFound); - } - g_pSquirrel->messageBuffer = new SquirrelMessageBuffer(); -} - -template void SquirrelManager::VMDestroyed() -{ - m_pSQVM = nullptr; -} - -template void SquirrelManager::ExecuteCode(const char* pCode) -{ - if (!m_pSQVM || !m_pSQVM->sqvm) - { - spdlog::error("Cannot execute code, {} squirrel vm is not initialised", GetContextName(context)); - return; - } - - spdlog::info("Executing {} script code {} ", GetContextName(context), pCode); - - std::string strCode(pCode); - CompileBufferState bufferState = CompileBufferState(strCode); - - SQRESULT compileResult = compilebuffer(&bufferState, "console"); - spdlog::info("sq_compilebuffer returned {}", PrintSQRESULT.at(compileResult)); - - if (compileResult != SQRESULT_ERROR) - { - pushroottable(m_pSQVM->sqvm); - SQRESULT callResult = _call(m_pSQVM->sqvm, 0); - spdlog::info("sq_call returned {}", PrintSQRESULT.at(callResult)); - } -} - -template void SquirrelManager::AddFuncRegistration( - std::string returnType, std::string name, std::string argTypes, std::string helpText, SQFunction func) -{ - SQFuncRegistration* reg = new SQFuncRegistration; - - reg->squirrelFuncName = new char[name.size() + 1]; - strcpy((char*)reg->squirrelFuncName, name.c_str()); - reg->cppFuncName = reg->squirrelFuncName; - - reg->helpText = new char[helpText.size() + 1]; - strcpy((char*)reg->helpText, helpText.c_str()); - - reg->returnTypeString = new char[returnType.size() + 1]; - strcpy((char*)reg->returnTypeString, returnType.c_str()); - reg->returnType = SQReturnTypeFromString(returnType.c_str()); - - reg->argTypes = new char[argTypes.size() + 1]; - strcpy((char*)reg->argTypes, argTypes.c_str()); - - reg->funcPtr = func; - - m_funcRegistrations.push_back(reg); -} - -template SQRESULT SquirrelManager::setupfunc(const SQChar* funcname) -{ - pushroottable(m_pSQVM->sqvm); - pushstring(m_pSQVM->sqvm, funcname, -1); - - SQRESULT result = get(m_pSQVM->sqvm, -2); - if (result != SQRESULT_ERROR) - pushroottable(m_pSQVM->sqvm); - - return result; -} - -template void SquirrelManager::AddFuncOverride(std::string name, SQFunction func) -{ - m_funcOverrides[name] = func; -} - -// hooks -bool IsUIVM(ScriptContext context, HSquirrelVM* pSqvm) -{ - return context != ScriptContext::SERVER && g_pSquirrel->m_pSQVM && - g_pSquirrel->m_pSQVM->sqvm == pSqvm; -} - -template void* (*__fastcall sq_compiler_create)(HSquirrelVM* sqvm, void* a2, void* a3, SQBool bShouldThrowError); -template void* __fastcall sq_compiler_createHook(HSquirrelVM* sqvm, void* a2, void* a3, SQBool bShouldThrowError) -{ - // store whether errors generated from this compile should be fatal - if (IsUIVM(context, sqvm)) - g_pSquirrel->m_bFatalCompilationErrors = bShouldThrowError; - else - g_pSquirrel->m_bFatalCompilationErrors = bShouldThrowError; - - return sq_compiler_create(sqvm, a2, a3, bShouldThrowError); -} - -template SQInteger (*SQPrint)(HSquirrelVM* sqvm, const char* fmt); -template SQInteger SQPrintHook(HSquirrelVM* sqvm, const char* fmt, ...) -{ - va_list va; - va_start(va, fmt); - - SQChar buf[1024]; - int charsWritten = vsnprintf_s(buf, _TRUNCATE, fmt, va); - - if (charsWritten > 0) - { - if (buf[charsWritten - 1] == '\n') - buf[charsWritten - 1] = '\0'; - g_pSquirrel->logger->info("{}", buf); - } - - va_end(va); - return 0; -} - -template CSquirrelVM* (*__fastcall CreateNewVM)(void* a1, ScriptContext realContext); -template CSquirrelVM* __fastcall CreateNewVMHook(void* a1, ScriptContext realContext) -{ - CSquirrelVM* sqvm = CreateNewVM(a1, realContext); - if (realContext == ScriptContext::UI) - g_pSquirrel->VMCreated(sqvm); - else - g_pSquirrel->VMCreated(sqvm); - - spdlog::info("CreateNewVM {} {}", GetContextName(realContext), (void*)sqvm); - return sqvm; -} - -template void (*__fastcall DestroyVM)(void* a1, CSquirrelVM* sqvm); -template void __fastcall DestroyVMHook(void* a1, CSquirrelVM* sqvm) -{ - ScriptContext realContext = context; // ui and client use the same function so we use this for prints - if (IsUIVM(context, sqvm->sqvm)) - { - realContext = ScriptContext::UI; - g_pSquirrel->VMDestroyed(); - DestroyVM(a1, sqvm); // If we pass UI here it crashes - } - else - { - g_pSquirrel->VMDestroyed(); - DestroyVM(a1, sqvm); - } - - spdlog::info("DestroyVM {} {}", GetContextName(realContext), (void*)sqvm); -} - -template -void (*__fastcall SQCompileError)(HSquirrelVM* sqvm, const char* error, const char* file, int line, int column); -template -void __fastcall ScriptCompileErrorHook(HSquirrelVM* sqvm, const char* error, const char* file, int line, int column) -{ - bool bIsFatalError = g_pSquirrel->m_bFatalCompilationErrors; - ScriptContext realContext = context; // ui and client use the same function so we use this for prints - if (IsUIVM(context, sqvm)) - { - realContext = ScriptContext::UI; - bIsFatalError = g_pSquirrel->m_bFatalCompilationErrors; - } - - auto logger = getSquirrelLoggerByContext(realContext); - - logger->error("COMPILE ERROR {}", error); - logger->error("{} line [{}] column [{}]", file, line, column); - - // use disconnect to display an error message for the compile error, but only if the compilation error was fatal - // todo, we could get this from sqvm itself probably, rather than hooking sq_compiler_create - if (bIsFatalError) - { - // kill dedicated server if we hit this - if (IsDedicatedServer()) - { - // flush the logger before we exit so debug things get saved to log file - logger->flush(); - exit(EXIT_FAILURE); - } - else - { - R2::Cbuf_AddText( - R2::Cbuf_GetCurrentPlayer(), - fmt::format("disconnect \"Encountered {} script compilation error, see console for details.\"", GetContextName(realContext)) - .c_str(), - R2::cmd_source_t::kCommandSrcCode); - - // likely temp: show console so user can see any errors, as error message wont display if ui is dead - // maybe we could disable all mods other than the coremods and try a reload before doing this? - // could also maybe do some vgui bullshit to show something visually rather than console - if (realContext == ScriptContext::UI) - R2::Cbuf_AddText(R2::Cbuf_GetCurrentPlayer(), "showconsole", R2::cmd_source_t::kCommandSrcCode); - } - } - - // dont call the original function since it kills game lol -} - -template -int64_t (*__fastcall RegisterSquirrelFunction)(CSquirrelVM* sqvm, SQFuncRegistration* funcReg, char unknown); -template -int64_t __fastcall RegisterSquirrelFunctionHook(CSquirrelVM* sqvm, SQFuncRegistration* funcReg, char unknown) -{ - if (IsUIVM(context, sqvm->sqvm)) - { - if (g_pSquirrel->m_funcOverrides.count(funcReg->squirrelFuncName)) - { - g_pSquirrel->m_funcOriginals[funcReg->squirrelFuncName] = funcReg->funcPtr; - funcReg->funcPtr = g_pSquirrel->m_funcOverrides[funcReg->squirrelFuncName]; - spdlog::info("Replacing {} in UI", std::string(funcReg->squirrelFuncName)); - } - - return g_pSquirrel->RegisterSquirrelFunc(sqvm, funcReg, unknown); - } - - if (g_pSquirrel->m_funcOverrides.find(funcReg->squirrelFuncName) != g_pSquirrel->m_funcOverrides.end()) - { - g_pSquirrel->m_funcOriginals[funcReg->squirrelFuncName] = funcReg->funcPtr; - funcReg->funcPtr = g_pSquirrel->m_funcOverrides[funcReg->squirrelFuncName]; - spdlog::info("Replacing {} in Client", std::string(funcReg->squirrelFuncName)); - } - - return g_pSquirrel->RegisterSquirrelFunc(sqvm, funcReg, unknown); -} - -template bool (*__fastcall CallScriptInitCallback)(void* sqvm, const char* callback); -template bool __fastcall CallScriptInitCallbackHook(void* sqvm, const char* callback) -{ - ScriptContext realContext = context; - bool bShouldCallCustomCallbacks = true; - - if (context == ScriptContext::CLIENT) - { - if (!strcmp(callback, "UICodeCallback_UIInit")) - realContext = ScriptContext::UI; - else if (strcmp(callback, "ClientCodeCallback_MapSpawn")) - bShouldCallCustomCallbacks = false; - } - else if (context == ScriptContext::SERVER) - bShouldCallCustomCallbacks = !strcmp(callback, "CodeCallback_MapSpawn"); - - if (bShouldCallCustomCallbacks) - { - for (Mod mod : g_pModManager->m_LoadedMods) - { - if (!mod.m_bEnabled) - continue; - - for (ModScript script : mod.Scripts) - { - for (ModScriptCallback modCallback : script.Callbacks) - { - if (modCallback.Context == realContext && modCallback.BeforeCallback.length()) - { - spdlog::info("Running custom {} script callback \"{}\"", GetContextName(realContext), modCallback.BeforeCallback); - CallScriptInitCallback(sqvm, modCallback.BeforeCallback.c_str()); - } - } - } - } - } - - spdlog::info("{} CodeCallback {} called", GetContextName(realContext), callback); - if (!bShouldCallCustomCallbacks) - spdlog::info("Not executing custom callbacks for CodeCallback {}", callback); - bool ret = CallScriptInitCallback(sqvm, callback); - - // run after callbacks - if (bShouldCallCustomCallbacks) - { - for (Mod mod : g_pModManager->m_LoadedMods) - { - if (!mod.m_bEnabled) - continue; - - for (ModScript script : mod.Scripts) - { - for (ModScriptCallback modCallback : script.Callbacks) - { - if (modCallback.Context == realContext && modCallback.AfterCallback.length()) - { - spdlog::info("Running custom {} script callback \"{}\"", GetContextName(realContext), modCallback.AfterCallback); - CallScriptInitCallback(sqvm, modCallback.AfterCallback.c_str()); - } - } - } - } - } - - return ret; -} - -template void ConCommand_script(const CCommand& args) -{ - g_pSquirrel->ExecuteCode(args.ArgS()); -} - -// literal class type that wraps a constant expression string -template struct TemplateStringLiteral -{ - constexpr TemplateStringLiteral(const char (&str)[N]) - { - std::copy_n(str, N, value); - } - - char value[N]; -}; - -template SQRESULT SQ_StubbedFunc(HSquirrelVM* sqvm) -{ - spdlog::info("Blocking call to stubbed function {} in {}", funcName.value, GetContextName(context)); - return SQRESULT_NULL; -} - -template void StubUnsafeSQFuncs() -{ - if (!Tier0::CommandLine()->CheckParm("-allowunsafesqfuncs")) - { - g_pSquirrel->AddFuncOverride("DevTextBufferWrite", SQ_StubbedFunc); - g_pSquirrel->AddFuncOverride("DevTextBufferClear", SQ_StubbedFunc); - g_pSquirrel->AddFuncOverride("DevTextBufferDumpToFile", SQ_StubbedFunc); - g_pSquirrel->AddFuncOverride("Dev_CommandLineAddParam", SQ_StubbedFunc); - g_pSquirrel->AddFuncOverride("DevP4Checkout", SQ_StubbedFunc); - g_pSquirrel->AddFuncOverride("DevP4Add", SQ_StubbedFunc); - } -} - -template void SquirrelManager::ProcessMessageBuffer() -{ - auto maybeMessage = messageBuffer->pop(); - if (!maybeMessage) - { - return; - } - - SquirrelMessage message = maybeMessage.value(); - - SQObject functionobj {}; - int result = sq_getfunction(m_pSQVM->sqvm, message.functionName.c_str(), &functionobj, 0); - if (result != 0) // This func returns 0 on success for some reason - { - NS::log::squirrel_logger()->error( - "ProcessMessageBuffer was unable to find function with name '{}'. Is it global?", message.functionName); - return; - } - pushobject(m_pSQVM->sqvm, &functionobj); // Push the function object - pushroottable(m_pSQVM->sqvm); - if (message.isExternal) - { - message.externalFunc(m_pSQVM->sqvm); - } - else - { - for (auto& v : message.args) - { - // Execute lambda to push arg to stack - v(); - } - } - - _call(m_pSQVM->sqvm, message.args.size()); -} - -ON_DLL_LOAD_RELIESON("client.dll", ClientSquirrel, ConCommand, (CModule module)) -{ - AUTOHOOK_DISPATCH_MODULE(client.dll) - - g_pSquirrel->__sq_defconst = module.Offset(0x12120).As(); - g_pSquirrel->__sq_defconst = g_pSquirrel->__sq_defconst; - - g_pSquirrel->__sq_compilebuffer = module.Offset(0x3110).As(); - g_pSquirrel->__sq_pushroottable = module.Offset(0x5860).As(); - g_pSquirrel->__sq_compilebuffer = g_pSquirrel->__sq_compilebuffer; - g_pSquirrel->__sq_pushroottable = g_pSquirrel->__sq_pushroottable; - - g_pSquirrel->__sq_call = module.Offset(0x8650).As(); - g_pSquirrel->__sq_call = g_pSquirrel->__sq_call; - - g_pSquirrel->__sq_newarray = module.Offset(0x39F0).As(); - g_pSquirrel->__sq_arrayappend = module.Offset(0x3C70).As(); - g_pSquirrel->__sq_newarray = g_pSquirrel->__sq_newarray; - g_pSquirrel->__sq_arrayappend = g_pSquirrel->__sq_arrayappend; - - g_pSquirrel->__sq_newtable = module.Offset(0x3960).As(); - g_pSquirrel->__sq_newslot = module.Offset(0x70B0).As(); - g_pSquirrel->__sq_newtable = g_pSquirrel->__sq_newtable; - g_pSquirrel->__sq_newslot = g_pSquirrel->__sq_newslot; - - g_pSquirrel->__sq_pushstring = module.Offset(0x3440).As(); - g_pSquirrel->__sq_pushinteger = module.Offset(0x36A0).As(); - g_pSquirrel->__sq_pushfloat = module.Offset(0x3800).As(); - g_pSquirrel->__sq_pushbool = module.Offset(0x3710).As(); - g_pSquirrel->__sq_pushasset = module.Offset(0x3560).As(); - g_pSquirrel->__sq_pushvector = module.Offset(0x3780).As(); - g_pSquirrel->__sq_pushobject = module.Offset(0x83D0).As(); - g_pSquirrel->__sq_raiseerror = module.Offset(0x8470).As(); - g_pSquirrel->__sq_pushstring = g_pSquirrel->__sq_pushstring; - g_pSquirrel->__sq_pushinteger = g_pSquirrel->__sq_pushinteger; - g_pSquirrel->__sq_pushfloat = g_pSquirrel->__sq_pushfloat; - g_pSquirrel->__sq_pushbool = g_pSquirrel->__sq_pushbool; - g_pSquirrel->__sq_pushvector = g_pSquirrel->__sq_pushvector; - g_pSquirrel->__sq_pushasset = g_pSquirrel->__sq_pushasset; - g_pSquirrel->__sq_pushobject = g_pSquirrel->__sq_pushobject; - g_pSquirrel->__sq_raiseerror = g_pSquirrel->__sq_raiseerror; - - g_pSquirrel->__sq_getstring = module.Offset(0x60C0).As(); - g_pSquirrel->__sq_getinteger = module.Offset(0x60E0).As(); - g_pSquirrel->__sq_getfloat = module.Offset(0x6100).As(); - g_pSquirrel->__sq_getbool = module.Offset(0x6130).As(); - g_pSquirrel->__sq_get = module.Offset(0x7C30).As(); - g_pSquirrel->__sq_getasset = module.Offset(0x6010).As(); - g_pSquirrel->__sq_getuserdata = module.Offset(0x63D0).As(); - g_pSquirrel->__sq_getvector = module.Offset(0x6140).As(); - g_pSquirrel->__sq_getstring = g_pSquirrel->__sq_getstring; - g_pSquirrel->__sq_getinteger = g_pSquirrel->__sq_getinteger; - g_pSquirrel->__sq_getfloat = g_pSquirrel->__sq_getfloat; - g_pSquirrel->__sq_getbool = g_pSquirrel->__sq_getbool; - g_pSquirrel->__sq_get = g_pSquirrel->__sq_get; - g_pSquirrel->__sq_getasset = g_pSquirrel->__sq_getasset; - g_pSquirrel->__sq_getuserdata = g_pSquirrel->__sq_getuserdata; - g_pSquirrel->__sq_getvector = g_pSquirrel->__sq_getvector; - g_pSquirrel->__sq_getthisentity = g_pSquirrel->__sq_getthisentity; - g_pSquirrel->__sq_getobject = g_pSquirrel->__sq_getobject; - - g_pSquirrel->__sq_createuserdata = module.Offset(0x38D0).As(); - g_pSquirrel->__sq_setuserdatatypeid = module.Offset(0x6490).As(); - g_pSquirrel->__sq_createuserdata = g_pSquirrel->__sq_createuserdata; - g_pSquirrel->__sq_setuserdatatypeid = g_pSquirrel->__sq_setuserdatatypeid; - - g_pSquirrel->__sq_GetEntityConstant_CBaseEntity = module.Offset(0x3E49B0).As(); - g_pSquirrel->__sq_getentityfrominstance = module.Offset(0x114F0).As(); - g_pSquirrel->__sq_GetEntityConstant_CBaseEntity = - g_pSquirrel->__sq_GetEntityConstant_CBaseEntity; - g_pSquirrel->__sq_getentityfrominstance = g_pSquirrel->__sq_getentityfrominstance; - - // Message buffer stuff - g_pSquirrel->messageBuffer = g_pSquirrel->messageBuffer; - g_pSquirrel->__sq_getfunction = module.Offset(0x572FB0).As(); - g_pSquirrel->__sq_getfunction = g_pSquirrel->__sq_getfunction; - - MAKEHOOK( - module.Offset(0x108E0), - &RegisterSquirrelFunctionHook, - &g_pSquirrel->RegisterSquirrelFunc); - g_pSquirrel->RegisterSquirrelFunc = g_pSquirrel->RegisterSquirrelFunc; - - g_pSquirrel->logger = NS::log::SCRIPT_CL; - g_pSquirrel->logger = NS::log::SCRIPT_UI; - - // uiscript_reset concommand: don't loop forever if compilation fails - module.Offset(0x3C6E4C).NOP(6); - - MAKEHOOK(module.Offset(0x8AD0), &sq_compiler_createHook, &sq_compiler_create); - - MAKEHOOK(module.Offset(0x12B00), &SQPrintHook, &SQPrint); - MAKEHOOK(module.Offset(0x12BA0), &SQPrintHook, &SQPrint); - - MAKEHOOK(module.Offset(0x26130), &CreateNewVMHook, &CreateNewVM); - MAKEHOOK(module.Offset(0x26E70), &DestroyVMHook, &DestroyVM); - MAKEHOOK(module.Offset(0x79A50), &ScriptCompileErrorHook, &SQCompileError); - - MAKEHOOK(module.Offset(0x10190), &CallScriptInitCallbackHook, &CallScriptInitCallback); - - RegisterConCommand("script_client", ConCommand_script, "Executes script code on the client vm", FCVAR_CLIENTDLL); - RegisterConCommand("script_ui", ConCommand_script, "Executes script code on the ui vm", FCVAR_CLIENTDLL); - - StubUnsafeSQFuncs(); - StubUnsafeSQFuncs(); - - g_pSquirrel->__sq_getfunction = module.Offset(0x6CB0).As(); - g_pSquirrel->__sq_getfunction = g_pSquirrel->__sq_getfunction; -} - -ON_DLL_LOAD_RELIESON("server.dll", ServerSquirrel, ConCommand, (CModule module)) -{ - AUTOHOOK_DISPATCH_MODULE(server.dll) - - g_pSquirrel->__sq_defconst = module.Offset(0x1F550).As(); - - g_pSquirrel->__sq_compilebuffer = module.Offset(0x3110).As(); - g_pSquirrel->__sq_pushroottable = module.Offset(0x5840).As(); - g_pSquirrel->__sq_call = module.Offset(0x8620).As(); - - g_pSquirrel->__sq_newarray = module.Offset(0x39F0).As(); - g_pSquirrel->__sq_arrayappend = module.Offset(0x3C70).As(); - - g_pSquirrel->__sq_newtable = module.Offset(0x3960).As(); - g_pSquirrel->__sq_newslot = module.Offset(0x7080).As(); - - g_pSquirrel->__sq_pushstring = module.Offset(0x3440).As(); - g_pSquirrel->__sq_pushinteger = module.Offset(0x36A0).As(); - g_pSquirrel->__sq_pushfloat = module.Offset(0x3800).As(); - g_pSquirrel->__sq_pushbool = module.Offset(0x3710).As(); - g_pSquirrel->__sq_pushasset = module.Offset(0x3560).As(); - g_pSquirrel->__sq_pushvector = module.Offset(0x3780).As(); - g_pSquirrel->__sq_pushobject = module.Offset(0x83A0).As(); - - g_pSquirrel->__sq_raiseerror = module.Offset(0x8440).As(); - - g_pSquirrel->__sq_getstring = module.Offset(0x60A0).As(); - g_pSquirrel->__sq_getinteger = module.Offset(0x60C0).As(); - g_pSquirrel->__sq_getfloat = module.Offset(0x60E0).As(); - g_pSquirrel->__sq_getbool = module.Offset(0x6110).As(); - g_pSquirrel->__sq_getasset = module.Offset(0x5FF0).As(); - g_pSquirrel->__sq_getuserdata = module.Offset(0x63B0).As(); - g_pSquirrel->__sq_getvector = module.Offset(0x6120).As(); - g_pSquirrel->__sq_get = module.Offset(0x7C00).As(); - - g_pSquirrel->__sq_getthisentity = module.Offset(0x203B0).As(); - g_pSquirrel->__sq_getobject = module.Offset(0x6140).As(); - - g_pSquirrel->__sq_createuserdata = module.Offset(0x38D0).As(); - g_pSquirrel->__sq_setuserdatatypeid = module.Offset(0x6470).As(); - - g_pSquirrel->__sq_GetEntityConstant_CBaseEntity = module.Offset(0x418AF0).As(); - g_pSquirrel->__sq_getentityfrominstance = module.Offset(0x1E920).As(); - - g_pSquirrel->logger = NS::log::SCRIPT_SV; - // Message buffer stuff - g_pSquirrel->__sq_getfunction = module.Offset(0x6C85).As(); - - MAKEHOOK( - module.Offset(0x1DD10), - &RegisterSquirrelFunctionHook, - &g_pSquirrel->RegisterSquirrelFunc); - - MAKEHOOK(module.Offset(0x8AA0), &sq_compiler_createHook, &sq_compiler_create); - - MAKEHOOK(module.Offset(0x1FE90), &SQPrintHook, &SQPrint); - MAKEHOOK(module.Offset(0x260E0), &CreateNewVMHook, &CreateNewVM); - MAKEHOOK(module.Offset(0x26E20), &DestroyVMHook, &DestroyVM); - MAKEHOOK(module.Offset(0x799E0), &ScriptCompileErrorHook, &SQCompileError); - MAKEHOOK(module.Offset(0x1D5C0), &CallScriptInitCallbackHook, &CallScriptInitCallback); - - // FCVAR_CHEAT and FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS allows clients to execute this, but since it's unsafe we only allow it when cheats - // are enabled for script_client and script_ui, we don't use cheats, so clients can execute them on themselves all they want - RegisterConCommand( - "script", - ConCommand_script, - "Executes script code on the server vm", - FCVAR_GAMEDLL | FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS | FCVAR_CHEAT); - - StubUnsafeSQFuncs(); -} - -void InitialiseSquirrelManagers() -{ - g_pSquirrel = new SquirrelManager; - g_pSquirrel = new SquirrelManager; - g_pSquirrel = new SquirrelManager; -} diff --git a/NorthstarDLL/squirrel.h b/NorthstarDLL/squirrel.h deleted file mode 100644 index a1f1dd5f..00000000 --- a/NorthstarDLL/squirrel.h +++ /dev/null @@ -1,469 +0,0 @@ -#pragma once - -#include "squirrelclasstypes.h" -#include "squirrelautobind.h" -#include "vector.h" -#include "logging.h" - -// stolen from ttf2sdk: sqvm types -typedef float SQFloat; -typedef long SQInteger; -typedef unsigned long SQUnsignedInteger; -typedef char SQChar; -typedef SQUnsignedInteger SQBool; - -static constexpr int operator&(ScriptContext first, ScriptContext second) -{ - return first == second; -} - -static constexpr int operator&(int first, ScriptContext second) -{ - return first & (1 << static_cast(second)); -} - -static constexpr int operator|(ScriptContext first, ScriptContext second) -{ - return (1 << static_cast(first)) + (1 << static_cast(second)); -} - -static constexpr int operator|(int first, ScriptContext second) -{ - return first + (1 << static_cast(second)); -} - -const char* GetContextName(ScriptContext context); -const char* GetContextName_Short(ScriptContext context); -eSQReturnType SQReturnTypeFromString(const char* pReturnType); -const char* SQTypeNameFromID(const int iTypeId); - -std::shared_ptr getSquirrelLoggerByContext(ScriptContext context); - -namespace NS::log -{ - template std::shared_ptr squirrel_logger(); -}; // namespace NS::log - -void schedule_call_external(ScriptContext context, const char* func_name, SquirrelMessage_External_Pop function); - -// This base class means that only the templated functions have to be rebuilt for each template instance -// Cuts down on compile time by ~5 seconds -class SquirrelManagerBase -{ - protected: - std::vector m_funcRegistrations; - - public: - CSquirrelVM* m_pSQVM; - std::map m_funcOverrides = {}; - std::map m_funcOriginals = {}; - - bool m_bFatalCompilationErrors = false; - - std::shared_ptr logger; - -#pragma region SQVM funcs - RegisterSquirrelFuncType RegisterSquirrelFunc; - sq_defconstType __sq_defconst; - - sq_compilebufferType __sq_compilebuffer; - sq_callType __sq_call; - sq_raiseerrorType __sq_raiseerror; - - sq_newarrayType __sq_newarray; - sq_arrayappendType __sq_arrayappend; - - sq_newtableType __sq_newtable; - sq_newslotType __sq_newslot; - - sq_pushroottableType __sq_pushroottable; - sq_pushstringType __sq_pushstring; - sq_pushintegerType __sq_pushinteger; - sq_pushfloatType __sq_pushfloat; - sq_pushboolType __sq_pushbool; - sq_pushassetType __sq_pushasset; - sq_pushvectorType __sq_pushvector; - sq_pushobjectType __sq_pushobject; - - sq_getstringType __sq_getstring; - sq_getintegerType __sq_getinteger; - sq_getfloatType __sq_getfloat; - sq_getboolType __sq_getbool; - sq_getType __sq_get; - sq_getassetType __sq_getasset; - sq_getuserdataType __sq_getuserdata; - sq_getvectorType __sq_getvector; - sq_getthisentityType __sq_getthisentity; - sq_getobjectType __sq_getobject; - - sq_createuserdataType __sq_createuserdata; - sq_setuserdatatypeidType __sq_setuserdatatypeid; - sq_getfunctionType __sq_getfunction; - - sq_getentityfrominstanceType __sq_getentityfrominstance; - sq_GetEntityConstantType __sq_GetEntityConstant_CBaseEntity; - -#pragma endregion - -#pragma region SQVM func wrappers - inline void defconst(CSquirrelVM* sqvm, const SQChar* pName, int nValue) - { - __sq_defconst(sqvm, pName, nValue); - } - - inline SQRESULT - compilebuffer(CompileBufferState* bufferState, const SQChar* bufferName = "unnamedbuffer", const SQBool bShouldThrowError = false) - { - return __sq_compilebuffer(m_pSQVM->sqvm, bufferState, bufferName, -1, bShouldThrowError); - } - - inline SQRESULT _call(HSquirrelVM* sqvm, const SQInteger args) - { - return __sq_call(sqvm, args + 1, false, false); - } - - inline SQInteger raiseerror(HSquirrelVM* sqvm, const const SQChar* sError) - { - return __sq_raiseerror(sqvm, sError); - } - - inline void newarray(HSquirrelVM* sqvm, const SQInteger stackpos = 0) - { - __sq_newarray(sqvm, stackpos); - } - - inline SQRESULT arrayappend(HSquirrelVM* sqvm, const SQInteger stackpos) - { - return __sq_arrayappend(sqvm, stackpos); - } - - inline SQRESULT newtable(HSquirrelVM* sqvm) - { - return __sq_newtable(sqvm); - } - - inline SQRESULT newslot(HSquirrelVM* sqvm, SQInteger idx, SQBool bStatic) - { - return __sq_newslot(sqvm, idx, bStatic); - } - - inline void pushroottable(HSquirrelVM* sqvm) - { - __sq_pushroottable(sqvm); - } - - inline void pushstring(HSquirrelVM* sqvm, const SQChar* sVal, int length = -1) - { - __sq_pushstring(sqvm, sVal, length); - } - - inline void pushinteger(HSquirrelVM* sqvm, const SQInteger iVal) - { - __sq_pushinteger(sqvm, iVal); - } - - inline void pushfloat(HSquirrelVM* sqvm, const SQFloat flVal) - { - __sq_pushfloat(sqvm, flVal); - } - - inline void pushbool(HSquirrelVM* sqvm, const SQBool bVal) - { - __sq_pushbool(sqvm, bVal); - } - - inline void pushasset(HSquirrelVM* sqvm, const SQChar* sVal, int length = -1) - { - __sq_pushasset(sqvm, sVal, length); - } - - inline void pushvector(HSquirrelVM* sqvm, const Vector3 pVal) - { - __sq_pushvector(sqvm, *(float**)&pVal); - } - - inline void pushobject(HSquirrelVM* sqvm, SQObject* obj) - { - __sq_pushobject(sqvm, obj); - } - - inline const SQChar* getstring(HSquirrelVM* sqvm, const SQInteger stackpos) - { - return __sq_getstring(sqvm, stackpos); - } - - inline SQInteger getinteger(HSquirrelVM* sqvm, const SQInteger stackpos) - { - return __sq_getinteger(sqvm, stackpos); - } - - inline SQFloat getfloat(HSquirrelVM* sqvm, const SQInteger stackpos) - { - return __sq_getfloat(sqvm, stackpos); - } - - inline SQBool getbool(HSquirrelVM* sqvm, const SQInteger stackpos) - { - return __sq_getbool(sqvm, stackpos); - } - - inline SQRESULT get(HSquirrelVM* sqvm, const SQInteger stackpos) - { - return __sq_get(sqvm, stackpos); - } - - inline Vector3 getvector(HSquirrelVM* sqvm, const SQInteger stackpos) - { - float* pRet = __sq_getvector(sqvm, stackpos); - return *(Vector3*)&pRet; - } - - inline int sq_getfunction(HSquirrelVM* sqvm, const char* name, SQObject* returnObj, const char* signature) - { - return __sq_getfunction(sqvm, name, returnObj, signature); - } - - inline SQRESULT getasset(HSquirrelVM* sqvm, const SQInteger stackpos, const char** result) - { - return __sq_getasset(sqvm, stackpos, result); - } - - template inline SQRESULT getuserdata(HSquirrelVM* sqvm, const SQInteger stackpos, T* data, uint64_t* typeId) - { - return __sq_getuserdata(sqvm, stackpos, (void**)data, typeId); // this sometimes crashes idk - } - - template inline T* createuserdata(HSquirrelVM* sqvm, SQInteger size) - { - void* ret = __sq_createuserdata(sqvm, size); - memset(ret, 0, size); - return (T*)ret; - } - - inline SQRESULT setuserdatatypeid(HSquirrelVM* sqvm, const SQInteger stackpos, uint64_t typeId) - { - return __sq_setuserdatatypeid(sqvm, stackpos, typeId); - } - - template inline SQBool getthisentity(HSquirrelVM* sqvm, T* ppEntity) - { - return __sq_getentity(sqvm, (void**)ppEntity); - } - - template inline T* getentity(HSquirrelVM* sqvm, SQInteger iStackPos) - { - SQObject obj; - __sq_getobject(sqvm, iStackPos, &obj); - - // there are entity constants for other types, but seemingly CBaseEntity's is the only one needed - return (T*)__sq_getentityfrominstance(m_pSQVM, &obj, __sq_GetEntityConstant_CBaseEntity()); - } -#pragma endregion -}; - -template class SquirrelManager : public virtual SquirrelManagerBase -{ - public: -#pragma region MessageBuffer - SquirrelMessageBuffer* messageBuffer; - - template SquirrelMessage AsyncCall(std::string funcname, Args... args) - { - // This function schedules a call to be executed on the next frame - // This is useful for things like threads and plugins, which do not run on the main thread - FunctionVector functionVector; - SqRecurseArgs(functionVector, args...); - SquirrelMessage message = {funcname, functionVector}; - messageBuffer->push(message); - return message; - } - - SquirrelMessage AsyncCall(std::string funcname) - { - // This function schedules a call to be executed on the next frame - // This is useful for things like threads and plugins, which do not run on the main thread - FunctionVector functionVector = {}; - SquirrelMessage message = {funcname, functionVector}; - messageBuffer->push(message); - return message; - } - - SQRESULT Call(const char* funcname) - { - // Warning! - // This function assumes the squirrel VM is stopped/blocked at the moment of call - // Calling this function while the VM is running is likely to result in a crash due to stack destruction - // If you want to call into squirrel asynchronously, use `AsyncCall` instead - - if (!m_pSQVM || !m_pSQVM->sqvm) - { - spdlog::error( - "{} was called on context {} while VM was not initialized. This will crash", __FUNCTION__, GetContextName(context)); - } - - SQObject functionobj {}; - int result = sq_getfunction(m_pSQVM->sqvm, funcname, &functionobj, 0); - if (result != 0) // This func returns 0 on success for some reason - { - NS::log::squirrel_logger()->error("Call was unable to find function with name '{}'. Is it global?", funcname); - return SQRESULT_ERROR; - } - pushobject(m_pSQVM->sqvm, &functionobj); // Push the function object - pushroottable(m_pSQVM->sqvm); // Push root table - return _call(m_pSQVM->sqvm, 0); - } - - template SQRESULT Call(const char* funcname, Args... args) - { - // Warning! - // This function assumes the squirrel VM is stopped/blocked at the moment of call - // Calling this function while the VM is running is likely to result in a crash due to stack destruction - // If you want to call into squirrel asynchronously, use `schedule_call` instead - if (!m_pSQVM || !m_pSQVM->sqvm) - { - spdlog::error( - "{} was called on context {} while VM was not initialized. This will crash", __FUNCTION__, GetContextName(context)); - } - SQObject functionobj {}; - int result = sq_getfunction(m_pSQVM->sqvm, funcname, &functionobj, 0); - if (result != 0) // This func returns 0 on success for some reason - { - NS::log::squirrel_logger()->error("Call was unable to find function with name '{}'. Is it global?", funcname); - return SQRESULT_ERROR; - } - pushobject(m_pSQVM->sqvm, &functionobj); // Push the function object - pushroottable(m_pSQVM->sqvm); // Push root table - - FunctionVector functionVector; - SqRecurseArgs(functionVector, args...); - - for (auto& v : functionVector) - { - // Execute lambda to push arg to stack - v(); - } - - return _call(m_pSQVM->sqvm, functionVector.size()); - } - -#pragma endregion - - public: - SquirrelManager() - { - m_pSQVM = nullptr; - } - - void VMCreated(CSquirrelVM* newSqvm); - void VMDestroyed(); - void ExecuteCode(const char* code); - void AddFuncRegistration(std::string returnType, std::string name, std::string argTypes, std::string helpText, SQFunction func); - SQRESULT setupfunc(const SQChar* funcname); - void AddFuncOverride(std::string name, SQFunction func); - void ProcessMessageBuffer(); -}; - -template SquirrelManager* g_pSquirrel; - -void InitialiseSquirrelManagers(); - -/* - Beware all ye who enter below. - This place is not a place of honor... no highly esteemed deed is commemorated here... nothing valued is here. - What is here was dangerous and repulsive to us. This message is a warning about danger. -*/ - -#pragma region MessageBuffer templates - -// Clang-formatting makes this whole thing unreadable -// clang-format off - -#ifndef MessageBufferFuncs -#define MessageBufferFuncs -// Bools -template -requires std::convertible_to && (!std::is_floating_point_v) && (!std::convertible_to) && (!std::convertible_to) -inline VoidFunction SQMessageBufferPushArg(T& arg) { - return [arg]{ g_pSquirrel->pushbool(g_pSquirrel->m_pSQVM->sqvm, static_cast(arg)); }; -} -// Vectors -template -inline VoidFunction SQMessageBufferPushArg(Vector3& arg) { - return [arg]{ g_pSquirrel->pushvector(g_pSquirrel->m_pSQVM->sqvm, arg); }; -} -// Vectors -template -inline VoidFunction SQMessageBufferPushArg(SQObject* arg) { - return [arg]{ g_pSquirrel->pushSQObject(g_pSquirrel->m_pSQVM->sqvm, arg); }; -} -// Ints -template -requires std::convertible_to && (!std::is_floating_point_v) -inline VoidFunction SQMessageBufferPushArg(T& arg) { - return [arg]{ g_pSquirrel->pushinteger(g_pSquirrel->m_pSQVM->sqvm, static_cast(arg)); }; -} -// Floats -template -requires std::convertible_to && (std::is_floating_point_v) -inline VoidFunction SQMessageBufferPushArg(T& arg) { - return [arg]{ g_pSquirrel->pushfloat(g_pSquirrel->m_pSQVM->sqvm, static_cast(arg)); }; -} -// Strings -template -requires (std::convertible_to || std::is_constructible_v) -inline VoidFunction SQMessageBufferPushArg(T& arg) { - auto converted = std::string(arg); - return [converted]{ g_pSquirrel->pushstring(g_pSquirrel->m_pSQVM->sqvm, converted.c_str(), converted.length()); }; -} -// Assets -template -inline VoidFunction SQMessageBufferPushArg(SquirrelAsset& arg) { - return [arg]{ g_pSquirrel->pushasset(g_pSquirrel->m_pSQVM->sqvm, arg.path.c_str(), arg.path.length()); }; -} -// Maps -template -requires is_iterable -inline VoidFunction SQMessageBufferPushArg(T& arg) { - FunctionVector localv = {}; - localv.push_back([]{g_pSquirrel->newarray(g_pSquirrel->m_pSQVM->sqvm, 0);}); - - for (const auto& item : arg) { - localv.push_back(SQMessageBufferPushArg(item)); - localv.push_back([]{g_pSquirrel->arrayappend(g_pSquirrel->m_pSQVM->sqvm, -2);}); - } - - return [localv] { for (auto& func : localv) { func(); } }; -} -// Vectors -template -requires is_map -inline VoidFunction SQMessageBufferPushArg(T& map) { - FunctionVector localv = {}; - localv.push_back([]{g_pSquirrel->newtable(g_pSquirrel->m_pSQVM->sqvm);}); - - for (const auto& item : map) { - localv.push_back(SQMessageBufferPushArg(item.first)); - localv.push_back(SQMessageBufferPushArg(item.second)); - localv.push_back([]{g_pSquirrel->newslot(g_pSquirrel->m_pSQVM->sqvm, -3, false);}); - } - - return [localv]{ for (auto& func : localv) { func(); } }; -} - -template -inline void SqRecurseArgs(FunctionVector& v, T& arg) { - v.push_back(SQMessageBufferPushArg(arg)); -} - -// This function is separated from the PushArg function so as to not generate too many template instances -// This is the main function responsible for unrolling the argument pack -template -inline void SqRecurseArgs(FunctionVector& v, T& arg, Args... args) { - v.push_back(SQMessageBufferPushArg(arg)); - SqRecurseArgs(v, args...); -} - -// clang-format on -#endif - -#pragma endregion diff --git a/NorthstarDLL/squirrel/squirrel.cpp b/NorthstarDLL/squirrel/squirrel.cpp new file mode 100644 index 00000000..eb4b6bed --- /dev/null +++ b/NorthstarDLL/squirrel/squirrel.cpp @@ -0,0 +1,739 @@ +#include "pch.h" +#include "squirrel.h" +#include "core/convar/concommand.h" +#include "mods/modmanager.h" +#include "dedicated/dedicated.h" +#include "engine/r2engine.h" +#include "core/tier0.h" + +#include + +AUTOHOOK_INIT() + +std::shared_ptr getSquirrelLoggerByContext(ScriptContext context) +{ + switch (context) + { + case ScriptContext::UI: + return NS::log::SCRIPT_UI; + case ScriptContext::CLIENT: + return NS::log::SCRIPT_CL; + case ScriptContext::SERVER: + return NS::log::SCRIPT_SV; + default: + throw std::runtime_error("getSquirrelLoggerByContext called with invalid context"); + return nullptr; + } +} + +namespace NS::log +{ + template std::shared_ptr squirrel_logger() + { + // Switch statements can't be constexpr afaik + // clang-format off + if constexpr (context == ScriptContext::UI) { return SCRIPT_UI; } + if constexpr (context == ScriptContext::CLIENT) { return SCRIPT_CL; } + if constexpr (context == ScriptContext::SERVER) { return SCRIPT_SV; } + // clang-format on + } +}; // namespace NS::log + +const char* GetContextName(ScriptContext context) +{ + switch (context) + { + case ScriptContext::CLIENT: + return "CLIENT"; + case ScriptContext::SERVER: + return "SERVER"; + case ScriptContext::UI: + return "UI"; + default: + return "UNKNOWN"; + } +} + +const char* GetContextName_Short(ScriptContext context) +{ + switch (context) + { + case ScriptContext::CLIENT: + return "CL"; + case ScriptContext::SERVER: + return "SV"; + case ScriptContext::UI: + return "UI"; + default: + return "??"; + } +} + +eSQReturnType SQReturnTypeFromString(const char* pReturnType) +{ + static const std::map sqReturnTypeNameToString = { + {"bool", eSQReturnType::Boolean}, + {"float", eSQReturnType::Float}, + {"vector", eSQReturnType::Vector}, + {"int", eSQReturnType::Integer}, + {"entity", eSQReturnType::Entity}, + {"string", eSQReturnType::String}, + {"array", eSQReturnType::Arrays}, + {"asset", eSQReturnType::Asset}, + {"table", eSQReturnType::Table}}; + + if (sqReturnTypeNameToString.find(pReturnType) != sqReturnTypeNameToString.end()) + return sqReturnTypeNameToString.at(pReturnType); + else + return eSQReturnType::Default; // previous default value +} + +const char* SQTypeNameFromID(int type) +{ + switch (type) + { + case OT_ASSET: + return "asset"; + case OT_INTEGER: + return "int"; + case OT_BOOL: + return "bool"; + case SQOBJECT_NUMERIC: + return "float or int"; + case OT_NULL: + return "null"; + case OT_VECTOR: + return "vector"; + case 0: + return "var"; + case OT_USERDATA: + return "userdata"; + case OT_FLOAT: + return "float"; + case OT_STRING: + return "string"; + case OT_ARRAY: + return "array"; + case 0x8000200: + return "function"; + case 0x8100000: + return "structdef"; + case OT_THREAD: + return "thread"; + case OT_FUNCPROTO: + return "function"; + case OT_CLAAS: + return "class"; + case OT_WEAKREF: + return "weakref"; + case 0x8080000: + return "unimplemented function"; + case 0x8200000: + return "struct instance"; + case OT_TABLE: + return "table"; + case 0xA008000: + return "instance"; + case OT_ENTITY: + return "entity"; + } + return ""; +} + +// Allows for generating squirrelmessages from plugins. +// Not used in this version, but will be used later +void AsyncCall_External(ScriptContext context, const char* func_name, SquirrelMessage_External_Pop function) +{ + SquirrelMessage message {}; + message.functionName = func_name; + message.isExternal = true; + message.externalFunc = function; + switch (context) + { + case ScriptContext::CLIENT: + g_pSquirrel->messageBuffer->push(message); + case ScriptContext::SERVER: + g_pSquirrel->messageBuffer->push(message); + case ScriptContext::UI: + g_pSquirrel->messageBuffer->push(message); + } +} + +// needed to define implementations for squirrelmanager outside of squirrel.h without compiler errors +template class SquirrelManager; +template class SquirrelManager; +template class SquirrelManager; + +template void SquirrelManager::VMCreated(CSquirrelVM* newSqvm) +{ + m_pSQVM = newSqvm; + + for (SQFuncRegistration* funcReg : m_funcRegistrations) + { + spdlog::info("Registering {} function {}", GetContextName(context), funcReg->squirrelFuncName); + RegisterSquirrelFunc(m_pSQVM, funcReg, 1); + } + + for (auto& pair : g_pModManager->m_DependencyConstants) + { + bool bWasFound = false; + + for (Mod& dependency : g_pModManager->m_LoadedMods) + { + if (!dependency.m_bEnabled) + continue; + + if (dependency.Name == pair.second) + { + bWasFound = true; + break; + } + } + + defconst(m_pSQVM, pair.first.c_str(), bWasFound); + } + g_pSquirrel->messageBuffer = new SquirrelMessageBuffer(); +} + +template void SquirrelManager::VMDestroyed() +{ + m_pSQVM = nullptr; +} + +template void SquirrelManager::ExecuteCode(const char* pCode) +{ + if (!m_pSQVM || !m_pSQVM->sqvm) + { + spdlog::error("Cannot execute code, {} squirrel vm is not initialised", GetContextName(context)); + return; + } + + spdlog::info("Executing {} script code {} ", GetContextName(context), pCode); + + std::string strCode(pCode); + CompileBufferState bufferState = CompileBufferState(strCode); + + SQRESULT compileResult = compilebuffer(&bufferState, "console"); + spdlog::info("sq_compilebuffer returned {}", PrintSQRESULT.at(compileResult)); + + if (compileResult != SQRESULT_ERROR) + { + pushroottable(m_pSQVM->sqvm); + SQRESULT callResult = _call(m_pSQVM->sqvm, 0); + spdlog::info("sq_call returned {}", PrintSQRESULT.at(callResult)); + } +} + +template void SquirrelManager::AddFuncRegistration( + std::string returnType, std::string name, std::string argTypes, std::string helpText, SQFunction func) +{ + SQFuncRegistration* reg = new SQFuncRegistration; + + reg->squirrelFuncName = new char[name.size() + 1]; + strcpy((char*)reg->squirrelFuncName, name.c_str()); + reg->cppFuncName = reg->squirrelFuncName; + + reg->helpText = new char[helpText.size() + 1]; + strcpy((char*)reg->helpText, helpText.c_str()); + + reg->returnTypeString = new char[returnType.size() + 1]; + strcpy((char*)reg->returnTypeString, returnType.c_str()); + reg->returnType = SQReturnTypeFromString(returnType.c_str()); + + reg->argTypes = new char[argTypes.size() + 1]; + strcpy((char*)reg->argTypes, argTypes.c_str()); + + reg->funcPtr = func; + + m_funcRegistrations.push_back(reg); +} + +template SQRESULT SquirrelManager::setupfunc(const SQChar* funcname) +{ + pushroottable(m_pSQVM->sqvm); + pushstring(m_pSQVM->sqvm, funcname, -1); + + SQRESULT result = get(m_pSQVM->sqvm, -2); + if (result != SQRESULT_ERROR) + pushroottable(m_pSQVM->sqvm); + + return result; +} + +template void SquirrelManager::AddFuncOverride(std::string name, SQFunction func) +{ + m_funcOverrides[name] = func; +} + +// hooks +bool IsUIVM(ScriptContext context, HSquirrelVM* pSqvm) +{ + return context != ScriptContext::SERVER && g_pSquirrel->m_pSQVM && + g_pSquirrel->m_pSQVM->sqvm == pSqvm; +} + +template void* (*__fastcall sq_compiler_create)(HSquirrelVM* sqvm, void* a2, void* a3, SQBool bShouldThrowError); +template void* __fastcall sq_compiler_createHook(HSquirrelVM* sqvm, void* a2, void* a3, SQBool bShouldThrowError) +{ + // store whether errors generated from this compile should be fatal + if (IsUIVM(context, sqvm)) + g_pSquirrel->m_bFatalCompilationErrors = bShouldThrowError; + else + g_pSquirrel->m_bFatalCompilationErrors = bShouldThrowError; + + return sq_compiler_create(sqvm, a2, a3, bShouldThrowError); +} + +template SQInteger (*SQPrint)(HSquirrelVM* sqvm, const char* fmt); +template SQInteger SQPrintHook(HSquirrelVM* sqvm, const char* fmt, ...) +{ + va_list va; + va_start(va, fmt); + + SQChar buf[1024]; + int charsWritten = vsnprintf_s(buf, _TRUNCATE, fmt, va); + + if (charsWritten > 0) + { + if (buf[charsWritten - 1] == '\n') + buf[charsWritten - 1] = '\0'; + g_pSquirrel->logger->info("{}", buf); + } + + va_end(va); + return 0; +} + +template CSquirrelVM* (*__fastcall CreateNewVM)(void* a1, ScriptContext realContext); +template CSquirrelVM* __fastcall CreateNewVMHook(void* a1, ScriptContext realContext) +{ + CSquirrelVM* sqvm = CreateNewVM(a1, realContext); + if (realContext == ScriptContext::UI) + g_pSquirrel->VMCreated(sqvm); + else + g_pSquirrel->VMCreated(sqvm); + + spdlog::info("CreateNewVM {} {}", GetContextName(realContext), (void*)sqvm); + return sqvm; +} + +template void (*__fastcall DestroyVM)(void* a1, HSquirrelVM* sqvm); +template void __fastcall DestroyVMHook(void* a1, HSquirrelVM* sqvm) +{ + ScriptContext realContext = context; // ui and client use the same function so we use this for prints + if (IsUIVM(context, sqvm)) + { + realContext = ScriptContext::UI; + g_pSquirrel->VMDestroyed(); + DestroyVM(a1, sqvm); // If we pass UI here it crashes + } + else + { + g_pSquirrel->m_pSQVM = nullptr; // Fixes a race-like bug + DestroyVM(a1, sqvm); + } + + spdlog::info("DestroyVM {} {}", GetContextName(realContext), (void*)sqvm); +} + +template +void (*__fastcall SQCompileError)(HSquirrelVM* sqvm, const char* error, const char* file, int line, int column); +template +void __fastcall ScriptCompileErrorHook(HSquirrelVM* sqvm, const char* error, const char* file, int line, int column) +{ + bool bIsFatalError = g_pSquirrel->m_bFatalCompilationErrors; + ScriptContext realContext = context; // ui and client use the same function so we use this for prints + if (IsUIVM(context, sqvm)) + { + realContext = ScriptContext::UI; + bIsFatalError = g_pSquirrel->m_bFatalCompilationErrors; + } + + auto logger = getSquirrelLoggerByContext(realContext); + + logger->error("COMPILE ERROR {}", error); + logger->error("{} line [{}] column [{}]", file, line, column); + + // use disconnect to display an error message for the compile error, but only if the compilation error was fatal + // todo, we could get this from sqvm itself probably, rather than hooking sq_compiler_create + if (bIsFatalError) + { + // kill dedicated server if we hit this + if (IsDedicatedServer()) + { + // flush the logger before we exit so debug things get saved to log file + logger->flush(); + exit(EXIT_FAILURE); + } + else + { + R2::Cbuf_AddText( + R2::Cbuf_GetCurrentPlayer(), + fmt::format("disconnect \"Encountered {} script compilation error, see console for details.\"", GetContextName(realContext)) + .c_str(), + R2::cmd_source_t::kCommandSrcCode); + + // likely temp: show console so user can see any errors, as error message wont display if ui is dead + // maybe we could disable all mods other than the coremods and try a reload before doing this? + // could also maybe do some vgui bullshit to show something visually rather than console + if (realContext == ScriptContext::UI) + R2::Cbuf_AddText(R2::Cbuf_GetCurrentPlayer(), "showconsole", R2::cmd_source_t::kCommandSrcCode); + } + } + + // dont call the original function since it kills game lol +} + +template +int64_t (*__fastcall RegisterSquirrelFunction)(CSquirrelVM* sqvm, SQFuncRegistration* funcReg, char unknown); +template +int64_t __fastcall RegisterSquirrelFunctionHook(CSquirrelVM* sqvm, SQFuncRegistration* funcReg, char unknown) +{ + if (IsUIVM(context, sqvm->sqvm)) + { + if (g_pSquirrel->m_funcOverrides.count(funcReg->squirrelFuncName)) + { + g_pSquirrel->m_funcOriginals[funcReg->squirrelFuncName] = funcReg->funcPtr; + funcReg->funcPtr = g_pSquirrel->m_funcOverrides[funcReg->squirrelFuncName]; + spdlog::info("Replacing {} in UI", std::string(funcReg->squirrelFuncName)); + } + + return g_pSquirrel->RegisterSquirrelFunc(sqvm, funcReg, unknown); + } + + if (g_pSquirrel->m_funcOverrides.find(funcReg->squirrelFuncName) != g_pSquirrel->m_funcOverrides.end()) + { + g_pSquirrel->m_funcOriginals[funcReg->squirrelFuncName] = funcReg->funcPtr; + funcReg->funcPtr = g_pSquirrel->m_funcOverrides[funcReg->squirrelFuncName]; + spdlog::info("Replacing {} in Client", std::string(funcReg->squirrelFuncName)); + } + + return g_pSquirrel->RegisterSquirrelFunc(sqvm, funcReg, unknown); +} + +template bool (*__fastcall CallScriptInitCallback)(void* sqvm, const char* callback); +template bool __fastcall CallScriptInitCallbackHook(void* sqvm, const char* callback) +{ + ScriptContext realContext = context; + bool bShouldCallCustomCallbacks = true; + + if (context == ScriptContext::CLIENT) + { + if (!strcmp(callback, "UICodeCallback_UIInit")) + realContext = ScriptContext::UI; + else if (strcmp(callback, "ClientCodeCallback_MapSpawn")) + bShouldCallCustomCallbacks = false; + } + else if (context == ScriptContext::SERVER) + bShouldCallCustomCallbacks = !strcmp(callback, "CodeCallback_MapSpawn"); + + if (bShouldCallCustomCallbacks) + { + for (Mod mod : g_pModManager->m_LoadedMods) + { + if (!mod.m_bEnabled) + continue; + + for (ModScript script : mod.Scripts) + { + for (ModScriptCallback modCallback : script.Callbacks) + { + if (modCallback.Context == realContext && modCallback.BeforeCallback.length()) + { + spdlog::info("Running custom {} script callback \"{}\"", GetContextName(realContext), modCallback.BeforeCallback); + CallScriptInitCallback(sqvm, modCallback.BeforeCallback.c_str()); + } + } + } + } + } + + spdlog::info("{} CodeCallback {} called", GetContextName(realContext), callback); + if (!bShouldCallCustomCallbacks) + spdlog::info("Not executing custom callbacks for CodeCallback {}", callback); + bool ret = CallScriptInitCallback(sqvm, callback); + + // run after callbacks + if (bShouldCallCustomCallbacks) + { + for (Mod mod : g_pModManager->m_LoadedMods) + { + if (!mod.m_bEnabled) + continue; + + for (ModScript script : mod.Scripts) + { + for (ModScriptCallback modCallback : script.Callbacks) + { + if (modCallback.Context == realContext && modCallback.AfterCallback.length()) + { + spdlog::info("Running custom {} script callback \"{}\"", GetContextName(realContext), modCallback.AfterCallback); + CallScriptInitCallback(sqvm, modCallback.AfterCallback.c_str()); + } + } + } + } + } + + return ret; +} + +template void ConCommand_script(const CCommand& args) +{ + g_pSquirrel->ExecuteCode(args.ArgS()); +} + +// literal class type that wraps a constant expression string +template struct TemplateStringLiteral +{ + constexpr TemplateStringLiteral(const char (&str)[N]) + { + std::copy_n(str, N, value); + } + + char value[N]; +}; + +template SQRESULT SQ_StubbedFunc(HSquirrelVM* sqvm) +{ + spdlog::info("Blocking call to stubbed function {} in {}", funcName.value, GetContextName(context)); + return SQRESULT_NULL; +} + +template void StubUnsafeSQFuncs() +{ + if (!Tier0::CommandLine()->CheckParm("-allowunsafesqfuncs")) + { + g_pSquirrel->AddFuncOverride("DevTextBufferWrite", SQ_StubbedFunc); + g_pSquirrel->AddFuncOverride("DevTextBufferClear", SQ_StubbedFunc); + g_pSquirrel->AddFuncOverride("DevTextBufferDumpToFile", SQ_StubbedFunc); + g_pSquirrel->AddFuncOverride("Dev_CommandLineAddParam", SQ_StubbedFunc); + g_pSquirrel->AddFuncOverride("DevP4Checkout", SQ_StubbedFunc); + g_pSquirrel->AddFuncOverride("DevP4Add", SQ_StubbedFunc); + } +} + +template void SquirrelManager::ProcessMessageBuffer() +{ + auto maybeMessage = messageBuffer->pop(); + if (!maybeMessage) + { + return; + } + + SquirrelMessage message = maybeMessage.value(); + + SQObject functionobj {}; + int result = sq_getfunction(m_pSQVM->sqvm, message.functionName.c_str(), &functionobj, 0); + if (result != 0) // This func returns 0 on success for some reason + { + NS::log::squirrel_logger()->error( + "ProcessMessageBuffer was unable to find function with name '{}'. Is it global?", message.functionName); + return; + } + pushobject(m_pSQVM->sqvm, &functionobj); // Push the function object + pushroottable(m_pSQVM->sqvm); + if (message.isExternal) + { + message.externalFunc(m_pSQVM->sqvm); + } + else + { + for (auto& v : message.args) + { + // Execute lambda to push arg to stack + v(); + } + } + + _call(m_pSQVM->sqvm, message.args.size()); +} + +ON_DLL_LOAD_RELIESON("client.dll", ClientSquirrel, ConCommand, (CModule module)) +{ + AUTOHOOK_DISPATCH_MODULE(client.dll) + + g_pSquirrel->__sq_defconst = module.Offset(0x12120).As(); + g_pSquirrel->__sq_defconst = g_pSquirrel->__sq_defconst; + + g_pSquirrel->__sq_compilebuffer = module.Offset(0x3110).As(); + g_pSquirrel->__sq_pushroottable = module.Offset(0x5860).As(); + g_pSquirrel->__sq_compilebuffer = g_pSquirrel->__sq_compilebuffer; + g_pSquirrel->__sq_pushroottable = g_pSquirrel->__sq_pushroottable; + + g_pSquirrel->__sq_call = module.Offset(0x8650).As(); + g_pSquirrel->__sq_call = g_pSquirrel->__sq_call; + + g_pSquirrel->__sq_newarray = module.Offset(0x39F0).As(); + g_pSquirrel->__sq_arrayappend = module.Offset(0x3C70).As(); + g_pSquirrel->__sq_newarray = g_pSquirrel->__sq_newarray; + g_pSquirrel->__sq_arrayappend = g_pSquirrel->__sq_arrayappend; + + g_pSquirrel->__sq_newtable = module.Offset(0x3960).As(); + g_pSquirrel->__sq_newslot = module.Offset(0x70B0).As(); + g_pSquirrel->__sq_newtable = g_pSquirrel->__sq_newtable; + g_pSquirrel->__sq_newslot = g_pSquirrel->__sq_newslot; + + g_pSquirrel->__sq_pushstring = module.Offset(0x3440).As(); + g_pSquirrel->__sq_pushinteger = module.Offset(0x36A0).As(); + g_pSquirrel->__sq_pushfloat = module.Offset(0x3800).As(); + g_pSquirrel->__sq_pushbool = module.Offset(0x3710).As(); + g_pSquirrel->__sq_pushasset = module.Offset(0x3560).As(); + g_pSquirrel->__sq_pushvector = module.Offset(0x3780).As(); + g_pSquirrel->__sq_pushobject = module.Offset(0x83D0).As(); + g_pSquirrel->__sq_raiseerror = module.Offset(0x8470).As(); + g_pSquirrel->__sq_pushstring = g_pSquirrel->__sq_pushstring; + g_pSquirrel->__sq_pushinteger = g_pSquirrel->__sq_pushinteger; + g_pSquirrel->__sq_pushfloat = g_pSquirrel->__sq_pushfloat; + g_pSquirrel->__sq_pushbool = g_pSquirrel->__sq_pushbool; + g_pSquirrel->__sq_pushvector = g_pSquirrel->__sq_pushvector; + g_pSquirrel->__sq_pushasset = g_pSquirrel->__sq_pushasset; + g_pSquirrel->__sq_pushobject = g_pSquirrel->__sq_pushobject; + g_pSquirrel->__sq_raiseerror = g_pSquirrel->__sq_raiseerror; + + g_pSquirrel->__sq_getstring = module.Offset(0x60C0).As(); + g_pSquirrel->__sq_getinteger = module.Offset(0x60E0).As(); + g_pSquirrel->__sq_getfloat = module.Offset(0x6100).As(); + g_pSquirrel->__sq_getbool = module.Offset(0x6130).As(); + g_pSquirrel->__sq_get = module.Offset(0x7C30).As(); + g_pSquirrel->__sq_getasset = module.Offset(0x6010).As(); + g_pSquirrel->__sq_getuserdata = module.Offset(0x63D0).As(); + g_pSquirrel->__sq_getvector = module.Offset(0x6140).As(); + g_pSquirrel->__sq_getstring = g_pSquirrel->__sq_getstring; + g_pSquirrel->__sq_getinteger = g_pSquirrel->__sq_getinteger; + g_pSquirrel->__sq_getfloat = g_pSquirrel->__sq_getfloat; + g_pSquirrel->__sq_getbool = g_pSquirrel->__sq_getbool; + g_pSquirrel->__sq_get = g_pSquirrel->__sq_get; + g_pSquirrel->__sq_getasset = g_pSquirrel->__sq_getasset; + g_pSquirrel->__sq_getuserdata = g_pSquirrel->__sq_getuserdata; + g_pSquirrel->__sq_getvector = g_pSquirrel->__sq_getvector; + g_pSquirrel->__sq_getthisentity = g_pSquirrel->__sq_getthisentity; + g_pSquirrel->__sq_getobject = g_pSquirrel->__sq_getobject; + + g_pSquirrel->__sq_createuserdata = module.Offset(0x38D0).As(); + g_pSquirrel->__sq_setuserdatatypeid = module.Offset(0x6490).As(); + g_pSquirrel->__sq_createuserdata = g_pSquirrel->__sq_createuserdata; + g_pSquirrel->__sq_setuserdatatypeid = g_pSquirrel->__sq_setuserdatatypeid; + + g_pSquirrel->__sq_GetEntityConstant_CBaseEntity = module.Offset(0x3E49B0).As(); + g_pSquirrel->__sq_getentityfrominstance = module.Offset(0x114F0).As(); + g_pSquirrel->__sq_GetEntityConstant_CBaseEntity = + g_pSquirrel->__sq_GetEntityConstant_CBaseEntity; + g_pSquirrel->__sq_getentityfrominstance = g_pSquirrel->__sq_getentityfrominstance; + + // Message buffer stuff + g_pSquirrel->messageBuffer = g_pSquirrel->messageBuffer; + g_pSquirrel->__sq_getfunction = module.Offset(0x572FB0).As(); + g_pSquirrel->__sq_getfunction = g_pSquirrel->__sq_getfunction; + + MAKEHOOK( + module.Offset(0x108E0), + &RegisterSquirrelFunctionHook, + &g_pSquirrel->RegisterSquirrelFunc); + g_pSquirrel->RegisterSquirrelFunc = g_pSquirrel->RegisterSquirrelFunc; + + g_pSquirrel->logger = NS::log::SCRIPT_CL; + g_pSquirrel->logger = NS::log::SCRIPT_UI; + + // uiscript_reset concommand: don't loop forever if compilation fails + module.Offset(0x3C6E4C).NOP(6); + + MAKEHOOK(module.Offset(0x8AD0), &sq_compiler_createHook, &sq_compiler_create); + + MAKEHOOK(module.Offset(0x12B00), &SQPrintHook, &SQPrint); + MAKEHOOK(module.Offset(0x12BA0), &SQPrintHook, &SQPrint); + + MAKEHOOK(module.Offset(0x26130), &CreateNewVMHook, &CreateNewVM); + MAKEHOOK(module.Offset(0x26E70), &DestroyVMHook, &DestroyVM); + MAKEHOOK(module.Offset(0x79A50), &ScriptCompileErrorHook, &SQCompileError); + + MAKEHOOK(module.Offset(0x10190), &CallScriptInitCallbackHook, &CallScriptInitCallback); + + RegisterConCommand("script_client", ConCommand_script, "Executes script code on the client vm", FCVAR_CLIENTDLL); + RegisterConCommand("script_ui", ConCommand_script, "Executes script code on the ui vm", FCVAR_CLIENTDLL); + + StubUnsafeSQFuncs(); + StubUnsafeSQFuncs(); + + g_pSquirrel->__sq_getfunction = module.Offset(0x6CB0).As(); + g_pSquirrel->__sq_getfunction = g_pSquirrel->__sq_getfunction; +} + +ON_DLL_LOAD_RELIESON("server.dll", ServerSquirrel, ConCommand, (CModule module)) +{ + AUTOHOOK_DISPATCH_MODULE(server.dll) + + g_pSquirrel->__sq_defconst = module.Offset(0x1F550).As(); + + g_pSquirrel->__sq_compilebuffer = module.Offset(0x3110).As(); + g_pSquirrel->__sq_pushroottable = module.Offset(0x5840).As(); + g_pSquirrel->__sq_call = module.Offset(0x8620).As(); + + g_pSquirrel->__sq_newarray = module.Offset(0x39F0).As(); + g_pSquirrel->__sq_arrayappend = module.Offset(0x3C70).As(); + + g_pSquirrel->__sq_newtable = module.Offset(0x3960).As(); + g_pSquirrel->__sq_newslot = module.Offset(0x7080).As(); + + g_pSquirrel->__sq_pushstring = module.Offset(0x3440).As(); + g_pSquirrel->__sq_pushinteger = module.Offset(0x36A0).As(); + g_pSquirrel->__sq_pushfloat = module.Offset(0x3800).As(); + g_pSquirrel->__sq_pushbool = module.Offset(0x3710).As(); + g_pSquirrel->__sq_pushasset = module.Offset(0x3560).As(); + g_pSquirrel->__sq_pushvector = module.Offset(0x3780).As(); + g_pSquirrel->__sq_pushobject = module.Offset(0x83A0).As(); + + g_pSquirrel->__sq_raiseerror = module.Offset(0x8440).As(); + + g_pSquirrel->__sq_getstring = module.Offset(0x60A0).As(); + g_pSquirrel->__sq_getinteger = module.Offset(0x60C0).As(); + g_pSquirrel->__sq_getfloat = module.Offset(0x60E0).As(); + g_pSquirrel->__sq_getbool = module.Offset(0x6110).As(); + g_pSquirrel->__sq_getasset = module.Offset(0x5FF0).As(); + g_pSquirrel->__sq_getuserdata = module.Offset(0x63B0).As(); + g_pSquirrel->__sq_getvector = module.Offset(0x6120).As(); + g_pSquirrel->__sq_get = module.Offset(0x7C00).As(); + + g_pSquirrel->__sq_getthisentity = module.Offset(0x203B0).As(); + g_pSquirrel->__sq_getobject = module.Offset(0x6140).As(); + + g_pSquirrel->__sq_createuserdata = module.Offset(0x38D0).As(); + g_pSquirrel->__sq_setuserdatatypeid = module.Offset(0x6470).As(); + + g_pSquirrel->__sq_GetEntityConstant_CBaseEntity = module.Offset(0x418AF0).As(); + g_pSquirrel->__sq_getentityfrominstance = module.Offset(0x1E920).As(); + + g_pSquirrel->logger = NS::log::SCRIPT_SV; + // Message buffer stuff + g_pSquirrel->__sq_getfunction = module.Offset(0x6C85).As(); + + MAKEHOOK( + module.Offset(0x1DD10), + &RegisterSquirrelFunctionHook, + &g_pSquirrel->RegisterSquirrelFunc); + + MAKEHOOK(module.Offset(0x8AA0), &sq_compiler_createHook, &sq_compiler_create); + + MAKEHOOK(module.Offset(0x1FE90), &SQPrintHook, &SQPrint); + MAKEHOOK(module.Offset(0x260E0), &CreateNewVMHook, &CreateNewVM); + MAKEHOOK(module.Offset(0x26E20), &DestroyVMHook, &DestroyVM); + MAKEHOOK(module.Offset(0x799E0), &ScriptCompileErrorHook, &SQCompileError); + MAKEHOOK(module.Offset(0x1D5C0), &CallScriptInitCallbackHook, &CallScriptInitCallback); + + // FCVAR_CHEAT and FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS allows clients to execute this, but since it's unsafe we only allow it when cheats + // are enabled for script_client and script_ui, we don't use cheats, so clients can execute them on themselves all they want + RegisterConCommand( + "script", + ConCommand_script, + "Executes script code on the server vm", + FCVAR_GAMEDLL | FCVAR_GAMEDLL_FOR_REMOTE_CLIENTS | FCVAR_CHEAT); + + StubUnsafeSQFuncs(); +} + +void InitialiseSquirrelManagers() +{ + g_pSquirrel = new SquirrelManager; + g_pSquirrel = new SquirrelManager; + g_pSquirrel = new SquirrelManager; +} diff --git a/NorthstarDLL/squirrel/squirrel.h b/NorthstarDLL/squirrel/squirrel.h new file mode 100644 index 00000000..3e3d08c9 --- /dev/null +++ b/NorthstarDLL/squirrel/squirrel.h @@ -0,0 +1,469 @@ +#pragma once + +#include "logging/logging.h" +#include "squirrelclasstypes.h" +#include "squirrelautobind.h" +#include "core/math/vector.h" + +// stolen from ttf2sdk: sqvm types +typedef float SQFloat; +typedef long SQInteger; +typedef unsigned long SQUnsignedInteger; +typedef char SQChar; +typedef SQUnsignedInteger SQBool; + +static constexpr int operator&(ScriptContext first, ScriptContext second) +{ + return first == second; +} + +static constexpr int operator&(int first, ScriptContext second) +{ + return first & (1 << static_cast(second)); +} + +static constexpr int operator|(ScriptContext first, ScriptContext second) +{ + return (1 << static_cast(first)) + (1 << static_cast(second)); +} + +static constexpr int operator|(int first, ScriptContext second) +{ + return first + (1 << static_cast(second)); +} + +const char* GetContextName(ScriptContext context); +const char* GetContextName_Short(ScriptContext context); +eSQReturnType SQReturnTypeFromString(const char* pReturnType); +const char* SQTypeNameFromID(const int iTypeId); + +std::shared_ptr getSquirrelLoggerByContext(ScriptContext context); + +namespace NS::log +{ + template std::shared_ptr squirrel_logger(); +}; // namespace NS::log + +void schedule_call_external(ScriptContext context, const char* func_name, SquirrelMessage_External_Pop function); + +// This base class means that only the templated functions have to be rebuilt for each template instance +// Cuts down on compile time by ~5 seconds +class SquirrelManagerBase +{ + protected: + std::vector m_funcRegistrations; + + public: + CSquirrelVM* m_pSQVM; + std::map m_funcOverrides = {}; + std::map m_funcOriginals = {}; + + bool m_bFatalCompilationErrors = false; + + std::shared_ptr logger; + +#pragma region SQVM funcs + RegisterSquirrelFuncType RegisterSquirrelFunc; + sq_defconstType __sq_defconst; + + sq_compilebufferType __sq_compilebuffer; + sq_callType __sq_call; + sq_raiseerrorType __sq_raiseerror; + + sq_newarrayType __sq_newarray; + sq_arrayappendType __sq_arrayappend; + + sq_newtableType __sq_newtable; + sq_newslotType __sq_newslot; + + sq_pushroottableType __sq_pushroottable; + sq_pushstringType __sq_pushstring; + sq_pushintegerType __sq_pushinteger; + sq_pushfloatType __sq_pushfloat; + sq_pushboolType __sq_pushbool; + sq_pushassetType __sq_pushasset; + sq_pushvectorType __sq_pushvector; + sq_pushobjectType __sq_pushobject; + + sq_getstringType __sq_getstring; + sq_getintegerType __sq_getinteger; + sq_getfloatType __sq_getfloat; + sq_getboolType __sq_getbool; + sq_getType __sq_get; + sq_getassetType __sq_getasset; + sq_getuserdataType __sq_getuserdata; + sq_getvectorType __sq_getvector; + sq_getthisentityType __sq_getthisentity; + sq_getobjectType __sq_getobject; + + sq_createuserdataType __sq_createuserdata; + sq_setuserdatatypeidType __sq_setuserdatatypeid; + sq_getfunctionType __sq_getfunction; + + sq_getentityfrominstanceType __sq_getentityfrominstance; + sq_GetEntityConstantType __sq_GetEntityConstant_CBaseEntity; + +#pragma endregion + +#pragma region SQVM func wrappers + inline void defconst(CSquirrelVM* sqvm, const SQChar* pName, int nValue) + { + __sq_defconst(sqvm, pName, nValue); + } + + inline SQRESULT + compilebuffer(CompileBufferState* bufferState, const SQChar* bufferName = "unnamedbuffer", const SQBool bShouldThrowError = false) + { + return __sq_compilebuffer(m_pSQVM->sqvm, bufferState, bufferName, -1, bShouldThrowError); + } + + inline SQRESULT _call(HSquirrelVM* sqvm, const SQInteger args) + { + return __sq_call(sqvm, args + 1, false, false); + } + + inline SQInteger raiseerror(HSquirrelVM* sqvm, const SQChar* sError) + { + return __sq_raiseerror(sqvm, sError); + } + + inline void newarray(HSquirrelVM* sqvm, const SQInteger stackpos = 0) + { + __sq_newarray(sqvm, stackpos); + } + + inline SQRESULT arrayappend(HSquirrelVM* sqvm, const SQInteger stackpos) + { + return __sq_arrayappend(sqvm, stackpos); + } + + inline SQRESULT newtable(HSquirrelVM* sqvm) + { + return __sq_newtable(sqvm); + } + + inline SQRESULT newslot(HSquirrelVM* sqvm, SQInteger idx, SQBool bStatic) + { + return __sq_newslot(sqvm, idx, bStatic); + } + + inline void pushroottable(HSquirrelVM* sqvm) + { + __sq_pushroottable(sqvm); + } + + inline void pushstring(HSquirrelVM* sqvm, const SQChar* sVal, int length = -1) + { + __sq_pushstring(sqvm, sVal, length); + } + + inline void pushinteger(HSquirrelVM* sqvm, const SQInteger iVal) + { + __sq_pushinteger(sqvm, iVal); + } + + inline void pushfloat(HSquirrelVM* sqvm, const SQFloat flVal) + { + __sq_pushfloat(sqvm, flVal); + } + + inline void pushbool(HSquirrelVM* sqvm, const SQBool bVal) + { + __sq_pushbool(sqvm, bVal); + } + + inline void pushasset(HSquirrelVM* sqvm, const SQChar* sVal, int length = -1) + { + __sq_pushasset(sqvm, sVal, length); + } + + inline void pushvector(HSquirrelVM* sqvm, const Vector3 pVal) + { + __sq_pushvector(sqvm, *(float**)&pVal); + } + + inline void pushobject(HSquirrelVM* sqvm, SQObject* obj) + { + __sq_pushobject(sqvm, obj); + } + + inline const SQChar* getstring(HSquirrelVM* sqvm, const SQInteger stackpos) + { + return __sq_getstring(sqvm, stackpos); + } + + inline SQInteger getinteger(HSquirrelVM* sqvm, const SQInteger stackpos) + { + return __sq_getinteger(sqvm, stackpos); + } + + inline SQFloat getfloat(HSquirrelVM* sqvm, const SQInteger stackpos) + { + return __sq_getfloat(sqvm, stackpos); + } + + inline SQBool getbool(HSquirrelVM* sqvm, const SQInteger stackpos) + { + return __sq_getbool(sqvm, stackpos); + } + + inline SQRESULT get(HSquirrelVM* sqvm, const SQInteger stackpos) + { + return __sq_get(sqvm, stackpos); + } + + inline Vector3 getvector(HSquirrelVM* sqvm, const SQInteger stackpos) + { + float* pRet = __sq_getvector(sqvm, stackpos); + return *(Vector3*)&pRet; + } + + inline int sq_getfunction(HSquirrelVM* sqvm, const char* name, SQObject* returnObj, const char* signature) + { + return __sq_getfunction(sqvm, name, returnObj, signature); + } + + inline SQRESULT getasset(HSquirrelVM* sqvm, const SQInteger stackpos, const char** result) + { + return __sq_getasset(sqvm, stackpos, result); + } + + template inline SQRESULT getuserdata(HSquirrelVM* sqvm, const SQInteger stackpos, T* data, uint64_t* typeId) + { + return __sq_getuserdata(sqvm, stackpos, (void**)data, typeId); // this sometimes crashes idk + } + + template inline T* createuserdata(HSquirrelVM* sqvm, SQInteger size) + { + void* ret = __sq_createuserdata(sqvm, size); + memset(ret, 0, size); + return (T*)ret; + } + + inline SQRESULT setuserdatatypeid(HSquirrelVM* sqvm, const SQInteger stackpos, uint64_t typeId) + { + return __sq_setuserdatatypeid(sqvm, stackpos, typeId); + } + + template inline SQBool getthisentity(HSquirrelVM* sqvm, T* ppEntity) + { + return __sq_getentity(sqvm, (void**)ppEntity); + } + + template inline T* getentity(HSquirrelVM* sqvm, SQInteger iStackPos) + { + SQObject obj; + __sq_getobject(sqvm, iStackPos, &obj); + + // there are entity constants for other types, but seemingly CBaseEntity's is the only one needed + return (T*)__sq_getentityfrominstance(m_pSQVM, &obj, __sq_GetEntityConstant_CBaseEntity()); + } +#pragma endregion +}; + +template class SquirrelManager : public virtual SquirrelManagerBase +{ + public: +#pragma region MessageBuffer + SquirrelMessageBuffer* messageBuffer; + + template SquirrelMessage AsyncCall(std::string funcname, Args... args) + { + // This function schedules a call to be executed on the next frame + // This is useful for things like threads and plugins, which do not run on the main thread + FunctionVector functionVector; + SqRecurseArgs(functionVector, args...); + SquirrelMessage message = {funcname, functionVector}; + messageBuffer->push(message); + return message; + } + + SquirrelMessage AsyncCall(std::string funcname) + { + // This function schedules a call to be executed on the next frame + // This is useful for things like threads and plugins, which do not run on the main thread + FunctionVector functionVector = {}; + SquirrelMessage message = {funcname, functionVector}; + messageBuffer->push(message); + return message; + } + + SQRESULT Call(const char* funcname) + { + // Warning! + // This function assumes the squirrel VM is stopped/blocked at the moment of call + // Calling this function while the VM is running is likely to result in a crash due to stack destruction + // If you want to call into squirrel asynchronously, use `AsyncCall` instead + + if (!m_pSQVM || !m_pSQVM->sqvm) + { + spdlog::error( + "{} was called on context {} while VM was not initialized. This will crash", __FUNCTION__, GetContextName(context)); + } + + SQObject functionobj {}; + int result = sq_getfunction(m_pSQVM->sqvm, funcname, &functionobj, 0); + if (result != 0) // This func returns 0 on success for some reason + { + NS::log::squirrel_logger()->error("Call was unable to find function with name '{}'. Is it global?", funcname); + return SQRESULT_ERROR; + } + pushobject(m_pSQVM->sqvm, &functionobj); // Push the function object + pushroottable(m_pSQVM->sqvm); // Push root table + return _call(m_pSQVM->sqvm, 0); + } + + template SQRESULT Call(const char* funcname, Args... args) + { + // Warning! + // This function assumes the squirrel VM is stopped/blocked at the moment of call + // Calling this function while the VM is running is likely to result in a crash due to stack destruction + // If you want to call into squirrel asynchronously, use `schedule_call` instead + if (!m_pSQVM || !m_pSQVM->sqvm) + { + spdlog::error( + "{} was called on context {} while VM was not initialized. This will crash", __FUNCTION__, GetContextName(context)); + } + SQObject functionobj {}; + int result = sq_getfunction(m_pSQVM->sqvm, funcname, &functionobj, 0); + if (result != 0) // This func returns 0 on success for some reason + { + NS::log::squirrel_logger()->error("Call was unable to find function with name '{}'. Is it global?", funcname); + return SQRESULT_ERROR; + } + pushobject(m_pSQVM->sqvm, &functionobj); // Push the function object + pushroottable(m_pSQVM->sqvm); // Push root table + + FunctionVector functionVector; + SqRecurseArgs(functionVector, args...); + + for (auto& v : functionVector) + { + // Execute lambda to push arg to stack + v(); + } + + return _call(m_pSQVM->sqvm, functionVector.size()); + } + +#pragma endregion + + public: + SquirrelManager() + { + m_pSQVM = nullptr; + } + + void VMCreated(CSquirrelVM* newSqvm); + void VMDestroyed(); + void ExecuteCode(const char* code); + void AddFuncRegistration(std::string returnType, std::string name, std::string argTypes, std::string helpText, SQFunction func); + SQRESULT setupfunc(const SQChar* funcname); + void AddFuncOverride(std::string name, SQFunction func); + void ProcessMessageBuffer(); +}; + +template SquirrelManager* g_pSquirrel; + +void InitialiseSquirrelManagers(); + +/* + Beware all ye who enter below. + This place is not a place of honor... no highly esteemed deed is commemorated here... nothing valued is here. + What is here was dangerous and repulsive to us. This message is a warning about danger. +*/ + +#pragma region MessageBuffer templates + +// Clang-formatting makes this whole thing unreadable +// clang-format off + +#ifndef MessageBufferFuncs +#define MessageBufferFuncs +// Bools +template +requires std::convertible_to && (!std::is_floating_point_v) && (!std::convertible_to) && (!std::convertible_to) +inline VoidFunction SQMessageBufferPushArg(T& arg) { + return [arg]{ g_pSquirrel->pushbool(g_pSquirrel->m_pSQVM->sqvm, static_cast(arg)); }; +} +// Vectors +template +inline VoidFunction SQMessageBufferPushArg(Vector3& arg) { + return [arg]{ g_pSquirrel->pushvector(g_pSquirrel->m_pSQVM->sqvm, arg); }; +} +// Vectors +template +inline VoidFunction SQMessageBufferPushArg(SQObject* arg) { + return [arg]{ g_pSquirrel->pushSQObject(g_pSquirrel->m_pSQVM->sqvm, arg); }; +} +// Ints +template +requires std::convertible_to && (!std::is_floating_point_v) +inline VoidFunction SQMessageBufferPushArg(T& arg) { + return [arg]{ g_pSquirrel->pushinteger(g_pSquirrel->m_pSQVM->sqvm, static_cast(arg)); }; +} +// Floats +template +requires std::convertible_to && (std::is_floating_point_v) +inline VoidFunction SQMessageBufferPushArg(T& arg) { + return [arg]{ g_pSquirrel->pushfloat(g_pSquirrel->m_pSQVM->sqvm, static_cast(arg)); }; +} +// Strings +template +requires (std::convertible_to || std::is_constructible_v) +inline VoidFunction SQMessageBufferPushArg(T& arg) { + auto converted = std::string(arg); + return [converted]{ g_pSquirrel->pushstring(g_pSquirrel->m_pSQVM->sqvm, converted.c_str(), converted.length()); }; +} +// Assets +template +inline VoidFunction SQMessageBufferPushArg(SquirrelAsset& arg) { + return [arg]{ g_pSquirrel->pushasset(g_pSquirrel->m_pSQVM->sqvm, arg.path.c_str(), arg.path.length()); }; +} +// Maps +template +requires is_iterable +inline VoidFunction SQMessageBufferPushArg(T& arg) { + FunctionVector localv = {}; + localv.push_back([]{g_pSquirrel->newarray(g_pSquirrel->m_pSQVM->sqvm, 0);}); + + for (const auto& item : arg) { + localv.push_back(SQMessageBufferPushArg(item)); + localv.push_back([]{g_pSquirrel->arrayappend(g_pSquirrel->m_pSQVM->sqvm, -2);}); + } + + return [localv] { for (auto& func : localv) { func(); } }; +} +// Vectors +template +requires is_map +inline VoidFunction SQMessageBufferPushArg(T& map) { + FunctionVector localv = {}; + localv.push_back([]{g_pSquirrel->newtable(g_pSquirrel->m_pSQVM->sqvm);}); + + for (const auto& item : map) { + localv.push_back(SQMessageBufferPushArg(item.first)); + localv.push_back(SQMessageBufferPushArg(item.second)); + localv.push_back([]{g_pSquirrel->newslot(g_pSquirrel->m_pSQVM->sqvm, -3, false);}); + } + + return [localv]{ for (auto& func : localv) { func(); } }; +} + +template +inline void SqRecurseArgs(FunctionVector& v, T& arg) { + v.push_back(SQMessageBufferPushArg(arg)); +} + +// This function is separated from the PushArg function so as to not generate too many template instances +// This is the main function responsible for unrolling the argument pack +template +inline void SqRecurseArgs(FunctionVector& v, T& arg, Args... args) { + v.push_back(SQMessageBufferPushArg(arg)); + SqRecurseArgs(v, args...); +} + +// clang-format on +#endif + +#pragma endregion diff --git a/NorthstarDLL/squirrel/squirrelautobind.cpp b/NorthstarDLL/squirrel/squirrelautobind.cpp new file mode 100644 index 00000000..d5f42477 --- /dev/null +++ b/NorthstarDLL/squirrel/squirrelautobind.cpp @@ -0,0 +1,21 @@ +#include "pch.h" +#include "squirrelautobind.h" + +SquirrelAutoBindContainer* g_pSqAutoBindContainer; + +ON_DLL_LOAD_RELIESON("client.dll", ClientSquirrelAutoBind, ClientSquirrel, (CModule module)) +{ + spdlog::info("ClientSquirrelAutoBInd AutoBindFuncsVectorsize {}", g_pSqAutoBindContainer->clientSqAutoBindFuncs.size()); + for (auto& autoBindFunc : g_pSqAutoBindContainer->clientSqAutoBindFuncs) + { + autoBindFunc(); + } +} + +ON_DLL_LOAD_RELIESON("server.dll", ServerSquirrelAutoBind, ServerSquirrel, (CModule module)) +{ + for (auto& autoBindFunc : g_pSqAutoBindContainer->serverSqAutoBindFuncs) + { + autoBindFunc(); + } +} diff --git a/NorthstarDLL/squirrel/squirrelautobind.h b/NorthstarDLL/squirrel/squirrelautobind.h new file mode 100644 index 00000000..865479a1 --- /dev/null +++ b/NorthstarDLL/squirrel/squirrelautobind.h @@ -0,0 +1,76 @@ +#pragma once +#include + +typedef void (*SqAutoBindFunc)(); + +class SquirrelAutoBindContainer +{ + public: + std::vector> clientSqAutoBindFuncs; + std::vector> serverSqAutoBindFuncs; +}; + +extern SquirrelAutoBindContainer* g_pSqAutoBindContainer; + +class __squirrelautobind; + +#define ADD_SQFUNC(returnType, funcName, argTypes, helpText, runOnContext) \ + template SQRESULT CONCAT2(Script_, funcName)(HSquirrelVM * sqvm); \ + namespace \ + { \ + __squirrelautobind CONCAT2(__squirrelautobind, __LINE__)( \ + []() \ + { \ + if constexpr (runOnContext & ScriptContext::UI) \ + g_pSquirrel->AddFuncRegistration( \ + returnType, __STR(funcName), argTypes, helpText, CONCAT2(Script_, funcName) < ScriptContext::UI >); \ + if constexpr (runOnContext & ScriptContext::CLIENT) \ + g_pSquirrel->AddFuncRegistration( \ + returnType, __STR(funcName), argTypes, helpText, CONCAT2(Script_, funcName) < ScriptContext::CLIENT >); \ + }, \ + []() \ + { \ + if constexpr (runOnContext & ScriptContext::SERVER) \ + g_pSquirrel->AddFuncRegistration( \ + returnType, __STR(funcName), argTypes, helpText, CONCAT2(Script_, funcName) < ScriptContext::SERVER >); \ + }); \ + } \ + template SQRESULT CONCAT2(Script_, funcName)(HSquirrelVM * sqvm) + +#define REPLACE_SQFUNC(funcName, runOnContext) \ + template SQRESULT CONCAT2(Script_, funcName)(HSquirrelVM * sqvm); \ + namespace \ + { \ + __squirrelautobind CONCAT2(__squirrelautobind, __LINE__)( \ + []() \ + { \ + if constexpr (runOnContext & ScriptContext::UI) \ + g_pSquirrel->AddFuncOverride(__STR(funcName), CONCAT2(Script_, funcName) < ScriptContext::UI >); \ + if constexpr (runOnContext & ScriptContext::CLIENT) \ + g_pSquirrel->AddFuncOverride( \ + __STR(funcName), CONCAT2(Script_, funcName) < ScriptContext::CLIENT >); \ + }, \ + []() \ + { \ + if constexpr (runOnContext & ScriptContext::SERVER) \ + g_pSquirrel->AddFuncOverride( \ + __STR(funcName), CONCAT2(Script_, funcName) < ScriptContext::SERVER >); \ + }); \ + } \ + template SQRESULT CONCAT2(Script_, funcName)(HSquirrelVM * sqvm) + +class __squirrelautobind +{ + public: + __squirrelautobind() = delete; + + __squirrelautobind(std::function clientAutoBindFunc, std::function serverAutoBindFunc) + { + // Bit hacky but we can't initialise this normally since this gets run automatically on load + if (g_pSqAutoBindContainer == nullptr) + g_pSqAutoBindContainer = new SquirrelAutoBindContainer(); + + g_pSqAutoBindContainer->clientSqAutoBindFuncs.push_back(clientAutoBindFunc); + g_pSqAutoBindContainer->serverSqAutoBindFuncs.push_back(serverAutoBindFunc); + } +}; diff --git a/NorthstarDLL/squirrel/squirrelclasstypes.h b/NorthstarDLL/squirrel/squirrelclasstypes.h new file mode 100644 index 00000000..e26bc8d0 --- /dev/null +++ b/NorthstarDLL/squirrel/squirrelclasstypes.h @@ -0,0 +1,239 @@ +#pragma once +#include "pch.h" +#include "squirreldatatypes.h" + +#include + +enum SQRESULT : SQInteger +{ + SQRESULT_ERROR = -1, + SQRESULT_NULL = 0, + SQRESULT_NOTNULL = 1, +}; + +typedef SQRESULT (*SQFunction)(HSquirrelVM* sqvm); + +enum class eSQReturnType +{ + Float = 0x1, + Vector = 0x3, + Integer = 0x5, + Boolean = 0x6, + Entity = 0xD, + String = 0x21, + Default = 0x20, + Arrays = 0x25, + Asset = 0x28, + Table = 0x26, +}; + +const std::map PrintSQRESULT = { + {SQRESULT::SQRESULT_ERROR, "SQRESULT_ERROR"}, + {SQRESULT::SQRESULT_NULL, "SQRESULT_NULL"}, + {SQRESULT::SQRESULT_NOTNULL, "SQRESULT_NOTNULL"}}; + +struct CompileBufferState +{ + const SQChar* buffer; + const SQChar* bufferPlusLength; + const SQChar* bufferAgain; + + CompileBufferState(const std::string& code) + { + buffer = code.c_str(); + bufferPlusLength = code.c_str() + code.size(); + bufferAgain = code.c_str(); + } +}; + +struct SQFuncRegistration +{ + const char* squirrelFuncName; + const char* cppFuncName; + const char* helpText; + const char* returnTypeString; + const char* argTypes; + uint32_t unknown1; + uint32_t devLevel; + const char* shortNameMaybe; + uint32_t unknown2; + eSQReturnType returnType; + uint32_t* externalBufferPointer; + uint64_t externalBufferSize; + uint64_t unknown3; + uint64_t unknown4; + SQFunction funcPtr; + + SQFuncRegistration() + { + memset(this, 0, sizeof(SQFuncRegistration)); + this->returnType = eSQReturnType::Default; + } +}; + +enum class ScriptContext : int +{ + SERVER, + CLIENT, + UI, +}; + +typedef std::vector> FunctionVector; +typedef std::function VoidFunction; + +// clang-format off +template +concept is_map = + // Simple maps + std::same_as> || + std::same_as> || + + // Nested maps + std::same_as < + std::map, + std::map + > || + std::same_as < + std::unordered_map, + std::unordered_map + > || + std::same_as < + std::unordered_map, + std::map + > || + std::same_as < + std::map, + std::unordered_map + > +; + +template +concept is_iterable = requires(std::ranges::range_value_t x) +{ + x.begin(); // must have `x.begin()` + x.end(); // and `x.end()` +}; + +// clang-format on + +typedef void (*SquirrelMessage_External_Pop)(HSquirrelVM* sqvm); +typedef void (*sq_schedule_call_externalType)(ScriptContext context, const char* funcname, SquirrelMessage_External_Pop function); + +class SquirrelMessage +{ + public: + std::string functionName; + FunctionVector args; + bool isExternal = false; + SquirrelMessage_External_Pop externalFunc = NULL; +}; + +class SquirrelMessageBuffer +{ + + private: + std::queue messages = {}; + + public: + std::mutex mutex; + std::optional pop() + { + std::lock_guard guard(mutex); + if (!messages.empty()) + { + auto message = messages.front(); + messages.pop(); + return message; + } + else + { + return std::nullopt; + } + } + + void unwind() + { + auto maybeMessage = this->pop(); + if (!maybeMessage) + { + spdlog::error("Plugin tried consuming SquirrelMessage while buffer was empty"); + return; + } + auto message = maybeMessage.value(); + for (auto& v : message.args) + { + // Execute lambda to push arg to stack + v(); + } + } + + void push(SquirrelMessage message) + { + std::lock_guard guard(mutex); + messages.push(message); + } +}; + +// Super simple wrapper class to allow pushing Assets via call +class SquirrelAsset +{ + public: + std::string path; + SquirrelAsset(std::string path) : path(path) {}; +}; + +#pragma region TypeDefs + +// core sqvm funcs +typedef int64_t (*RegisterSquirrelFuncType)(CSquirrelVM* sqvm, SQFuncRegistration* funcReg, char unknown); +typedef void (*sq_defconstType)(CSquirrelVM* sqvm, const SQChar* name, int value); + +typedef SQRESULT (*sq_compilebufferType)( + HSquirrelVM* sqvm, CompileBufferState* compileBuffer, const char* file, int a1, SQBool bShouldThrowError); +typedef SQRESULT (*sq_callType)(HSquirrelVM* sqvm, SQInteger iArgs, SQBool bShouldReturn, SQBool bThrowError); +typedef SQInteger (*sq_raiseerrorType)(HSquirrelVM* sqvm, const SQChar* pError); + +// sq stack array funcs +typedef void (*sq_newarrayType)(HSquirrelVM* sqvm, SQInteger iStackpos); +typedef SQRESULT (*sq_arrayappendType)(HSquirrelVM* sqvm, SQInteger iStackpos); + +// sq table funcs +typedef SQRESULT (*sq_newtableType)(HSquirrelVM* sqvm); +typedef SQRESULT (*sq_newslotType)(HSquirrelVM* sqvm, SQInteger idx, SQBool bStatic); + +// sq stack push funcs +typedef void (*sq_pushroottableType)(HSquirrelVM* sqvm); +typedef void (*sq_pushstringType)(HSquirrelVM* sqvm, const SQChar* pStr, SQInteger iLength); +typedef void (*sq_pushintegerType)(HSquirrelVM* sqvm, SQInteger i); +typedef void (*sq_pushfloatType)(HSquirrelVM* sqvm, SQFloat f); +typedef void (*sq_pushboolType)(HSquirrelVM* sqvm, SQBool b); +typedef void (*sq_pushassetType)(HSquirrelVM* sqvm, const SQChar* str, SQInteger iLength); +typedef void (*sq_pushvectorType)(HSquirrelVM* sqvm, const SQFloat* pVec); +typedef void (*sq_pushobjectType)(HSquirrelVM* sqvm, SQObject* pVec); + +// sq stack get funcs +typedef const SQChar* (*sq_getstringType)(HSquirrelVM* sqvm, SQInteger iStackpos); +typedef SQInteger (*sq_getintegerType)(HSquirrelVM* sqvm, SQInteger iStackpos); +typedef SQFloat (*sq_getfloatType)(HSquirrelVM*, SQInteger iStackpos); +typedef SQBool (*sq_getboolType)(HSquirrelVM*, SQInteger iStackpos); +typedef SQRESULT (*sq_getType)(HSquirrelVM* sqvm, SQInteger iStackpos); +typedef SQRESULT (*sq_getassetType)(HSquirrelVM* sqvm, SQInteger iStackpos, const char** pResult); +typedef SQRESULT (*sq_getuserdataType)(HSquirrelVM* sqvm, SQInteger iStackpos, void** pData, uint64_t* pTypeId); +typedef SQFloat* (*sq_getvectorType)(HSquirrelVM* sqvm, SQInteger iStackpos); +typedef SQBool (*sq_getthisentityType)(HSquirrelVM*, void** ppEntity); +typedef void (*sq_getobjectType)(HSquirrelVM*, SQInteger iStackPos, SQObject* pOutObj); + +// sq stack userpointer funcs +typedef void* (*sq_createuserdataType)(HSquirrelVM* sqvm, SQInteger iSize); +typedef SQRESULT (*sq_setuserdatatypeidType)(HSquirrelVM* sqvm, SQInteger iStackpos, uint64_t iTypeId); + +// sq misc entity funcs +typedef void* (*sq_getentityfrominstanceType)(CSquirrelVM* sqvm, SQObject* pInstance, char** ppEntityConstant); +typedef char** (*sq_GetEntityConstantType)(); + +typedef int (*sq_getfunctionType)(HSquirrelVM* sqvm, const char* name, SQObject* returnObj, const char* signature); + +#pragma endregion + +// These "external" versions of the types are for plugins +typedef int64_t (*RegisterSquirrelFuncType_External)(ScriptContext context, SQFuncRegistration* funcReg, char unknown); diff --git a/NorthstarDLL/squirrel/squirreldatatypes.h b/NorthstarDLL/squirrel/squirreldatatypes.h new file mode 100644 index 00000000..e9f88d08 --- /dev/null +++ b/NorthstarDLL/squirrel/squirreldatatypes.h @@ -0,0 +1,495 @@ +#pragma once +/* + This file has been generated by IDA. + It contains local type definitions from + the type library 'server.dll' +*/ + +struct HSquirrelVM; +struct CallInfo; +struct SQTable; +struct SQString; +struct SQFunctionProto; +struct SQClosure; +struct SQSharedState; +struct StringTable; +struct SQStructInstance; +struct SQStructDef; +struct SQNativeClosure; +struct SQArray; +struct tableNode; +struct SQUserData; + +typedef void (*releasehookType)(void* val, int size); + +// stolen from ttf2sdk: sqvm types +typedef float SQFloat; +typedef long SQInteger; +typedef unsigned long SQUnsignedInteger; +typedef char SQChar; +typedef SQUnsignedInteger SQBool; + +/* 127 */ +enum SQObjectType : int +{ + _RT_NULL = 0x1, + _RT_INTEGER = 0x2, + _RT_FLOAT = 0x4, + _RT_BOOL = 0x8, + _RT_STRING = 0x10, + _RT_TABLE = 0x20, + _RT_ARRAY = 0x40, + _RT_USERDATA = 0x80, + _RT_CLOSURE = 0x100, + _RT_NATIVECLOSURE = 0x200, + _RT_GENERATOR = 0x400, + OT_USERPOINTER = 0x800, + _RT_USERPOINTER = 0x800, + _RT_THREAD = 0x1000, + _RT_FUNCPROTO = 0x2000, + _RT_CLASS = 0x4000, + _RT_INSTANCE = 0x8000, + _RT_WEAKREF = 0x10000, + OT_VECTOR = 0x40000, + SQOBJECT_CANBEFALSE = 0x1000000, + OT_NULL = 0x1000001, + OT_BOOL = 0x1000008, + SQOBJECT_DELEGABLE = 0x2000000, + SQOBJECT_NUMERIC = 0x4000000, + OT_INTEGER = 0x5000002, + OT_FLOAT = 0x5000004, + SQOBJECT_REF_COUNTED = 0x8000000, + OT_STRING = 0x8000010, + OT_ARRAY = 0x8000040, + OT_CLOSURE = 0x8000100, + OT_NATIVECLOSURE = 0x8000200, + OT_ASSET = 0x8000400, + OT_THREAD = 0x8001000, + OT_FUNCPROTO = 0x8002000, + OT_CLAAS = 0x8004000, + OT_STRUCT = 0x8200000, + OT_WEAKREF = 0x8010000, + OT_TABLE = 0xA000020, + OT_USERDATA = 0xA000080, + OT_INSTANCE = 0xA008000, + OT_ENTITY = 0xA400000, +}; + +/* 156 */ +union SQObjectValue +{ + SQString* asString; + SQTable* asTable; + SQClosure* asClosure; + SQFunctionProto* asFuncProto; + SQStructDef* asStructDef; + long long as64Integer; + SQNativeClosure* asNativeClosure; + SQArray* asArray; + HSquirrelVM* asThread; + float asFloat; + int asInteger; + SQUserData* asUserdata; + SQStructInstance* asStructInstance; +}; + +/* 160 */ +struct SQVector +{ + SQObjectType _Type; + float x; + float y; + float z; +}; + +/* 128 */ +struct SQObject +{ + SQObjectType _Type; + int structNumber; + SQObjectValue _VAL; +}; + +/* 138 */ +struct alignas(8) SQString +{ + void* vftable; + int uiRef; + int padding; + SQString* _next_maybe; + SQSharedState* sharedState; + int length; + unsigned char gap_24[4]; + char _hash[8]; + char _val[1]; +}; + +/* 137 */ +struct alignas(8) SQTable +{ + void* vftable; + unsigned char gap_08[4]; + int uiRef; + unsigned char gap_10[8]; + void* pointer_18; + void* pointer_20; + void* _sharedState; + long long field_30; + tableNode* _nodes; + int _numOfNodes; + int size; + int field_48; + int _usedNodes; + unsigned char _gap_50[20]; + int field_64; + unsigned char _gap_68[80]; +}; + +/* 140 */ +struct alignas(8) SQClosure +{ + void* vftable; + unsigned char gap_08[4]; + int uiRef; + void* pointer_10; + void* pointer_18; + void* pointer_20; + void* sharedState; + SQObject obj_30; + SQObject _function; + SQObject* _outervalues; + unsigned char gap_58[8]; + unsigned char gap_60[96]; + SQObject* objectPointer_C0; + unsigned char gap_C8[16]; +}; + +/* 139 */ +struct alignas(8) SQFunctionProto +{ + void* vftable; + unsigned char gap_08[4]; + int uiRef; + unsigned char gap_10[8]; + void* pointer_18; + void* pointer_20; + void* sharedState; + void* pointer_30; + SQObjectType _fileNameType; + SQString* _fileName; + SQObjectType _funcNameType; + SQString* _funcName; + SQObject obj_58; + unsigned char gap_68[12]; + int _stacksize; + unsigned char gap_78[48]; + int nParameters; + unsigned char gap_AC[60]; + int nDefaultParams; + unsigned char gap_EC[200]; +}; + +/* 152 */ +struct SQStructDef +{ + void* vtable; + int uiRef; + unsigned char padding_C[4]; + unsigned char unknown[24]; + SQSharedState* sharedState; + SQObjectType _nameType; + SQString* _name; + unsigned char gap_38[16]; + SQObjectType _variableNamesType; + SQTable* _variableNames; + unsigned char gap_[32]; +}; + +/* 157 */ +struct alignas(8) SQNativeClosure +{ + void* vftable; + int uiRef; + unsigned char gap_C[4]; + long long value_10; + long long value_18; + long long value_20; + SQSharedState* sharedState; + char unknown_30; + unsigned char padding_34[7]; + long long value_38; + long long value_40; + long long value_48; + long long value_50; + long long value_58; + SQObjectType _nameType; + SQString* _name; + long long value_70; + long long value_78; + unsigned char justInCaseGap_80[300]; +}; + +/* 162 */ +struct SQArray +{ + void* vftable; + int uiRef; + unsigned char gap_24[36]; + SQObject* _values; + int _usedSlots; + int _allocated; +}; + +/* 129 */ +struct alignas(8) HSquirrelVM +{ + void* vftable; + int uiRef; + unsigned char gap_8[12]; + void* _toString; + void* _roottable_pointer; + void* pointer_28; + CallInfo* ci; + CallInfo* _callstack; + int _callsstacksize; + int _stackbase; + SQObject* _stackOfCurrentFunction; + SQSharedState* sharedState; + void* pointer_58; + void* pointer_60; + int _top; + SQObject* _stack; + unsigned char gap_78[8]; + SQObject* _vargvstack; + unsigned char gap_88[8]; + SQObject temp_reg; + unsigned char gapA0[8]; + void* pointer_A8; + unsigned char gap_B0[8]; + SQObject _roottable_object; + SQObject _lasterror; + SQObject _errorHandler; + long long field_E8; + int traps; + unsigned char gap_F4[12]; + int _nnativecalls; + int _suspended; + int _suspended_root; + int _callstacksize; + int _suspended_target; + int trapAmount; + int _suspend_varargs; + int unknown_field_11C; + SQObject object_120; +}; + +/* 150 */ +struct SQStructInstance +{ + void* vftable; + __int32 uiRef; + BYTE gap_C[4]; + __int64 unknown_10; + void* pointer_18; + __int64 unknown_20; + SQSharedState* _sharedState; + unsigned int size; + BYTE gap_34[4]; + SQObject data[1]; // This struct is dynamically sized, so this size is unknown +}; + +/* 148 */ +struct SQSharedState +{ + unsigned char gap_0[72]; + void* unknown; + unsigned char gap_50[16344]; + SQObjectType _unknownTableType00; + long long _unknownTableValue00; + unsigned char gap_4038[16]; + StringTable* _stringTable; + unsigned char gap_4050[32]; + SQObjectType _unknownTableType0; + long long _unknownTableValue0; + SQObjectType _unknownObjectType1; + long long _unknownObjectValue1; + unsigned char gap_4090[8]; + SQObjectType _unknownArrayType2; + long long _unknownArrayValue2; + SQObjectType _gobalsArrayType; + SQStructInstance* _globalsArray; + unsigned char gap_40B8[16]; + SQObjectType _nativeClosuresType; + SQTable* _nativeClosures; + SQObjectType _typedConstantsType; + SQTable* _typedConstants; + SQObjectType _untypedConstantsType; + SQTable* _untypedConstants; + SQObjectType _globalsMaybeType; + SQTable* _globals; + SQObjectType _functionsType; + SQTable* _functions; + SQObjectType _structsType; + SQTable* _structs; + SQObjectType _typeDefsType; + SQTable* _typeDefs; + SQObjectType unknownTableType; + SQTable* unknownTable; + SQObjectType _squirrelFilesType; + SQTable* _squirrelFiles; + unsigned char gap_4158[80]; + SQObjectType _nativeClosures2Type; + SQTable* _nativeClosures2; + SQObjectType _entityTypesMaybeType; + SQTable* _entityTypesMaybe; + SQObjectType unknownTable2Type; + SQTable* unknownTable2; + unsigned char gap_41D8[72]; + SQObjectType _compilerKeywordsType; + SQTable* _compilerKeywords; + HSquirrelVM* _currentThreadMaybe; + unsigned char gap_4238[8]; + SQObjectType unknownTable3Type; + SQTable* unknownTable3; + unsigned char gap_4250[16]; + SQObjectType unknownThreadType; + SQTable* unknownThread; + SQObjectType _tableNativeFunctionsType; + SQTable* _tableNativeFunctions; + SQObjectType _unknownTableType4; + long long _unknownObjectValue4; + SQObjectType _unknownObjectType5; + long long _unknownObjectValue5; + SQObjectType _unknownObjectType6; + long long _unknownObjectValue6; + SQObjectType _unknownObjectType7; + long long _unknownObjectValue7; + SQObjectType _unknownObjectType8; + long long _unknownObjectValue8; + SQObjectType _unknownObjectType9; + long long _unknownObjectValue9; + SQObjectType _unknownObjectType10; + long long _unknownObjectValue10; + SQObjectType _unknownObjectType11; + long long _unknownObjectValue11; + SQObjectType _unknownObjectType12; + long long _unknownObjectValue12; + SQObjectType _unknownObjectType13; + long long _unknownObjectValue13; + SQObjectType _unknownObjectType14; + long long _unknownObjectValue14; + SQObjectType _unknownObjectType15; + long long _unknownObjectValue15; + unsigned char gap_4340[16]; + void* printFunction; + unsigned char gap_4358[16]; + void* logEntityFunction; + unsigned char gap_4370[40]; + SQObjectType _waitStringType; + SQString* _waitStringValue; + SQObjectType _SpinOffAndWaitForStringType; + SQString* _SpinOffAndWaitForStringValue; + SQObjectType _SpinOffAndWaitForSoloStringType; + SQString* _SpinOffAndWaitForSoloStringValue; + SQObjectType _SpinOffStringType; + SQString* _SpinOffStringValue; + SQObjectType _SpinOffDelayedStringType; + SQString* _SpinOffDelayedStringValue; + unsigned char gap_43E8[8]; + bool enableDebugInfo; // functionality stripped + unsigned char gap_43F1[23]; +}; + +/* 165 */ +struct tableNode +{ + SQObject val; + SQObject key; + tableNode* next; +}; + +/* 136 */ +struct alignas(8) CallInfo +{ + long long ip; + SQObject* _literals; + SQObject obj10; + SQObject closure; + int _etraps[4]; + int _root; + short _vargs_size; + short _vargs_base; + unsigned char gap[16]; +}; + +/* 149 */ +struct StringTable +{ + unsigned char gap_0[12]; + int _numofslots; + unsigned char gap_10[200]; +}; + +/* 141 */ +struct alignas(8) SQStackInfos +{ + char* _name; + char* _sourceName; + int _line; +}; + +/* 151 */ +struct alignas(4) SQInstruction +{ + int op; + int arg1; + int output; + short arg2; + short arg3; +}; + +/* 154 */ +struct SQLexer +{ + unsigned char gap_0[112]; +}; + +/* 153 */ +struct SQCompiler +{ + unsigned char gap_0[4]; + int _token; + unsigned char gap_8[8]; + SQObject object_10; + SQLexer lexer; + unsigned char gap_90[752]; + HSquirrelVM* sqvm; + unsigned char gap_288[8]; +}; + +/* 155 */ +struct CSquirrelVM +{ + unsigned char gap_0[8]; + HSquirrelVM* sqvm; + unsigned char gap_10[44]; + int loadEnumFromFileMaybe; + unsigned char gap_40[200]; +}; + +struct SQUserData +{ + void* vftable; + int uiRef; + char gap_12[4]; + long long unknown_10; + long long unknown_18; + long long unknown_20; + long long sharedState; + long long unknown_30; + int size; + char padding1[4]; + releasehookType releaseHook; + long long typeId; + char data[1]; +}; diff --git a/NorthstarDLL/squirrelautobind.cpp b/NorthstarDLL/squirrelautobind.cpp deleted file mode 100644 index d5f42477..00000000 --- a/NorthstarDLL/squirrelautobind.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "pch.h" -#include "squirrelautobind.h" - -SquirrelAutoBindContainer* g_pSqAutoBindContainer; - -ON_DLL_LOAD_RELIESON("client.dll", ClientSquirrelAutoBind, ClientSquirrel, (CModule module)) -{ - spdlog::info("ClientSquirrelAutoBInd AutoBindFuncsVectorsize {}", g_pSqAutoBindContainer->clientSqAutoBindFuncs.size()); - for (auto& autoBindFunc : g_pSqAutoBindContainer->clientSqAutoBindFuncs) - { - autoBindFunc(); - } -} - -ON_DLL_LOAD_RELIESON("server.dll", ServerSquirrelAutoBind, ServerSquirrel, (CModule module)) -{ - for (auto& autoBindFunc : g_pSqAutoBindContainer->serverSqAutoBindFuncs) - { - autoBindFunc(); - } -} diff --git a/NorthstarDLL/squirrelautobind.h b/NorthstarDLL/squirrelautobind.h deleted file mode 100644 index 865479a1..00000000 --- a/NorthstarDLL/squirrelautobind.h +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once -#include - -typedef void (*SqAutoBindFunc)(); - -class SquirrelAutoBindContainer -{ - public: - std::vector> clientSqAutoBindFuncs; - std::vector> serverSqAutoBindFuncs; -}; - -extern SquirrelAutoBindContainer* g_pSqAutoBindContainer; - -class __squirrelautobind; - -#define ADD_SQFUNC(returnType, funcName, argTypes, helpText, runOnContext) \ - template SQRESULT CONCAT2(Script_, funcName)(HSquirrelVM * sqvm); \ - namespace \ - { \ - __squirrelautobind CONCAT2(__squirrelautobind, __LINE__)( \ - []() \ - { \ - if constexpr (runOnContext & ScriptContext::UI) \ - g_pSquirrel->AddFuncRegistration( \ - returnType, __STR(funcName), argTypes, helpText, CONCAT2(Script_, funcName) < ScriptContext::UI >); \ - if constexpr (runOnContext & ScriptContext::CLIENT) \ - g_pSquirrel->AddFuncRegistration( \ - returnType, __STR(funcName), argTypes, helpText, CONCAT2(Script_, funcName) < ScriptContext::CLIENT >); \ - }, \ - []() \ - { \ - if constexpr (runOnContext & ScriptContext::SERVER) \ - g_pSquirrel->AddFuncRegistration( \ - returnType, __STR(funcName), argTypes, helpText, CONCAT2(Script_, funcName) < ScriptContext::SERVER >); \ - }); \ - } \ - template SQRESULT CONCAT2(Script_, funcName)(HSquirrelVM * sqvm) - -#define REPLACE_SQFUNC(funcName, runOnContext) \ - template SQRESULT CONCAT2(Script_, funcName)(HSquirrelVM * sqvm); \ - namespace \ - { \ - __squirrelautobind CONCAT2(__squirrelautobind, __LINE__)( \ - []() \ - { \ - if constexpr (runOnContext & ScriptContext::UI) \ - g_pSquirrel->AddFuncOverride(__STR(funcName), CONCAT2(Script_, funcName) < ScriptContext::UI >); \ - if constexpr (runOnContext & ScriptContext::CLIENT) \ - g_pSquirrel->AddFuncOverride( \ - __STR(funcName), CONCAT2(Script_, funcName) < ScriptContext::CLIENT >); \ - }, \ - []() \ - { \ - if constexpr (runOnContext & ScriptContext::SERVER) \ - g_pSquirrel->AddFuncOverride( \ - __STR(funcName), CONCAT2(Script_, funcName) < ScriptContext::SERVER >); \ - }); \ - } \ - template SQRESULT CONCAT2(Script_, funcName)(HSquirrelVM * sqvm) - -class __squirrelautobind -{ - public: - __squirrelautobind() = delete; - - __squirrelautobind(std::function clientAutoBindFunc, std::function serverAutoBindFunc) - { - // Bit hacky but we can't initialise this normally since this gets run automatically on load - if (g_pSqAutoBindContainer == nullptr) - g_pSqAutoBindContainer = new SquirrelAutoBindContainer(); - - g_pSqAutoBindContainer->clientSqAutoBindFuncs.push_back(clientAutoBindFunc); - g_pSqAutoBindContainer->serverSqAutoBindFuncs.push_back(serverAutoBindFunc); - } -}; diff --git a/NorthstarDLL/squirrelclasstypes.h b/NorthstarDLL/squirrelclasstypes.h deleted file mode 100644 index e26bc8d0..00000000 --- a/NorthstarDLL/squirrelclasstypes.h +++ /dev/null @@ -1,239 +0,0 @@ -#pragma once -#include "pch.h" -#include "squirreldatatypes.h" - -#include - -enum SQRESULT : SQInteger -{ - SQRESULT_ERROR = -1, - SQRESULT_NULL = 0, - SQRESULT_NOTNULL = 1, -}; - -typedef SQRESULT (*SQFunction)(HSquirrelVM* sqvm); - -enum class eSQReturnType -{ - Float = 0x1, - Vector = 0x3, - Integer = 0x5, - Boolean = 0x6, - Entity = 0xD, - String = 0x21, - Default = 0x20, - Arrays = 0x25, - Asset = 0x28, - Table = 0x26, -}; - -const std::map PrintSQRESULT = { - {SQRESULT::SQRESULT_ERROR, "SQRESULT_ERROR"}, - {SQRESULT::SQRESULT_NULL, "SQRESULT_NULL"}, - {SQRESULT::SQRESULT_NOTNULL, "SQRESULT_NOTNULL"}}; - -struct CompileBufferState -{ - const SQChar* buffer; - const SQChar* bufferPlusLength; - const SQChar* bufferAgain; - - CompileBufferState(const std::string& code) - { - buffer = code.c_str(); - bufferPlusLength = code.c_str() + code.size(); - bufferAgain = code.c_str(); - } -}; - -struct SQFuncRegistration -{ - const char* squirrelFuncName; - const char* cppFuncName; - const char* helpText; - const char* returnTypeString; - const char* argTypes; - uint32_t unknown1; - uint32_t devLevel; - const char* shortNameMaybe; - uint32_t unknown2; - eSQReturnType returnType; - uint32_t* externalBufferPointer; - uint64_t externalBufferSize; - uint64_t unknown3; - uint64_t unknown4; - SQFunction funcPtr; - - SQFuncRegistration() - { - memset(this, 0, sizeof(SQFuncRegistration)); - this->returnType = eSQReturnType::Default; - } -}; - -enum class ScriptContext : int -{ - SERVER, - CLIENT, - UI, -}; - -typedef std::vector> FunctionVector; -typedef std::function VoidFunction; - -// clang-format off -template -concept is_map = - // Simple maps - std::same_as> || - std::same_as> || - - // Nested maps - std::same_as < - std::map, - std::map - > || - std::same_as < - std::unordered_map, - std::unordered_map - > || - std::same_as < - std::unordered_map, - std::map - > || - std::same_as < - std::map, - std::unordered_map - > -; - -template -concept is_iterable = requires(std::ranges::range_value_t x) -{ - x.begin(); // must have `x.begin()` - x.end(); // and `x.end()` -}; - -// clang-format on - -typedef void (*SquirrelMessage_External_Pop)(HSquirrelVM* sqvm); -typedef void (*sq_schedule_call_externalType)(ScriptContext context, const char* funcname, SquirrelMessage_External_Pop function); - -class SquirrelMessage -{ - public: - std::string functionName; - FunctionVector args; - bool isExternal = false; - SquirrelMessage_External_Pop externalFunc = NULL; -}; - -class SquirrelMessageBuffer -{ - - private: - std::queue messages = {}; - - public: - std::mutex mutex; - std::optional pop() - { - std::lock_guard guard(mutex); - if (!messages.empty()) - { - auto message = messages.front(); - messages.pop(); - return message; - } - else - { - return std::nullopt; - } - } - - void unwind() - { - auto maybeMessage = this->pop(); - if (!maybeMessage) - { - spdlog::error("Plugin tried consuming SquirrelMessage while buffer was empty"); - return; - } - auto message = maybeMessage.value(); - for (auto& v : message.args) - { - // Execute lambda to push arg to stack - v(); - } - } - - void push(SquirrelMessage message) - { - std::lock_guard guard(mutex); - messages.push(message); - } -}; - -// Super simple wrapper class to allow pushing Assets via call -class SquirrelAsset -{ - public: - std::string path; - SquirrelAsset(std::string path) : path(path) {}; -}; - -#pragma region TypeDefs - -// core sqvm funcs -typedef int64_t (*RegisterSquirrelFuncType)(CSquirrelVM* sqvm, SQFuncRegistration* funcReg, char unknown); -typedef void (*sq_defconstType)(CSquirrelVM* sqvm, const SQChar* name, int value); - -typedef SQRESULT (*sq_compilebufferType)( - HSquirrelVM* sqvm, CompileBufferState* compileBuffer, const char* file, int a1, SQBool bShouldThrowError); -typedef SQRESULT (*sq_callType)(HSquirrelVM* sqvm, SQInteger iArgs, SQBool bShouldReturn, SQBool bThrowError); -typedef SQInteger (*sq_raiseerrorType)(HSquirrelVM* sqvm, const SQChar* pError); - -// sq stack array funcs -typedef void (*sq_newarrayType)(HSquirrelVM* sqvm, SQInteger iStackpos); -typedef SQRESULT (*sq_arrayappendType)(HSquirrelVM* sqvm, SQInteger iStackpos); - -// sq table funcs -typedef SQRESULT (*sq_newtableType)(HSquirrelVM* sqvm); -typedef SQRESULT (*sq_newslotType)(HSquirrelVM* sqvm, SQInteger idx, SQBool bStatic); - -// sq stack push funcs -typedef void (*sq_pushroottableType)(HSquirrelVM* sqvm); -typedef void (*sq_pushstringType)(HSquirrelVM* sqvm, const SQChar* pStr, SQInteger iLength); -typedef void (*sq_pushintegerType)(HSquirrelVM* sqvm, SQInteger i); -typedef void (*sq_pushfloatType)(HSquirrelVM* sqvm, SQFloat f); -typedef void (*sq_pushboolType)(HSquirrelVM* sqvm, SQBool b); -typedef void (*sq_pushassetType)(HSquirrelVM* sqvm, const SQChar* str, SQInteger iLength); -typedef void (*sq_pushvectorType)(HSquirrelVM* sqvm, const SQFloat* pVec); -typedef void (*sq_pushobjectType)(HSquirrelVM* sqvm, SQObject* pVec); - -// sq stack get funcs -typedef const SQChar* (*sq_getstringType)(HSquirrelVM* sqvm, SQInteger iStackpos); -typedef SQInteger (*sq_getintegerType)(HSquirrelVM* sqvm, SQInteger iStackpos); -typedef SQFloat (*sq_getfloatType)(HSquirrelVM*, SQInteger iStackpos); -typedef SQBool (*sq_getboolType)(HSquirrelVM*, SQInteger iStackpos); -typedef SQRESULT (*sq_getType)(HSquirrelVM* sqvm, SQInteger iStackpos); -typedef SQRESULT (*sq_getassetType)(HSquirrelVM* sqvm, SQInteger iStackpos, const char** pResult); -typedef SQRESULT (*sq_getuserdataType)(HSquirrelVM* sqvm, SQInteger iStackpos, void** pData, uint64_t* pTypeId); -typedef SQFloat* (*sq_getvectorType)(HSquirrelVM* sqvm, SQInteger iStackpos); -typedef SQBool (*sq_getthisentityType)(HSquirrelVM*, void** ppEntity); -typedef void (*sq_getobjectType)(HSquirrelVM*, SQInteger iStackPos, SQObject* pOutObj); - -// sq stack userpointer funcs -typedef void* (*sq_createuserdataType)(HSquirrelVM* sqvm, SQInteger iSize); -typedef SQRESULT (*sq_setuserdatatypeidType)(HSquirrelVM* sqvm, SQInteger iStackpos, uint64_t iTypeId); - -// sq misc entity funcs -typedef void* (*sq_getentityfrominstanceType)(CSquirrelVM* sqvm, SQObject* pInstance, char** ppEntityConstant); -typedef char** (*sq_GetEntityConstantType)(); - -typedef int (*sq_getfunctionType)(HSquirrelVM* sqvm, const char* name, SQObject* returnObj, const char* signature); - -#pragma endregion - -// These "external" versions of the types are for plugins -typedef int64_t (*RegisterSquirrelFuncType_External)(ScriptContext context, SQFuncRegistration* funcReg, char unknown); diff --git a/NorthstarDLL/squirreldatatypes.h b/NorthstarDLL/squirreldatatypes.h deleted file mode 100644 index e9f88d08..00000000 --- a/NorthstarDLL/squirreldatatypes.h +++ /dev/null @@ -1,495 +0,0 @@ -#pragma once -/* - This file has been generated by IDA. - It contains local type definitions from - the type library 'server.dll' -*/ - -struct HSquirrelVM; -struct CallInfo; -struct SQTable; -struct SQString; -struct SQFunctionProto; -struct SQClosure; -struct SQSharedState; -struct StringTable; -struct SQStructInstance; -struct SQStructDef; -struct SQNativeClosure; -struct SQArray; -struct tableNode; -struct SQUserData; - -typedef void (*releasehookType)(void* val, int size); - -// stolen from ttf2sdk: sqvm types -typedef float SQFloat; -typedef long SQInteger; -typedef unsigned long SQUnsignedInteger; -typedef char SQChar; -typedef SQUnsignedInteger SQBool; - -/* 127 */ -enum SQObjectType : int -{ - _RT_NULL = 0x1, - _RT_INTEGER = 0x2, - _RT_FLOAT = 0x4, - _RT_BOOL = 0x8, - _RT_STRING = 0x10, - _RT_TABLE = 0x20, - _RT_ARRAY = 0x40, - _RT_USERDATA = 0x80, - _RT_CLOSURE = 0x100, - _RT_NATIVECLOSURE = 0x200, - _RT_GENERATOR = 0x400, - OT_USERPOINTER = 0x800, - _RT_USERPOINTER = 0x800, - _RT_THREAD = 0x1000, - _RT_FUNCPROTO = 0x2000, - _RT_CLASS = 0x4000, - _RT_INSTANCE = 0x8000, - _RT_WEAKREF = 0x10000, - OT_VECTOR = 0x40000, - SQOBJECT_CANBEFALSE = 0x1000000, - OT_NULL = 0x1000001, - OT_BOOL = 0x1000008, - SQOBJECT_DELEGABLE = 0x2000000, - SQOBJECT_NUMERIC = 0x4000000, - OT_INTEGER = 0x5000002, - OT_FLOAT = 0x5000004, - SQOBJECT_REF_COUNTED = 0x8000000, - OT_STRING = 0x8000010, - OT_ARRAY = 0x8000040, - OT_CLOSURE = 0x8000100, - OT_NATIVECLOSURE = 0x8000200, - OT_ASSET = 0x8000400, - OT_THREAD = 0x8001000, - OT_FUNCPROTO = 0x8002000, - OT_CLAAS = 0x8004000, - OT_STRUCT = 0x8200000, - OT_WEAKREF = 0x8010000, - OT_TABLE = 0xA000020, - OT_USERDATA = 0xA000080, - OT_INSTANCE = 0xA008000, - OT_ENTITY = 0xA400000, -}; - -/* 156 */ -union SQObjectValue -{ - SQString* asString; - SQTable* asTable; - SQClosure* asClosure; - SQFunctionProto* asFuncProto; - SQStructDef* asStructDef; - long long as64Integer; - SQNativeClosure* asNativeClosure; - SQArray* asArray; - HSquirrelVM* asThread; - float asFloat; - int asInteger; - SQUserData* asUserdata; - SQStructInstance* asStructInstance; -}; - -/* 160 */ -struct SQVector -{ - SQObjectType _Type; - float x; - float y; - float z; -}; - -/* 128 */ -struct SQObject -{ - SQObjectType _Type; - int structNumber; - SQObjectValue _VAL; -}; - -/* 138 */ -struct alignas(8) SQString -{ - void* vftable; - int uiRef; - int padding; - SQString* _next_maybe; - SQSharedState* sharedState; - int length; - unsigned char gap_24[4]; - char _hash[8]; - char _val[1]; -}; - -/* 137 */ -struct alignas(8) SQTable -{ - void* vftable; - unsigned char gap_08[4]; - int uiRef; - unsigned char gap_10[8]; - void* pointer_18; - void* pointer_20; - void* _sharedState; - long long field_30; - tableNode* _nodes; - int _numOfNodes; - int size; - int field_48; - int _usedNodes; - unsigned char _gap_50[20]; - int field_64; - unsigned char _gap_68[80]; -}; - -/* 140 */ -struct alignas(8) SQClosure -{ - void* vftable; - unsigned char gap_08[4]; - int uiRef; - void* pointer_10; - void* pointer_18; - void* pointer_20; - void* sharedState; - SQObject obj_30; - SQObject _function; - SQObject* _outervalues; - unsigned char gap_58[8]; - unsigned char gap_60[96]; - SQObject* objectPointer_C0; - unsigned char gap_C8[16]; -}; - -/* 139 */ -struct alignas(8) SQFunctionProto -{ - void* vftable; - unsigned char gap_08[4]; - int uiRef; - unsigned char gap_10[8]; - void* pointer_18; - void* pointer_20; - void* sharedState; - void* pointer_30; - SQObjectType _fileNameType; - SQString* _fileName; - SQObjectType _funcNameType; - SQString* _funcName; - SQObject obj_58; - unsigned char gap_68[12]; - int _stacksize; - unsigned char gap_78[48]; - int nParameters; - unsigned char gap_AC[60]; - int nDefaultParams; - unsigned char gap_EC[200]; -}; - -/* 152 */ -struct SQStructDef -{ - void* vtable; - int uiRef; - unsigned char padding_C[4]; - unsigned char unknown[24]; - SQSharedState* sharedState; - SQObjectType _nameType; - SQString* _name; - unsigned char gap_38[16]; - SQObjectType _variableNamesType; - SQTable* _variableNames; - unsigned char gap_[32]; -}; - -/* 157 */ -struct alignas(8) SQNativeClosure -{ - void* vftable; - int uiRef; - unsigned char gap_C[4]; - long long value_10; - long long value_18; - long long value_20; - SQSharedState* sharedState; - char unknown_30; - unsigned char padding_34[7]; - long long value_38; - long long value_40; - long long value_48; - long long value_50; - long long value_58; - SQObjectType _nameType; - SQString* _name; - long long value_70; - long long value_78; - unsigned char justInCaseGap_80[300]; -}; - -/* 162 */ -struct SQArray -{ - void* vftable; - int uiRef; - unsigned char gap_24[36]; - SQObject* _values; - int _usedSlots; - int _allocated; -}; - -/* 129 */ -struct alignas(8) HSquirrelVM -{ - void* vftable; - int uiRef; - unsigned char gap_8[12]; - void* _toString; - void* _roottable_pointer; - void* pointer_28; - CallInfo* ci; - CallInfo* _callstack; - int _callsstacksize; - int _stackbase; - SQObject* _stackOfCurrentFunction; - SQSharedState* sharedState; - void* pointer_58; - void* pointer_60; - int _top; - SQObject* _stack; - unsigned char gap_78[8]; - SQObject* _vargvstack; - unsigned char gap_88[8]; - SQObject temp_reg; - unsigned char gapA0[8]; - void* pointer_A8; - unsigned char gap_B0[8]; - SQObject _roottable_object; - SQObject _lasterror; - SQObject _errorHandler; - long long field_E8; - int traps; - unsigned char gap_F4[12]; - int _nnativecalls; - int _suspended; - int _suspended_root; - int _callstacksize; - int _suspended_target; - int trapAmount; - int _suspend_varargs; - int unknown_field_11C; - SQObject object_120; -}; - -/* 150 */ -struct SQStructInstance -{ - void* vftable; - __int32 uiRef; - BYTE gap_C[4]; - __int64 unknown_10; - void* pointer_18; - __int64 unknown_20; - SQSharedState* _sharedState; - unsigned int size; - BYTE gap_34[4]; - SQObject data[1]; // This struct is dynamically sized, so this size is unknown -}; - -/* 148 */ -struct SQSharedState -{ - unsigned char gap_0[72]; - void* unknown; - unsigned char gap_50[16344]; - SQObjectType _unknownTableType00; - long long _unknownTableValue00; - unsigned char gap_4038[16]; - StringTable* _stringTable; - unsigned char gap_4050[32]; - SQObjectType _unknownTableType0; - long long _unknownTableValue0; - SQObjectType _unknownObjectType1; - long long _unknownObjectValue1; - unsigned char gap_4090[8]; - SQObjectType _unknownArrayType2; - long long _unknownArrayValue2; - SQObjectType _gobalsArrayType; - SQStructInstance* _globalsArray; - unsigned char gap_40B8[16]; - SQObjectType _nativeClosuresType; - SQTable* _nativeClosures; - SQObjectType _typedConstantsType; - SQTable* _typedConstants; - SQObjectType _untypedConstantsType; - SQTable* _untypedConstants; - SQObjectType _globalsMaybeType; - SQTable* _globals; - SQObjectType _functionsType; - SQTable* _functions; - SQObjectType _structsType; - SQTable* _structs; - SQObjectType _typeDefsType; - SQTable* _typeDefs; - SQObjectType unknownTableType; - SQTable* unknownTable; - SQObjectType _squirrelFilesType; - SQTable* _squirrelFiles; - unsigned char gap_4158[80]; - SQObjectType _nativeClosures2Type; - SQTable* _nativeClosures2; - SQObjectType _entityTypesMaybeType; - SQTable* _entityTypesMaybe; - SQObjectType unknownTable2Type; - SQTable* unknownTable2; - unsigned char gap_41D8[72]; - SQObjectType _compilerKeywordsType; - SQTable* _compilerKeywords; - HSquirrelVM* _currentThreadMaybe; - unsigned char gap_4238[8]; - SQObjectType unknownTable3Type; - SQTable* unknownTable3; - unsigned char gap_4250[16]; - SQObjectType unknownThreadType; - SQTable* unknownThread; - SQObjectType _tableNativeFunctionsType; - SQTable* _tableNativeFunctions; - SQObjectType _unknownTableType4; - long long _unknownObjectValue4; - SQObjectType _unknownObjectType5; - long long _unknownObjectValue5; - SQObjectType _unknownObjectType6; - long long _unknownObjectValue6; - SQObjectType _unknownObjectType7; - long long _unknownObjectValue7; - SQObjectType _unknownObjectType8; - long long _unknownObjectValue8; - SQObjectType _unknownObjectType9; - long long _unknownObjectValue9; - SQObjectType _unknownObjectType10; - long long _unknownObjectValue10; - SQObjectType _unknownObjectType11; - long long _unknownObjectValue11; - SQObjectType _unknownObjectType12; - long long _unknownObjectValue12; - SQObjectType _unknownObjectType13; - long long _unknownObjectValue13; - SQObjectType _unknownObjectType14; - long long _unknownObjectValue14; - SQObjectType _unknownObjectType15; - long long _unknownObjectValue15; - unsigned char gap_4340[16]; - void* printFunction; - unsigned char gap_4358[16]; - void* logEntityFunction; - unsigned char gap_4370[40]; - SQObjectType _waitStringType; - SQString* _waitStringValue; - SQObjectType _SpinOffAndWaitForStringType; - SQString* _SpinOffAndWaitForStringValue; - SQObjectType _SpinOffAndWaitForSoloStringType; - SQString* _SpinOffAndWaitForSoloStringValue; - SQObjectType _SpinOffStringType; - SQString* _SpinOffStringValue; - SQObjectType _SpinOffDelayedStringType; - SQString* _SpinOffDelayedStringValue; - unsigned char gap_43E8[8]; - bool enableDebugInfo; // functionality stripped - unsigned char gap_43F1[23]; -}; - -/* 165 */ -struct tableNode -{ - SQObject val; - SQObject key; - tableNode* next; -}; - -/* 136 */ -struct alignas(8) CallInfo -{ - long long ip; - SQObject* _literals; - SQObject obj10; - SQObject closure; - int _etraps[4]; - int _root; - short _vargs_size; - short _vargs_base; - unsigned char gap[16]; -}; - -/* 149 */ -struct StringTable -{ - unsigned char gap_0[12]; - int _numofslots; - unsigned char gap_10[200]; -}; - -/* 141 */ -struct alignas(8) SQStackInfos -{ - char* _name; - char* _sourceName; - int _line; -}; - -/* 151 */ -struct alignas(4) SQInstruction -{ - int op; - int arg1; - int output; - short arg2; - short arg3; -}; - -/* 154 */ -struct SQLexer -{ - unsigned char gap_0[112]; -}; - -/* 153 */ -struct SQCompiler -{ - unsigned char gap_0[4]; - int _token; - unsigned char gap_8[8]; - SQObject object_10; - SQLexer lexer; - unsigned char gap_90[752]; - HSquirrelVM* sqvm; - unsigned char gap_288[8]; -}; - -/* 155 */ -struct CSquirrelVM -{ - unsigned char gap_0[8]; - HSquirrelVM* sqvm; - unsigned char gap_10[44]; - int loadEnumFromFileMaybe; - unsigned char gap_40[200]; -}; - -struct SQUserData -{ - void* vftable; - int uiRef; - char gap_12[4]; - long long unknown_10; - long long unknown_18; - long long unknown_20; - long long sharedState; - long long unknown_30; - int size; - char padding1[4]; - releasehookType releaseHook; - long long typeId; - char data[1]; -}; diff --git a/NorthstarDLL/structs.h b/NorthstarDLL/structs.h deleted file mode 100644 index 939fd302..00000000 --- a/NorthstarDLL/structs.h +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once -//clang-format off -// About this file: -// This file contains several macros used to define reversed structs -// The reason we use these macros is to make it easier to update existing structs -// when new fields are reversed -// This means we dont have to manually add padding, and recalculate when updating - -// Technical note: -// While functionally, these structs act like a regular struct, they are actually -// defined as unions with anonymous structs in them. -// This means that each field is essentially an offset into a union. -// We acknowledge that this goes against C++'s active-member guideline for unions -// However, this is not such a big deal here as these structs will not be constructed - -// Usage: -// To use these macros, define a struct like so: -/* -OFFSET_STRUCT(Name) -{ - STRUCT_SIZE(0x100) // Total in-memory struct size - FIELD(0x0, int field) // offset, signature -} -*/ - -#define OFFSET_STRUCT(name) union name -#define STRUCT_SIZE(size) char __size[size]; -#define STRUCT_FIELD_OFFSET(offset, signature) \ - struct \ - { \ - char CONCAT2(pad, __LINE__)[offset]; \ - signature; \ - }; - -// Special case for a 0-offset field -#define STRUCT_FIELD_NOOFFSET(offset, signature) signature; - -// Based on: https://stackoverflow.com/questions/11632219/c-preprocessor-macro-specialisation-based-on-an-argument -// Yes, this is hacky, but it works quite well actually -// This basically makes sure that when the offset is 0x0, no padding field gets generated -#define OFFSET_0x0 () - -#define IIF(c) CONCAT2(IIF_, c) -#define IIF_0(t, ...) __VA_ARGS__ -#define IIF_1(t, ...) t - -#define PROBE(x) x, 1 - -#define MSVC_VA_ARGS_WORKAROUND(define, args) define args -#define CHECK(...) MSVC_VA_ARGS_WORKAROUND(CHECK_N, (__VA_ARGS__, 0)) -#define CHECK_N(x, n, ...) n - -#define DO_PROBE(offset) PROBE_PROXY(OFFSET_##offset) // concatenate prefix with offset -#define PROBE_PROXY(...) PROBE_PRIMITIVE(__VA_ARGS__) // expand arguments -#define PROBE_PRIMITIVE(x) PROBE_COMBINE_##x // merge -#define PROBE_COMBINE_(...) PROBE(~) // if merge successful, expand to probe - -#define IS_0(offset) CHECK(DO_PROBE(offset)) - -#define FIELD(offset, signature) IIF(IS_0(offset))(STRUCT_FIELD_NOOFFSET, STRUCT_FIELD_OFFSET)(offset, signature) -#define FIELDS FIELD - -//clang-format on diff --git a/NorthstarDLL/tier0.cpp b/NorthstarDLL/tier0.cpp deleted file mode 100644 index 61ad7783..00000000 --- a/NorthstarDLL/tier0.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "pch.h" -#include "tier0.h" - -// use the Tier0 namespace for tier0 funcs -namespace Tier0 -{ - IMemAlloc* g_pMemAllocSingleton; - - ErrorType Error; - CommandLineType CommandLine; - Plat_FloatTimeType Plat_FloatTime; - ThreadInServerFrameThreadType ThreadInServerFrameThread; -} // namespace Tier0 - -typedef Tier0::IMemAlloc* (*CreateGlobalMemAllocType)(); -CreateGlobalMemAllocType CreateGlobalMemAlloc; - -// needs to be a seperate function, since memalloc.cpp calls it -void TryCreateGlobalMemAlloc() -{ - // init memalloc stuff - CreateGlobalMemAlloc = - reinterpret_cast(GetProcAddress(GetModuleHandleA("tier0.dll"), "CreateGlobalMemAlloc")); - Tier0::g_pMemAllocSingleton = CreateGlobalMemAlloc(); // if it already exists, this returns the preexisting IMemAlloc instance -} - -ON_DLL_LOAD("tier0.dll", Tier0GameFuncs, (CModule module)) -{ - // shouldn't be necessary, but do this just in case - TryCreateGlobalMemAlloc(); - - // setup tier0 funcs - Tier0::Error = module.GetExport("Error").As(); - Tier0::CommandLine = module.GetExport("CommandLine").As(); - Tier0::Plat_FloatTime = module.GetExport("Plat_FloatTime").As(); - Tier0::ThreadInServerFrameThread = module.GetExport("ThreadInServerFrameThread").As(); -} diff --git a/NorthstarDLL/tier0.h b/NorthstarDLL/tier0.h deleted file mode 100644 index 92a63027..00000000 --- a/NorthstarDLL/tier0.h +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once -namespace Tier0 -{ - 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; - }; - - 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 {} - }; - - extern IMemAlloc* g_pMemAllocSingleton; - - 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; -} // namespace Tier0 - -void TryCreateGlobalMemAlloc(); diff --git a/NorthstarDLL/util/printcommands.cpp b/NorthstarDLL/util/printcommands.cpp new file mode 100644 index 00000000..5552c50c --- /dev/null +++ b/NorthstarDLL/util/printcommands.cpp @@ -0,0 +1,174 @@ +#include "pch.h" +#include "printcommands.h" +#include "core/convar/convar.h" +#include "core/convar/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 "); + 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 [...]"); + 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 "); + 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; +} diff --git a/NorthstarDLL/util/printcommands.h b/NorthstarDLL/util/printcommands.h new file mode 100644 index 00000000..cb72e5cc --- /dev/null +++ b/NorthstarDLL/util/printcommands.h @@ -0,0 +1,6 @@ +#pragma once +#include "core/convar/concommand.h" + +void PrintCommandHelpDialogue(const ConCommandBase* command, const char* name); +void TryPrintCvarHelpForCommand(const char* pCommand); +void InitialiseCommandPrint(); diff --git a/NorthstarDLL/util/printmaps.cpp b/NorthstarDLL/util/printmaps.cpp new file mode 100644 index 00000000..e0192d69 --- /dev/null +++ b/NorthstarDLL/util/printmaps.cpp @@ -0,0 +1,168 @@ +#include "pch.h" +#include "printmaps.h" +#include "core/convar/convar.h" +#include "core/convar/concommand.h" +#include "mods/modmanager.h" +#include "core/tier0.h" +#include "engine/r2engine.h" + +#include +#include + +AUTOHOOK_INIT() + +enum class MapSource_t +{ + VPK, + GAMEDIR, + MOD +}; + +const std::unordered_map 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 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 "); + 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; +} diff --git a/NorthstarDLL/util/printmaps.h b/NorthstarDLL/util/printmaps.h new file mode 100644 index 00000000..b01761c0 --- /dev/null +++ b/NorthstarDLL/util/printmaps.h @@ -0,0 +1,2 @@ +#pragma once +void InitialiseMapsPrint(); diff --git a/NorthstarDLL/util/version.cpp b/NorthstarDLL/util/version.cpp new file mode 100644 index 00000000..8697b61f --- /dev/null +++ b/NorthstarDLL/util/version.cpp @@ -0,0 +1,96 @@ +#include "util/version.h" +#include "pch.h" +#include "ns_version.h" +#include "dedicated/dedicated.h" + +char version[16]; +char NSUserAgent[256]; + +void InitialiseVersion() +{ + constexpr int northstar_version[4] {NORTHSTAR_VERSION}; + int ua_len = 0; + + // We actually use the rightmost integer do determine whether or not we're a debug/dev build + // If it is set to 1, we are a dev build + // On github CI, we set this 1 to a 0 automatically as we replace the 0,0,0,1 with the real version number + if (northstar_version[3] == 1) + { + sprintf(version, "%d.%d.%d.%d+dev", northstar_version[0], northstar_version[1], northstar_version[2], northstar_version[3]); + ua_len += snprintf( + NSUserAgent + ua_len, + sizeof(NSUserAgent) - ua_len, + "R2Northstar/%d.%d.%d+dev", + northstar_version[0], + northstar_version[1], + northstar_version[2]); + } + else + { + sprintf(version, "%d.%d.%d.%d", northstar_version[0], northstar_version[1], northstar_version[2], northstar_version[3]); + ua_len += snprintf( + NSUserAgent + ua_len, + sizeof(NSUserAgent) - ua_len, + "R2Northstar/%d.%d.%d", + northstar_version[0], + northstar_version[1], + northstar_version[2]); + } + + if (IsDedicatedServer()) + ua_len += snprintf(NSUserAgent + ua_len, sizeof(NSUserAgent) - ua_len, " (Dedicated)"); + + // Add the host platform info to the user agent. + // + // note: ntdll will always be loaded + HMODULE ntdll = GetModuleHandleA("ntdll"); + if (ntdll) + { + // real win32 version info (i.e., ignore manifest) + DWORD(WINAPI * RtlGetVersion)(LPOSVERSIONINFOEXW); + *(FARPROC*)(&RtlGetVersion) = GetProcAddress(ntdll, "RtlGetVersion"); + + // wine version (7.0-rc1, 7.0, 7.11, etc) + const char*(CDECL * wine_get_version)(void); + *(FARPROC*)(&wine_get_version) = GetProcAddress(ntdll, "wine_get_version"); + + // human-readable build string (e.g., "wine-7.22 (Staging)") + const char*(CDECL * wine_get_build_id)(void); + *(FARPROC*)(&wine_get_build_id) = GetProcAddress(ntdll, "wine_get_build_id"); + + // uname sysname (Darwin, Linux, etc) and release (kernel version string) + void(CDECL * wine_get_host_version)(const char** sysname, const char** release); + *(FARPROC*)(&wine_get_host_version) = GetProcAddress(ntdll, "wine_get_host_version"); + + OSVERSIONINFOEXW osvi = {}; + const char *wine_version = NULL, *wine_build_id = NULL, *wine_sysname = NULL, *wine_release = NULL; + if (RtlGetVersion) + RtlGetVersion(&osvi); + if (wine_get_version) + wine_version = wine_get_version(); + if (wine_get_build_id) + wine_build_id = wine_get_build_id(); + if (wine_get_host_version) + wine_get_host_version(&wine_sysname, &wine_release); + + // windows version + if (osvi.dwMajorVersion) + ua_len += snprintf( + NSUserAgent + ua_len, + sizeof(NSUserAgent) - ua_len, + " Windows/%d.%d.%d", + osvi.dwMajorVersion, + osvi.dwMinorVersion, + osvi.dwBuildNumber); + + // wine version + if (wine_version && wine_build_id) + ua_len += snprintf(NSUserAgent + ua_len, sizeof(NSUserAgent) - ua_len, " Wine/%s (%s)", wine_version, wine_build_id); + + // wine host system version + if (wine_sysname && wine_release) + ua_len += snprintf(NSUserAgent + ua_len, sizeof(NSUserAgent) - ua_len, " %s/%s", wine_sysname, wine_release); + } + + return; +} diff --git a/NorthstarDLL/util/version.h b/NorthstarDLL/util/version.h new file mode 100644 index 00000000..261a4cf1 --- /dev/null +++ b/NorthstarDLL/util/version.h @@ -0,0 +1,6 @@ +#pragma once + +extern char version[16]; +extern char NSUserAgent[32]; + +void InitialiseVersion(); diff --git a/NorthstarDLL/vector.h b/NorthstarDLL/vector.h deleted file mode 100644 index c06e0bba..00000000 --- a/NorthstarDLL/vector.h +++ /dev/null @@ -1,52 +0,0 @@ -#pragma once - -union Vector3 -{ - struct - { - float x; - float y; - float z; - }; - - float raw[3]; - - Vector3() : x(0), y(0), z(0) {} - Vector3(float x, float y, float z) : x(x), y(y), z(z) {} - Vector3(float* pRawFloats) // assumes float[3] => vector - { - memcpy(raw, pRawFloats, sizeof(this)); - } - - void MakeValid() - { - for (auto& fl : raw) - if (isnan(fl)) - fl = 0; - } - - // todo: more operators maybe - bool operator==(const Vector3& other) - { - return x == other.x && y == other.y && z == other.z; - } -}; - -union QAngle -{ - struct - { - float x; - float y; - float z; - float w; - }; - - float raw[4]; - - // todo: more operators maybe - bool operator==(const QAngle& other) - { - return x == other.x && y == other.y && z == other.z && w == other.w; - } -}; diff --git a/NorthstarDLL/version.cpp b/NorthstarDLL/version.cpp deleted file mode 100644 index 088fb491..00000000 --- a/NorthstarDLL/version.cpp +++ /dev/null @@ -1,96 +0,0 @@ -#include "version.h" -#include "pch.h" -#include "ns_version.h" -#include "dedicated.h" - -char version[16]; -char NSUserAgent[256]; - -void InitialiseVersion() -{ - constexpr int northstar_version[4] {NORTHSTAR_VERSION}; - int ua_len = 0; - - // We actually use the rightmost integer do determine whether or not we're a debug/dev build - // If it is set to 1, we are a dev build - // On github CI, we set this 1 to a 0 automatically as we replace the 0,0,0,1 with the real version number - if (northstar_version[3] == 1) - { - sprintf(version, "%d.%d.%d.%d+dev", northstar_version[0], northstar_version[1], northstar_version[2], northstar_version[3]); - ua_len += snprintf( - NSUserAgent + ua_len, - sizeof(NSUserAgent) - ua_len, - "R2Northstar/%d.%d.%d+dev", - northstar_version[0], - northstar_version[1], - northstar_version[2]); - } - else - { - sprintf(version, "%d.%d.%d.%d", northstar_version[0], northstar_version[1], northstar_version[2], northstar_version[3]); - ua_len += snprintf( - NSUserAgent + ua_len, - sizeof(NSUserAgent) - ua_len, - "R2Northstar/%d.%d.%d", - northstar_version[0], - northstar_version[1], - northstar_version[2]); - } - - if (IsDedicatedServer()) - ua_len += snprintf(NSUserAgent + ua_len, sizeof(NSUserAgent) - ua_len, " (Dedicated)"); - - // Add the host platform info to the user agent. - // - // note: ntdll will always be loaded - HMODULE ntdll = GetModuleHandleA("ntdll"); - if (ntdll) - { - // real win32 version info (i.e., ignore manifest) - DWORD(WINAPI * RtlGetVersion)(LPOSVERSIONINFOEXW); - *(FARPROC*)(&RtlGetVersion) = GetProcAddress(ntdll, "RtlGetVersion"); - - // wine version (7.0-rc1, 7.0, 7.11, etc) - const char*(CDECL * wine_get_version)(void); - *(FARPROC*)(&wine_get_version) = GetProcAddress(ntdll, "wine_get_version"); - - // human-readable build string (e.g., "wine-7.22 (Staging)") - const char*(CDECL * wine_get_build_id)(void); - *(FARPROC*)(&wine_get_build_id) = GetProcAddress(ntdll, "wine_get_build_id"); - - // uname sysname (Darwin, Linux, etc) and release (kernel version string) - void(CDECL * wine_get_host_version)(const char** sysname, const char** release); - *(FARPROC*)(&wine_get_host_version) = GetProcAddress(ntdll, "wine_get_host_version"); - - OSVERSIONINFOEXW osvi = {}; - const char *wine_version = NULL, *wine_build_id = NULL, *wine_sysname = NULL, *wine_release = NULL; - if (RtlGetVersion) - RtlGetVersion(&osvi); - if (wine_get_version) - wine_version = wine_get_version(); - if (wine_get_build_id) - wine_build_id = wine_get_build_id(); - if (wine_get_host_version) - wine_get_host_version(&wine_sysname, &wine_release); - - // windows version - if (osvi.dwMajorVersion) - ua_len += snprintf( - NSUserAgent + ua_len, - sizeof(NSUserAgent) - ua_len, - " Windows/%d.%d.%d", - osvi.dwMajorVersion, - osvi.dwMinorVersion, - osvi.dwBuildNumber); - - // wine version - if (wine_version && wine_build_id) - ua_len += snprintf(NSUserAgent + ua_len, sizeof(NSUserAgent) - ua_len, " Wine/%s (%s)", wine_version, wine_build_id); - - // wine host system version - if (wine_sysname && wine_release) - ua_len += snprintf(NSUserAgent + ua_len, sizeof(NSUserAgent) - ua_len, " %s/%s", wine_sysname, wine_release); - } - - return; -} diff --git a/NorthstarDLL/version.h b/NorthstarDLL/version.h deleted file mode 100644 index 261a4cf1..00000000 --- a/NorthstarDLL/version.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -extern char version[16]; -extern char NSUserAgent[32]; - -void InitialiseVersion(); -- cgit v1.2.3