aboutsummaryrefslogtreecommitdiff
path: root/primedev/engine
diff options
context:
space:
mode:
authorJack <66967891+ASpoonPlaysGames@users.noreply.github.com>2023-12-27 00:32:01 +0000
committerGitHub <noreply@github.com>2023-12-27 01:32:01 +0100
commitf5ab6fb5e8be7b73e6003d4145081d5e0c0ce287 (patch)
tree90f2c6a4885dbd181799e2325cf33588697674e1 /primedev/engine
parentbb8ed59f6891b1196c5f5bbe7346cd171c8215fa (diff)
downloadNorthstarLauncher-f5ab6fb5e8be7b73e6003d4145081d5e0c0ce287.tar.gz
NorthstarLauncher-f5ab6fb5e8be7b73e6003d4145081d5e0c0ce287.zip
Folder restructuring from primedev (#624)v1.21.2-rc3v1.21.2
Copies of over the primedev folder structure for easier cherry-picking of further changes Co-authored-by: F1F7Y <filip.bartos07@proton.me>
Diffstat (limited to 'primedev/engine')
-rw-r--r--primedev/engine/host.cpp33
-rw-r--r--primedev/engine/hoststate.cpp191
-rw-r--r--primedev/engine/hoststate.h41
-rw-r--r--primedev/engine/r2engine.cpp37
-rw-r--r--primedev/engine/r2engine.h260
-rw-r--r--primedev/engine/runframe.cpp19
6 files changed, 581 insertions, 0 deletions
diff --git a/primedev/engine/host.cpp b/primedev/engine/host.cpp
new file mode 100644
index 00000000..dacb8fc1
--- /dev/null
+++ b/primedev/engine/host.cpp
@@ -0,0 +1,33 @@
+#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)
+ Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec autoexec_ns_server", cmd_source_t::kCommandSrcCode);
+ else
+ Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec autoexec_ns_client", cmd_source_t::kCommandSrcCode);
+}
+
+ON_DLL_LOAD("engine.dll", Host_Init, (CModule module))
+{
+ AUTOHOOK_DISPATCH()
+}
diff --git a/primedev/engine/hoststate.cpp b/primedev/engine/hoststate.cpp
new file mode 100644
index 00000000..ec728afb
--- /dev/null
+++ b/primedev/engine/hoststate.cpp
@@ -0,0 +1,191 @@
+#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"
+#include "plugins/plugins.h"
+#include "plugins/pluginbackend.h"
+
+AUTOHOOK_INIT()
+
+CHostState* g_pHostState;
+
+std::string sLastMode;
+
+VAR_AT(engine.dll + 0x13FA6070, ConVar*, Cvar_hostport);
+FUNCTION_AT(engine.dll + 0x1232C0, void, __fastcall, _Cmd_Exec_f, (const CCommand& arg, bool bOnlyIfExists, bool bUseWhitelists));
+
+void ServerStartingOrChangingMap()
+{
+ ConVar* Cvar_mp_gamemode = g_pCVar->FindVar("mp_gamemode");
+
+ // directly call _Cmd_Exec_f to avoid weirdness with ; being in mp_gamemode potentially
+ // if we ran exec {mp_gamemode} and mp_gamemode contained semicolons, this could be used to execute more commands
+ 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 (sLastMode.length() &&
+ CCommand__Tokenize(tempCommand, fmt::format("exec server/cleanup_gamemode_{}", sLastMode).c_str(), cmd_source_t::kCommandSrcCode))
+ _Cmd_Exec_f(tempCommand, false, false);
+
+ memset(commandBuf, 0, sizeof(commandBuf));
+ if (CCommand__Tokenize(
+ tempCommand,
+ fmt::format("exec server/setup_gamemode_{}", sLastMode = Cvar_mp_gamemode->GetString()).c_str(),
+ cmd_source_t::kCommandSrcCode))
+ {
+ _Cmd_Exec_f(tempCommand, false, false);
+ }
+
+ Cbuf_Execute(); // exec everything right now
+
+ // 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);
+ g_pServerAuthentication->m_bStartingLocalSPGame = true;
+ }
+ else
+ g_pServerAuthentication->m_bStartingLocalSPGame = false;
+}
+
+// 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)
+ R2::SetCurrentPlaylist("tdm");
+
+ ServerStartingOrChangingMap();
+
+ double dStartTime = Plat_FloatTime();
+ CHostState__State_NewGame(self);
+ spdlog::info("loading took {}s", Plat_FloatTime() - dStartTime);
+
+ // setup server presence
+ g_pServerPresence->CreatePresence();
+ g_pServerPresence->SetMap(g_pHostState->m_levelName, true);
+ g_pServerPresence->SetPlaylist(R2::GetCurrentPlaylistName());
+ g_pServerPresence->SetPort(Cvar_hostport->GetInt());
+
+ g_pServerAuthentication->m_bNeedLocalAuthForNewgame = false;
+}
+
+// clang-format off
+AUTOHOOK(CHostState__State_LoadGame, engine.dll + 0x16E730,
+void, __fastcall, (CHostState* self))
+// clang-format on
+{
+ // singleplayer server starting
+ // useless in 99% of cases but without it things could potentially break very much
+
+ spdlog::info("HostState: LoadGame");
+
+ Cbuf_AddText(Cbuf_GetCurrentPlayer(), "exec autoexec_ns_server", cmd_source_t::kCommandSrcCode);
+ Cbuf_Execute();
+
+ // this is normally done in ServerStartingOrChangingMap(), but seemingly the map name isn't set at this point
+ g_pCVar->FindVar("net_data_block_enabled")->SetValue(true);
+ g_pServerAuthentication->m_bStartingLocalSPGame = true;
+
+ double dStartTime = Plat_FloatTime();
+ CHostState__State_LoadGame(self);
+ spdlog::info("loading took {}s", Plat_FloatTime() - dStartTime);
+
+ // no server presence, can't do it because no map name in hoststate
+ // and also not super important for sp saves really
+
+ 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 = Plat_FloatTime();
+ CHostState__State_ChangeLevelMP(self);
+ spdlog::info("loading took {}s", 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();
+
+ CHostState__State_GameShutdown(self);
+
+ // run gamemode cleanup cfg now instead of when we start next map
+ if (sLastMode.length())
+ {
+ 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, fmt::format("exec server/cleanup_gamemode_{}", sLastMode).c_str(), cmd_source_t::kCommandSrcCode))
+ {
+ _Cmd_Exec_f(tempCommand, false, false);
+ Cbuf_Execute();
+ }
+
+ sLastMode.clear();
+ }
+}
+
+// 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 (*g_pServerState == 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<ScriptContext::UI>->m_pSQVM != nullptr && g_pSquirrel<ScriptContext::UI>->m_pSQVM->sqvm != nullptr)
+ g_pSquirrel<ScriptContext::UI>->ProcessMessageBuffer();
+
+ if (g_pSquirrel<ScriptContext::CLIENT>->m_pSQVM != nullptr && g_pSquirrel<ScriptContext::CLIENT>->m_pSQVM->sqvm != nullptr)
+ g_pSquirrel<ScriptContext::CLIENT>->ProcessMessageBuffer();
+
+ if (g_pSquirrel<ScriptContext::SERVER>->m_pSQVM != nullptr && g_pSquirrel<ScriptContext::SERVER>->m_pSQVM->sqvm != nullptr)
+ g_pSquirrel<ScriptContext::SERVER>->ProcessMessageBuffer();
+
+ g_pPluginManager->RunFrame();
+}
+
+ON_DLL_LOAD_RELIESON("engine.dll", HostState, ConVar, (CModule module))
+{
+ AUTOHOOK_DISPATCH()
+
+ g_pHostState = module.Offset(0x7CF180).RCast<CHostState*>();
+}
diff --git a/primedev/engine/hoststate.h b/primedev/engine/hoststate.h
new file mode 100644
index 00000000..290903ab
--- /dev/null
+++ b/primedev/engine/hoststate.h
@@ -0,0 +1,41 @@
+#pragma once
+
+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;
diff --git a/primedev/engine/r2engine.cpp b/primedev/engine/r2engine.cpp
new file mode 100644
index 00000000..88500376
--- /dev/null
+++ b/primedev/engine/r2engine.cpp
@@ -0,0 +1,37 @@
+#include "r2engine.h"
+
+Cbuf_GetCurrentPlayerType Cbuf_GetCurrentPlayer;
+Cbuf_AddTextType Cbuf_AddText;
+Cbuf_ExecuteType Cbuf_Execute;
+
+bool (*CCommand__Tokenize)(CCommand& self, const char* pCommandString, cmd_source_t commandSource);
+
+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
+
+CGlobalVars* g_pGlobals;
+
+ON_DLL_LOAD("engine.dll", R2Engine, (CModule module))
+{
+ Cbuf_GetCurrentPlayer = module.Offset(0x120630).RCast<Cbuf_GetCurrentPlayerType>();
+ Cbuf_AddText = module.Offset(0x1203B0).RCast<Cbuf_AddTextType>();
+ Cbuf_Execute = module.Offset(0x1204B0).RCast<Cbuf_ExecuteType>();
+
+ CCommand__Tokenize = module.Offset(0x418380).RCast<bool (*)(CCommand&, const char*, cmd_source_t)>();
+
+ g_pEngine = module.Offset(0x7D70C8).Deref().RCast<CEngine*>();
+
+ CBaseClient__Disconnect = module.Offset(0x1012C0).RCast<void (*)(void*, uint32_t, const char*, ...)>();
+ g_pClientArray = module.Offset(0x12A53F90).RCast<CBaseClient*>();
+
+ g_pServerState = module.Offset(0x12A53D48).RCast<server_state_t*>();
+
+ g_pGlobals = module.Offset(0x7C6F70).RCast<CGlobalVars*>();
+}
diff --git a/primedev/engine/r2engine.h b/primedev/engine/r2engine.h
new file mode 100644
index 00000000..e3bcc37e
--- /dev/null
+++ b/primedev/engine/r2engine.h
@@ -0,0 +1,260 @@
+#pragma once
+#include "shared/keyvalues.h"
+
+// 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;
+
+extern bool (*CCommand__Tokenize)(CCommand& self, const char* pCommandString, cmd_source_t commandSource);
+
+// 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() = 0; // unsure if this is where
+ virtual bool Load(bool dedicated, const char* baseDir) = 0;
+ virtual void Unload() = 0;
+ virtual void SetNextState(EngineState_t iNextState) = 0;
+ virtual EngineState_t GetState() = 0;
+ virtual void Frame() = 0;
+ virtual double GetFrameTime() = 0;
+ virtual float GetCurTime() = 0;
+
+ 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
+};
+
+enum class eSignonState : int
+{
+ NONE = 0, // no state yet; about to connect
+ CHALLENGE = 1, // client challenging server; all OOB packets
+ CONNECTED = 2, // client is connected to server; netchans ready
+ NEW = 3, // just got serverinfo and string tables
+ PRESPAWN = 4, // received signon buffers
+ GETTINGDATA = 5, // respawn-defined signonstate, assumedly this is for persistence
+ SPAWN = 6, // ready to receive entity packets
+ FIRSTSNAP = 7, // another respawn-defined one
+ FULL = 8, // we are fully connected; first non-delta packet received
+ CHANGELEVEL = 9, // server is changing level; please wait
+};
+
+// clang-format off
+OFFSET_STRUCT(CBaseClient)
+{
+ STRUCT_SIZE(0x2D728)
+ FIELD(0x16, char m_Name[64])
+ FIELD(0x258, KeyValues* m_ConVars)
+ FIELD(0x2A0, eSignonState m_Signon)
+ FIELD(0x358, char m_ClanTag[16])
+ FIELD(0x484, bool m_bFakePlayer)
+ 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;
+
+// clang-format off
+OFFSET_STRUCT(CGlobalVars)
+{
+ FIELD(0x0,
+ // Absolute time (per frame still - Use Plat_FloatTime() for a high precision real time
+ // perf clock, but not that it doesn't obey host_timescale/host_framerate)
+ double m_flRealTime);
+
+ FIELDS(0x8,
+ // Absolute frame counter - continues to increase even if game is paused
+ int m_nFrameCount;
+
+ // Non-paused frametime
+ float m_flAbsoluteFrameTime;
+
+ // Current time
+ //
+ // On the client, this (along with tickcount) takes a different meaning based on what
+ // piece of code you're in:
+ //
+ // - While receiving network packets (like in PreDataUpdate/PostDataUpdate and proxies),
+ // this is set to the SERVER TICKCOUNT for that packet. There is no interval between
+ // the server ticks.
+ // [server_current_Tick * tick_interval]
+ //
+ // - While rendering, this is the exact client clock
+ // [client_current_tick * tick_interval + interpolation_amount]
+ //
+ // - During prediction, this is based on the client's current tick:
+ // [client_current_tick * tick_interval]
+ float m_flCurTime;
+ )
+
+ FIELDS(0x30,
+ // Time spent on last server or client frame (has nothing to do with think intervals)
+ float m_flFrameTime;
+
+ // current maxplayers setting
+ int m_nMaxClients;
+ )
+
+ FIELDS(0x3C,
+ // Simulation ticks - does not increase when game is paused
+ uint32_t m_nTickCount; // this is weird and doesn't seem to increase once per frame?
+
+ // Simulation tick interval
+ float m_flTickInterval;
+ )
+
+ FIELDS(0x60,
+ const char* m_pMapName;
+ int m_nMapVersion;
+ )
+
+ //FIELD(0x98, double m_flRealTime); // again?
+};
+// clang-format on
+
+extern CGlobalVars* g_pGlobals;
diff --git a/primedev/engine/runframe.cpp b/primedev/engine/runframe.cpp
new file mode 100644
index 00000000..ddfd9253
--- /dev/null
+++ b/primedev/engine/runframe.cpp
@@ -0,0 +1,19 @@
+#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, (CEngine* self))
+// clang-format on
+{
+ CEngine__Frame(self);
+}
+
+ON_DLL_LOAD("engine.dll", RunFrame, (CModule module))
+{
+ AUTOHOOK_DISPATCH()
+}