Engine access
How the host loads game modules and lends native capabilities to plugins.
The host loads the game modules and installs and removes every native hook, and a plugin borrows what it needs from PluginContext::engine. A borrowed interface, function pointer, or entity
view must not outlive its module or world.
Modules and interfaces
MappedModuleImage::ForModule borrows an already loaded module's address and
bytes. ResolveEngineInterface calls that module's CreateInterface export
with the requested registration name. Neither operation loads, retains, or
unloads a module. Missing modules, exports, and registrations return errors.
CMake selects the Windows implementation or an unsupported-host implementation that returns errors. Callers use the same API on every host. Windows ABI calls and inline detours remain guarded where they need Windows types or SafetyHook. Portable parsing, callback dispatch, and fixture tests run without game modules.
Signatures and addresses
ParseSignature and SignatureScan operate on borrowed byte spans. Every
game signature is one entry in the game data file data/game_signatures.txtpb,
a protobuf text file of src/gameinterop/signatures/signatures.proto. An entry
has a stable id, the modules where the pattern matches exactly once, the pattern, what the match
resolves to, and the native shape.
- Loading. The SDK embeds the file as of its build.
modlock-hostloads the file named by--game-data, else the copy installed beside it, at start. When a game patch moves a pattern, a new file fixes it without a new host. A malformed entry is skipped with a log line, and only the features that resolve its id fail. - Resolving.
ResolveSignature(image, id)returns the match itself, the target of theE8call at a delta into the match, or the RIP-relative operand of a seven-byte instruction at a delta. A relative target checks that the whole instruction fits before reading its signed displacement. - Failing. A missing or ambiguous match fails with the entry's id.
After a game update, build modlock-sigcheck and run it against the Deadlock
install:
modlock-sigcheck <deadlock-dir> [--baseline <previous-deadlock-dir>] \
[--signatures data/game_signatures.txtpb]It resolves every entry in every module you pass it and exits nonzero when any
fails. --signatures checks an edited game data file without a rebuild. With a
copy of the previous build as the baseline, modlock-sigcheck also suggests a pattern for each
failed entry. It decodes the old match, turns its relative displacements and
branch targets into wildcards, and prints the shortest instruction-aligned
prefix that resolves once in the new build. Review each suggestion against the
recorded shape before replacing the entry in the game data file.
Virtual functions that modlock calls or hooks through a dispatch table are
slots entries in the same file. Each gives its module, its class's RTTI name,
the index it held when recorded, and a pattern for the function's entry bytes.
ResolveVirtualSlot(id) (modlock/gameinterop/virtual_slots.h) finds the
class's table through its RTTI and returns the index of the one function that
matches the pattern. A game update that inserts a virtual ahead of the slot
moves the index without breaking the caller, and the host logs the move once.
Zero or several matches are an error that gives the slot, and the feature that
needs it stays off.
modlock-sigcheck checks every slot the same way. A moved slot passes and
prints its new index, which belongs in the data file so the hint stays
current. A failure prints the reason: no function of the table matches, or the
indexes of every match when the pattern no longer tells them apart. Re-derive
or lengthen the pattern.
An address is evidence of a pattern match, not proof that an arbitrary function signature is safe to call. Callers must use the recorded calling convention and layout. Schema offsets remain a separate resolution path.
Hook lifetime
VtableSlotHook patches one entry in the module's original dispatch table.
It never copies the table or changes an object's table pointer, so unnamed slots
remain available to the engine. The caller supplies a valid slot and keeps the
table alive until restoration. Hooks stacked on the same slot must be removed
in reverse installation order.
ThunkOwner couples a slot hook to its callback cleanup. Destruction and move
assignment restore the old slot before they clear the callback state, and a move hands both duties to the new owner. Callback holders declare their owner after borrowed handler storage
so dispatch is restored before that storage is destroyed.
Inline detours use SafetyHook directly because native code can call a function without going through its virtual slot. Each hook keeps its own original-call signature, callback ordering, and thread contract. Installation and teardown follow the host's engine lifecycle, but the RAII owners do not synchronize concurrent hook installation or wait for callbacks on other threads.
What the tests cover
The SDK tests exercise unique signature matching, relative instruction bounds, dispatch-table preservation, move ownership, and restore-before-clear ordering. The independent hello plugin checks the installed SDK and plugin admission. Windows builds check the native code paths, but only a live game run can validate patterns and ABI layouts against a new game binary.