The mod lifecycle
When each part of a mod runs, on which thread, and what it may touch. The rules come from the comments in t3sdk.h; the API reference lists every call.
From start to exit
- T3SDK starts. The game loads
System\dinput8.dll, and T3SDK starts before the game's own start-up code. It checks the game build (on any other build it stops here, and no mod is loaded), installs its fixes and hooks, and loads the mods: the packaged mods in the launcher's load order, then loose DLLs inSystem\modsby name. T3Mod_Initruns, once per mod, in load order, before the engine exists. Keep the API pointer, check the API version, register callbacks and create hooks. Don't use the object functions yet: they return 0 orNULLuntil the engine is ready. Return 0 to stay loaded. Any other value (or a missingT3Mod_Init) unloads the DLL, and T3SDK removes the callbacks it registered.- The engine starts. Engine log callbacks receive the engine's log lines from the start, before the engine is ready.
- The engine is ready. Once the engine's objects exist and T3SDK has checked their layout,
EngineReady()returns non-zero and T3SDK logsengine ready. - Frames. Frame callbacks run once per pass of the game's message loop, on the main thread, from the first time
EngineReady()is true. The game waits for them, so keep them short. - The game exits. When the game begins exiting,
EngineReady()returns 0 again and frame callbacks stop. Object pointers you kept may now be freed. When the process ends, T3SDK calls each mod's optionalT3Mod_Shutdown, in reverse load order.
Threads
T3Mod_Init, frame callbacks andT3Mod_Shutdownrun on the game's main thread.- Engine log callbacks run on whichever thread wrote the log line. Protect any state they share with a frame callback (a critical section, or an atomic flag), and do engine work in the frame callback instead.
- T3SDK does not make engine objects safe to use from other threads. Use them from frame callbacks only.
Objects and memory
T3Object*pointers, and the object functions, are only good whileEngineReady()holds. Look objects up again rather than keeping pointers across the game's exit.Text comes back through your own buffers. Each text function returns the full length, like
snprintf, so a call withNULLand 0 tells you the size to allocate:cppchar name[256]; size_t length = api->ObjectPathName(object, name, sizeof name); // length >= sizeof name means the text was cut shortNothing allocated on one side of the API is freed on the other.
Hooks
CreateHook replaces a function of T3Main.exe with your detour and gives you a trampoline that calls the original. The hook stays off until EnableHook:
using SomeFn = int(__cdecl*)(int);
static SomeFn original;
static int __cdecl Detour(int value) {
return original(value) + 1;
}
// in T3Mod_Init; 0x10A00000 stands for a real function address from docs/engine.md
void* target = reinterpret_cast<void*>(0x10A00000);
if (api->CreateHook(target, reinterpret_cast<void*>(&Detour), reinterpret_cast<void**>(&original)) == 0) {
api->EnableHook(target);
}- Hooks can be created in
T3Mod_Init: the game's code is there from the start, even though the engine is not running yet. - Addresses are fixed because the game has one supported build, and T3SDK loads no mods on any other. Engine internals lists the known addresses with their evidence.
- The detour must match the target's calling convention. For a
__thiscallmethod, MSVC mods can use__fastcallwith an unused second parameter (edx). - Every mod and T3SDK share one hook table, so a function can be hooked only once.
CreateHookreturns an error (MH_ERROR_ALREADY_CREATED) when someone else got there first. T3SDK hooks a few engine functions itself;engine.mdmarks them "hooked". - If
T3Mod_Initfails after it created hooks, remove them before it returns: T3SDK removes a failed mod's callbacks, but not its hooks.
Errors
- An exception in a frame or log callback is caught. T3SDK logs it with your mod's name and switches that callback off; the game and other mods go on.
- The same holds for
T3Mod_Shutdown. T3Mod_Initis not guarded in the same way: a crash there ends T3SDK's start-up, and later mods do not load. Keep it simple.- Don't let C++ exceptions leave a callback.
API versions
api->version is the API version the installed T3SDK implements, and api->size the size of its function table. The table only grows. A member added after version 1 is usable when api->size covers it; the API reference says which version added each member.
#include <cstddef>
// NewCall stands for any member added after version 1.
bool hasNewCall = api->size >= offsetof(T3SdkApi, NewCall) + sizeof api->NewCall;Set api in your package's mod.json to the lowest version your mod needs; the launcher warns when the installed T3SDK is older (see Packaging).