Custom launcher and in-game DLL for the Unreal Engine game Splitgate, written in C++. The DLL is injected/loaded alongside the game and renders an in-game menu that drives a set of gameplay features; the launcher is the companion executable used to start it.
Important
Do not read these files — Internal/dllmain.cpp, Launcher/Launcher.cpp. If you
need something from one of them, ask and it will be provided to you.
Internal/hook/Hook.h is partially readable: you may read lines 1–17 (includes,
namespace Hook opening, injectionHook, and the SetHook signature) and lines 34 to the end
(Init / UnHook). Never read lines 18–33 — the body of
BYTE *SetHook(void **VTable, int index, void *TargetFunction) and
`Never read the full file
The Visual Studio solution (.sln at the repo root) contains three projects:
- Launcher — produces
Launcher.exe, the companion app the user runs. (details below) - Internal — produces
Internal.dll, the in-game module. (details below) - Tests — a GoogleTest console
.exe(vcpkggtest) covering the self-contained modules (settings, feature framework, event bus). See docs/testing.md.
Other top-level items: shared/ (headers used by both projects — Ipc.h for the
launcher/DLL init handshake, Logger.h for the shared console logger, ExceptionHandler.h
for the reusable crash handler, Settings.h for the generic SettingsFile<T> persistence,
LauncherSettings.h for the launcher-only settings the DLL's Network tab edits and the
launcher reads), Tools/ (build
scripts, incl. build.bat used by CI, and format.ps1 for clang-format),
.github/workflows/msbuild.yml (CI), .clang-format / .clang-tidy (style/lint config),
docs/ (see below), README.md.
Longer-form docs live in docs/ and are linked from the README:
- docs/settings.md — configuration structs, persistence, file location.
- docs/features.md — the feature framework and how to add a feature.
- docs/hooking.md — injection (the
WH_GETMESSAGEtechnique, theextern "C"SplitgateCallBackexport) and how the game's functions are hooked (PostRender,ProcessEvent). - docs/debugging.md — reading logs (
launcher.log/internal.log), the in-game console, crash stack traces, and the Debug-tab switches. - docs/game-dump.md — the Dumpspace SDK dump for Splitgate (hash
d2a5bd8c): the JSON files/schema, the GNames/GObjects/GWorld offsets, and how the referenceDSAPIwrapper consumes them. - docs/backend-redirect.md — redirecting the game to a self-hosted
backend (the
network/module: redirect map, HTTP logger, WinHTTP + libcurl hooks), target configurable via settings JSON; with scope/legal caveats. - docs/early-injection.md — design note on covering the backend calls the game makes before the DLL is injected (suspended-launch + early injection; not built).
- docs/scripting.md — embedding Python and writing user scripts.
- docs/testing.md — the gtest project and how to run it.
- docs/style.md — the clang-format / clang-tidy setup.
- docs/planned-features.md / docs/roadmap.md — design notes for near-term features and larger future work (not yet implemented).
- docs/ue4-cheatsheet.md — a living UE4 reverse-engineering reference (key objects, neighbour-derived offsets, and snippets), cross-linked to the repo's implementations.
Put Launcher.exe and Internal.dll in the same folder, start the game, open
Launcher.exe, then press Ins to show/hide the GUI. Prebuilt binaries are published
via nightly.link; otherwise build from source.
The launcher gets Internal.dll running inside the game via the classic SetWindowsHookEx
injection technique, then the DLL takes over:
- Launcher loads
Internal.dll, resolves the exportedSplitgateCallBack(a plain, undecorated name since it isextern "C"; shared asIpc::CallbackExport), finds thePortalWarswindow and its UI thread, and installs aWH_GETMESSAGEhook on that thread whose proc isSplitgateCallBack. This forces Windows to mapInternal.dllinto the game process. It thenPostThreadMessageWs a trigger message whoselParamcarries the returnedHHOOK, and exits. - Inside the game process,
SplitgateCallBack(dllmain.cpp) runs when that message is pulled from the queue. It captures theHHOOKfrommsg->lParamintoHook::injectionHook(soUnHook()can later remove the hook), guards against re-entry withHook::initialized, and callsHook::Init()(seehook/Hook.h), which installs theProcessEvent/PostRenderhooks, the GUI, Python, and the features.CallNextHookEx's first argument is ignored by Windows, so a not-yet-setinjectionHookthere is harmless — the handle only matters for the eventualUnhookWindowsHookEx. - From then on the DLL drives everything:
PostRenderrenders the menu + features each frame,ProcessEventfeeds the event bus, and Discord RPC runs in the background. The launcher deliberately does not unhook (that could unload the DLL); the DLL owns teardown inHook::UnHook().
So the launcher is only an injector (process/window discovery + hook install + handle
handoff); all in-game behavior lives in the DLL. Both sides agree on the trigger message via
RegisterWindowMessageW(L"SplitgateInit") (a session-unique id, no shared constant needed) —
see docs/hooking.md.
- Toolchain: Visual Studio 2022 (
v145), C++ latest standard, x64 Release only. - Dependencies via vcpkg (manifest mode).
Internalalso uses submodules underInternal/external/(pybind11, discord-rpc). Internalembeds CPython (pybind11 +<Python.h>) for user scripting, so a Python 3.x install is required to compile it. The Python root is resolved byInternal/python.props(imported from the.vcxproj), which tries in order:/p:PythonRoot=, theSPLITGATE_PYTHON_DIRenv var, well-known install dirs newest-first, then the installer's registryInstallPath; it injectsinclude/libsbefore ClCompile/Link and errors if none is found.pythonXX.libis auto-linked viapyconfig.h, so no explicit lib entry is needed — but the chosen version must match thepythonXX.dllat runtime.- Links
d3d11.lib— the in-game overlay renders through DirectX 11. - CI (
.github/workflows/msbuild.yml) builds onwindows-latest:setup-python(3.14, passed to the build viaSPLITGATE_PYTHON_DIR), vcpkg bootstrap + integrate,nuget restore, thenTools/build.bat, and uploadsx64/Release. Triggers on manual dispatch and on push/PR tomastertouchingInternal/**orLauncher/**.
Console application (SubSystem=Console), root namespace Launcher, x64 Release only
(v145, C++ latest, Unicode). No vcpkg/external dependencies and no extra linked libs — it's
self-contained relative to Internal. Preprocessor: _CRT_SECURE_NO_WARNINGS;NDEBUG;_CONSOLE.
Launcher.cpp— entry point / launcher logic. It is an injector/bootstrapper:LoadLibraryA("Internal.dll"), resolves the exportedSplitgateCallBackviaGetProcAddress(a plain, undecorated name since the export isextern "C", referenced throughIpc::CallbackExport), finds the game window (PortalWars), gets its UI thread + process id, installs aWH_GETMESSAGEhook (SetWindowsHookExW) pointing at that callback insideInternal.dll, posts a thread message (with theHHOOKin itslParam) to trigger it, then exits. Its whole job is process/window discovery + installing the Windows hook + handing off the handle. It does not unhook (the DLL owns that).utils/Logger.h— a thinLoggersubclass ofShared::Logger(shared/Logger.h) bound to the launcher's existing console andlauncher.log. All logging behavior (leveled/colored output, file mirroring with a local-time[HH:MM:SS]prefix,errorBox,stop) lives in the shared class.data/— Win32 resources:Logo.rc(compiled),Logo.h,logo.ico(the app icon);.apsfiles are the resource editor's cache.scripts/— bundled mitmproxy addon.pyfiles (default_proxy.py,watchdog.py) used byutils/Mitmproxy.hinProxyMode::Mitmproxy. Copied next toLauncher.exe(into ascripts/folder) at build time; fed their inputs via environment variables (SPLITGATE_REDIRECTS,SPLITGATE_GAME_PID) so they stay static, editable Python.
DynamicLibrary, root namespace Splitgate, exports gated behind SPLITGATE_EXPORTS.
Source folders (from the project file; contents documented as they are read):
hook/— the hooking engine.Hook.hdefinesnamespace Hookand drives the module's whole lifecycle:SetHook(VTable, index, TargetFunction)— swaps a single vtable entry and returns the original pointer (used for the manualPostRenderhook).Init()— the one-time bootstrap run on injection: opens the console, initializes the engine/Globals, walksWorld → OwningGameInstance → LocalPlayers[0] (UPortalWarsLocalPlayer) → ViewportClient → VFTableto capture thePostRenderandProcessEventvtables, seeds settings from the game (FOVfrom the save game, thenSettingsHelper::Load()), spawns aUConsole, starts Python (Scripts::Init()), installs the hooks (MinHook forProcessEvent, vtable swap forPostRender), initializes the ImGui GUI (GUI::Init()), and registers features (Features::Init()).UnHook()— teardown: disables/uninitializes MinHook, restores thePostRendervtable entry, destroys the console/GUI, disables the exception handler, removes the Win32 hook.hook/functions/— the two hooked game functions:ProcessEvent.h(funnels UE events, driving the event bus) andPostRender.h(per-frame, drivesFeatures::Execute()and the menu).
features/— the feature framework and the individual features (moved here out ofhook/).Feature.h— the baseFeaturetype (Init/Check/Run/Destroy, Enabled/OneTime state, and theEvents::Typethe feature runs on).FeatureRunner.h— theFeaturesregistry and per-feature tick (RunFeature) plus the render-loopExecute().Features.h— includes the concrete features, andInit()registers them and wires them to the event bus (including theSettingsChangedrefresh handler and theLoadIntoMaphandler).- Concrete features:
GodMode,InfiniteJetpack,NoRecoil,SpinBot,PlayerModifications,WeaponModifications,UserScripts,ThirdPerson,FreeCam,Radar,Esp. (The formerDrawActorsandLoadIntoMapfeatures were removed — ESP replaces the former; LoadIntoMap is now a menu-dispatched event.)
menu/— the GUI (Menu.h), withgui/(Config, Custom, Gui, Styles, Window) andsections/(Debug, Exploits, Misc, Settings, Visuals, Watermark —Visualsconfigures the ESP elements: 2D/3D boxes, bones, name, snaplines, health, distance, and their colors).scripting/— Python scripting via pybind11 (Scripts.h) and the C++ event bus (Events.h:Events::Typeenum,Events::Payload, Register/Dispatch). Exposesmodules/(Logger, Settings, Events) to user scripts.ue/— Unreal Engine SDK, split per type.sdk/holds one header per type (~96) withFwd.h(forward decls),Enums.h, andValues.h(aggregates theF*/T*value types);sdk.his the aggregator that includes everysdk/<Type>.h, andEngine.his the entry point callers include (it pulls insdk.h+custom.h+namespace Engine). A class'sProcessEventUFunction wrappers live insdk/<Type>.cpp(UFunctions are resolved lazily via a function-localstatic ... GObjects->FindObject(...)); the hand-added convenience members and free helpers (SpawnActor,LineTraceVisible,IsPostGameController) live incustom.cpp/custom.h.namespace Engine(all inline variables/templates, header-only) holds: the scanned internalsEngine::GObjects/GNames/GWorld/GetBoneMatrixFn; the resolved game objectsEngine::GEngine/World/PlayerController/GameplayStatics/KismetStringLibrary/KismetTextLibrary/Canvas/IsInGame; the two bootstrapsEngine::Init(signature scan, inEngine.cpp) andEngine::ResolveObjects(resolve CDOs, called again per map load); and theEngine::StaticClass<T>()/GetDefaultObj<T>()templates that read each type'sstatic constexpr ClassNametrait.discord/— Discord Rich Presence integration (rpc.h,handlers.h).settings/— configuration (Settings.h/.cpp).utils/— helpers (Util.h/.cpp, plus two facades overshared/:Logger.his anamespace Loggerfacade (Log/CreateConsole/DestroyConsole/SetConsoleVisibility) over oneShared::Loggerthat spawns the in-game console and logs tointernal.log; andExceptionHandler.hwiresShared::ExceptionHandlerwith the game's crash folder, the logger, andSettingsHelper::Deleteas the recovery hook, keeping theInit()/Disable()surface unchanged).dllmain.cpp— DLL entry point. Exports theWH_GETMESSAGEhook procedure the launcher installs —LRESULT CALLBACK SplitgateCallBack(int code, WPARAM wparam, LPARAM lparam)(lparamis theMSG*, valid only whencode >= 0). It is declaredextern "C", so the launcher resolves its plain, undecorated export nameSplitgateCallBack(shared asIpc::CallbackExportinshared/Ipc.h) — see docs/hooking.md for why this is stable where the old mangled name was not. On the trigger message it captures theHHOOKfrommsg->lParamintoHook::injectionHook, then (once, guarded byHook::initialized) initializes theExceptionHandler, runsHook::Init()(the bootstrap described above), logs the injection + module base address + menu hotkey, initializes Discord RPC, and chains toCallNextHookEx.Hook::injectionHook/Hook::initializedare declared inhook/Hook.h.
See .claude/rules/code-style.md — PascalCase file/namespace/ class names, camelCase variables, lowercase directories, reusable namespaces, and always-early-return control flow.
- Prefer many small, focused commits over one large one — split unrelated changes (a fix, a refactor, docs) into separate commits so each is reviewable and revertable on its own.
- Write commit messages in the Conventional Commits
style:
type(scope): summary(e.g.fix(hook): capture the injection hook handle,docs: add docs/hooking.md). Common types:feat,fix,docs,refactor,chore,test.