You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -129,7 +131,7 @@ DetourModKit is a lightweight C++ toolkit designed to simplify common tasks in g
129
131
<summary><strong>Format, Filesystem, Math, and Version Utilities</strong></summary>
130
132
131
133
-**Format** (`format.hpp`): Inline formatting helpers for memory addresses, byte values, VK codes, and hex integer vectors using `std::format`. Also includes string trim utilities.
-**Version** (`version.hpp`): Compile-time version checking via `DMK_VERSION_MAJOR`, `DMK_VERSION_MINOR`, `DMK_VERSION_PATCH`, `DMK_VERSION_STRING`, and `DMK_VERSION_AT_LEAST(major, minor, patch)`. Generated from CMake's `project(VERSION)` at configure time.
@@ -261,11 +267,108 @@ const auto resolved = sc::find_and_resolve_rip_relative(
261
267
- Indirect calls through memory: `FF 15 disp32` and `FF 25 disp32`. The disp32 points to a **pointer**; DMK returns the pointer's address, not the final target. Dereference it yourself.
262
268
- Instructions where the disp32 is interrupted by a SIB byte combination or a VEX/EVEX prefix boundary: supply your own longer `opcode_prefix` that covers up to the disp32 start.
Game binaries change across patches. A single literal AOB that locked onto a specific opcode window in one build is one compiler flag flip away from matching nothing on the next update. The cascade pattern is the standard defence: register several ordered candidates per target (most-specific first, most-generic last), let the scanner try each until one matches, and record the winner so you know which build of the game is actually running. Every long-lived modding community reinvents this eventually; DMK ships it as a first-class API so you do not have to reinvent the logging, the ordering rules, or the prologue-overwrite recovery path.
275
+
276
+
### 6.2 API shape
277
+
278
+
Defined in [include/DetourModKit/scanner.hpp](../../include/DetourModKit/scanner.hpp) inside `namespace DetourModKit::Scanner`:
279
+
280
+
```cpp
281
+
enumclassResolveMode : std::uint8_t
282
+
{
283
+
Direct, // Returned address = match + disp_offset
284
+
RipRelative // Read int32 disp at (match + disp_offset); target = match + instr_end_offset + disp
Both functions take a span so you can pass a `std::array`, `std::vector`, or any contiguous container. The `label` is the human-readable tag used when the winning candidate is logged; the `winning_name` on the returned `ResolveHit` aliases the matched candidate's `name` field, so the storage that holds those `string_view`s must outlive the hit (static string literals or an `std::array` in static storage are the usual patterns). `ResolveHit::address` is the post-resolution absolute address: for `Direct` candidates it equals `match + disp_offset`, and for `RipRelative` candidates it is the target of the displacement already resolved (not the raw match pointer), so callers can hook or call it directly.
"resolved {} at {:#x}", hit->winning_name, hit->address);
348
+
```
349
+
350
+
### 6.4 Prologue fallback variant
351
+
352
+
`resolve_cascade` is fine when the target function still looks the way your signature remembers it. It stops working as soon as another mod, loaded earlier in the process, inline-hooks the same function: SafetyHook, MinHook, and most hand-rolled detour libraries overwrite the first five bytes with a near-JMP (`E9 ?? ?? ?? ??`) to their trampoline. Your Direct-mode candidate that matches on a prologue byte sequence now sees `E9` instead of `48 89 5C 24 ...`, and the scan misses even though the function itself is still present.
353
+
354
+
`resolve_cascade_with_prologue_fallback` handles that exact scenario. On the happy path it is identical to `resolve_cascade`. If every candidate misses, it walks the list again and, for each Direct-mode candidate, rebuilds the pattern with the first five tokens replaced by `E9 ?? ?? ?? ??` while preserving the literal tail. It scans with the rewritten pattern and then applies two guardrails before accepting a hit: the rewritten pattern must match at most four locations in `.text` (so a near-JMP into random padding does not win), and the `E9` displacement must land inside a loaded module (so the jump target is a real function, not garbage). RipRelative candidates are skipped in the fallback phase since they target instructions deeper than the 5-byte prologue and are unaffected by the overwrite.
355
+
356
+
```cpp
357
+
constauto hit = sc::resolve_cascade_with_prologue_fallback(
358
+
k_weapon_fire_candidates, "weapon_fire");
359
+
```
360
+
361
+
There is one guardrail callers must be aware of. The fallback refuses to scan any candidate whose literal tail after the first five tokens contains fewer than five literal bytes, and surfaces that refusal as `ResolveError::PrologueFallbackNotApplicable`. A too-short tail would produce a pattern that matches every near-JMP in the executable region and invent a false positive; rather than gamble, the cascade treats the candidate as unusable for recovery. If you see this error, extend the offending candidate's pattern so it carries at least five literal bytes past the five-byte prologue window.
362
+
363
+
### 6.5 Ordering and logging
364
+
365
+
Put the most-specific candidate first. The cascade returns on the first successful resolution, so an overly-generic pattern placed near the head will shadow tighter patterns further down the list. The `winning_name` on `ResolveHit` tells you which candidate fired; log it or stash it in your mod's telemetry so you can correlate a running session with a specific build of the game after the fact. The cascade also emits an Info-level log line of the form `"<label> resolved via '<name>' at 0x..."` the first time it succeeds, so you get build identification for free even without explicit caller logging.
The raw Scanner API is intentionally low-level. Anything beyond a single call-site benefits from a thin layer above it. Below are patterns battle-tested in consumer projects.
267
370
268
-
### 6.1 Cache the `CompiledPattern`
371
+
### 7.1 Cache the `CompiledPattern`
269
372
270
373
`parse_aob` is cheap but not free. If you scan repeatedly (hot-reload, re-scan after a level load, fallback between candidates), parse once and hold the `CompiledPattern` in a static or a class member:
For a single logical hook, ship two or three signatures: one tight one for the current build, one wider one for the previous build, and a generic one as a safety net. Try them in order, stop on the first hit, log which one won.
305
408
@@ -322,7 +425,7 @@ uintptr_t resolve_first_hit(
322
425
}
323
426
```
324
427
325
-
### 6.3 Verify after match
428
+
### 7.3 Verify after match
326
429
327
430
A lone signature hit is necessary but not sufficient. Two lightweight checks catch the overwhelming majority of mis-hits:
DMK's helpers assume you want to land *on* the match. Real-world hooks sometimes want to step backward from the match, for example to arrive at the function start after anchoring on a later landmark. Store a signed offset per candidate and apply it after the match succeeds:
346
449
@@ -361,13 +464,13 @@ if (!hit) return 0;
361
464
const auto* target = hit + candidate.disp_offset; // may walk backwards
362
465
```
363
466
364
-
### 6.5 Name every candidate
467
+
### 7.5 Name every candidate
365
468
366
469
Anonymous signatures make regressions unreadable. Attach a human-friendly label to every candidate (`"player_ctx_load_v1"`, `"fire_weapon_v2_backcompat"`). Log that label when a hit is found or when all candidates fail. It pays for itself the first time a patch breaks one of thirty signatures.
If your pattern embeds a `|` marker, `find_pattern` has already applied `pattern->offset` to `hit`: pass `hit` directly to `resolve_rip_relative`. Adding `pattern->offset` again would double-apply and advance past the opcode.
388
491
389
-
### 7.2 Resolve a global pointer via `mov rax, [rip+disp32]`
492
+
### 8.2 Resolve a global pointer via `mov rax, [rip+disp32]`
390
493
391
494
```cpp
392
495
// Search 64 bytes from the match for the mov, then resolve.
@@ -406,7 +509,7 @@ auto global_ptr = dmk::Memory::read_ptr_unsafe(
406
509
407
510
If `hit` came from a pattern with a `|` offset marker, `find_pattern` has already applied the offset, so `hit` already points at the marked byte: pass it directly. Adding `pattern->offset` would double-apply and start the search window past the intended opcode.
408
511
409
-
### 7.3 Scan a packed binary
512
+
### 8.3 Scan a packed binary
410
513
411
514
```cpp
412
515
// Code decrypted into anonymous executable pages outside any loaded module.
Reminder: both `find_pattern` overloads return the marked byte when a `|` marker is present (and the match start when it is absent). `pattern->offset` is applied for you; adding it manually double-applies.
439
542
440
-
## 8. DOs and DON'Ts
543
+
## 9. DOs and DON'Ts
441
544
442
545
### DO
443
546
@@ -462,7 +565,7 @@ Reminder: both `find_pattern` overloads return the marked byte when a `|` marker
462
565
-**Don't** ignore a `PrefixNotFound` or `UnreadableDisplacement` error: they almost always mean the signature lost its context, not that the code simply moved.
463
566
-**Don't** trust a single-build signature in a long-lived mod without a fallback.
464
567
465
-
## 9. Troubleshooting
568
+
## 10. Troubleshooting
466
569
467
570
| Symptom | Likely cause | Remedy |
468
571
| ------- | ------------ | ------ |
@@ -474,7 +577,7 @@ Reminder: both `find_pattern` overloads return the marked byte when a `|` marker
474
577
| Works locally, fails on a different machine | Packer or anti-cheat transforming the module between load and scan | Switch to `scan_executable_regions`; add a later re-scan on first frame |
475
578
| Multi-GB scan is slow | Patterns whose only literal bytes are common (`48 8B`, `E8`, etc.) | Broaden the anchor to include a rarer byte; the anchor selector prefers rarer bytes |
0 commit comments