Skip to content

Commit 25b7daa

Browse files
authored
feat(config): add INI hot-reload via file watcher and hotkey (#72)
1 parent d6fd002 commit 25b7daa

10 files changed

Lines changed: 3041 additions & 8 deletions

File tree

AGENTS.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ include/DetourModKit/ # Public headers -- one per module
8787
logger.hpp # Synchronous singleton logger
8888
win_file_stream.hpp # Win32 shared-access file stream (CreateFile backend)
8989
config.hpp # INI configuration with callback setters
90+
config_watcher.hpp # Filesystem watcher (ReadDirectoryChangesW) for INI hot-reload
9091
input.hpp # Input polling (keyboard/mouse/XInput)
9192
input_codes.hpp # Unified InputCode type and named key tables
9293
memory.hpp # Memory read/write, sharded region cache
@@ -228,7 +229,7 @@ dispatcher.emit_safe(PlayerStateChanged{.health = player->health});
228229
- **Concurrency tests:** Use `std::atomic<bool> stop` flag pattern with multiple threads. See `AsyncMode_ConcurrentLogAndDisable` in `test_logger.cpp` for the reference pattern.
229230
- **Build flag:** Tests are enabled with `DMK_BUILD_TESTS=ON` (on by default in debug presets).
230231
231-
For detailed coverage analysis, see [docs/tests/README.md](docs/tests/README.md). For hot-reload testing patterns, see [docs/hot-reload/README.md](docs/hot-reload/README.md). For AOB signature construction, the Scanner API, and RIP-relative resolution, see [docs/misc/aob-signatures.md](docs/misc/aob-signatures.md).
232+
For detailed coverage analysis, see [docs/tests/README.md](docs/tests/README.md). For hot-reload testing patterns, see [docs/hot-reload/README.md](docs/hot-reload/README.md). For INI hot-reload (filesystem watcher and reload hotkey), see [docs/config-hot-reload/README.md](docs/config-hot-reload/README.md). For AOB signature construction, the Scanner API, and RIP-relative resolution, see [docs/misc/aob-signatures.md](docs/misc/aob-signatures.md).
232233
233234
After any code change, build and run the full test suite before committing:
234235
@@ -259,7 +260,8 @@ PATH="/c/msys64/mingw64/bin:$PATH" ./build/mingw-debug/tests/DetourModKit_tests.
259260
| InputPoller | Atomic `active_states_[]` array | `memory_order_relaxed` load per binding |
260261
| InputManager | `mutex` for lifecycle, `atomic<InputPoller*>` for reads | Lock-free `is_binding_active()` |
261262
| Memory cache | Sharded `SRWLOCK` + epoch-based shutdown | Shared reader locks per shard |
262-
| Config | `mutex` for registration; deferred setter invocation outside lock (no reentrancy guard needed -- setters may call back into Config) | N/A (startup only) |
263+
| Config | `mutex` for registration; deferred setter invocation outside lock (no reentrancy guard needed -- setters may call back into Config); `reload()` re-runs the registered items against the stashed INI path using the same deferred pattern and short-circuits on FNV-1a 64 hash match of the on-disk bytes to skip no-op reloads; bytes are read once per load/reload and fed to `CSimpleIniA::LoadData`, so the cached hash and the parsed INI state are guaranteed to reflect the same file snapshot (no TOCTOU between hash and parse); `enable_auto_reload()` owns a `ConfigWatcher` behind a separate `std::mutex` so start/stop transitions do not contend with registration traffic; setters invoked by the watcher run on the watcher thread, setters invoked by the reload hotkey run on a dedicated `ReloadServicer` thread (lazily started on first `register_reload_hotkey`, torn down in `clear_registered_items()`) so the `InputManager` poll thread never blocks on INI parsing; the servicer's press-request path takes its internal `m_mutex` around the predicate store before `cv.notify_one` to close the lost-wakeup window; all setters must be reentrant and thread-safe | N/A (startup only) |
264+
| ConfigWatcher | One `StoppableWorker` per instance; worker opens the parent directory with `FILE_FLAG_BACKUP_SEMANTICS` and `FILE_FLAG_OVERLAPPED`, then pumps `ReadDirectoryChangesW` via `GetOverlappedResultEx` with a 100 ms timeout so `stop_token` is observed promptly; debounce uses `steady_clock`; filename match is case-insensitive; `start()` and `stop()` are idempotent and serialized by an internal `std::mutex` | 100 ms `GetOverlappedResultEx` pump; idle CPU ~0 |
263265
| EventDispatcher | Lock-free `emit()` / `emit_safe()` via `std::atomic<std::shared_ptr<const std::vector<Entry>>>` snapshot (copy-on-write publish, acquire-load on read); zero-subscriber fast path skips the snapshot load via an atomic handler counter; writers serialize on a small `std::mutex` that never touches the emit hot path; thread-local reentrancy guard rejects subscribe/unsubscribe from within handlers so the no-mutation-during-emit invariant holds; `emit()` propagates handler exceptions, `emit_safe()` catches and skips them | Atomic acquire-load of a `shared_ptr` snapshot plus linear iteration over a contiguous vector; no reader lock |
264266
| Profiler | Lock-free ring buffer via atomic `fetch_add` on write position; odd/even sequence counter per sample slot prevents torn reads during concurrent export -- the sequence is opened and closed with unconditional `fetch_add` (never a load-then-store) so concurrent producers racing on the same slot cannot roll the counter backwards; `DMK_PROFILE_SCOPE(name)` requires `name` to be a string literal, enforced at compile time by a `ScopedProfile` constructor that only binds to `const char (&)[N]` | Single atomic increment + sequence-guarded field writes per sample |
265267

CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ configure_file(
144144
# --- DetourModKit Library Target Definition ---
145145
# Glob source files. CONFIGURE_DEPENDS helps CMake re-glob on CMakeLists changes.
146146
# For brand new files, a manual CMake re-run might still be needed.
147+
#
148+
# Notable modules picked up here (alphabetical, not exhaustive):
149+
# async_logger, bootstrap, config, config_watcher, event_dispatcher,
150+
# filesystem, hook_manager, input, logger, memory, profiler,
151+
# scanner, win_file_stream, worker.
147152
file(GLOB DETOURMODKIT_SOURCE_FILES
148153
CONFIGURE_DEPENDS
149154
"src/*.cpp"

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ DetourModKit is a lightweight C++ toolkit designed to simplify common tasks in g
1313
|--------|-------------|--------|
1414
| AOB Scanner | SIMD-accelerated pattern scanning with wildcards, RIP resolution, and multi-candidate cascade resolver with prologue fallback | `scanner.hpp` |
1515
| Hook Manager | Inline, mid-function, and VMT hooks via SafetyHook with cross-module duplicate-hook detection | `hook_manager.hpp` |
16-
| Configuration | INI-based settings with key combo support | `config.hpp` |
16+
| Configuration | INI-based settings with key combo support and hot-reload (file watcher + hotkey) | `config.hpp`, `config_watcher.hpp` |
1717
| Logger | Synchronous singleton logger with format strings | `logger.hpp` |
1818
| Async Logger | Lock-free bounded queue logger with batched writes | `async_logger.hpp` |
1919
| Memory Utilities | Readability checks, region cache, and safe pointer reads | `memory.hpp` |
@@ -62,9 +62,40 @@ DetourModKit is a lightweight C++ toolkit designed to simplify common tasks in g
6262
- Format: `modifier+trigger` (e.g., `Ctrl+Shift+F3`)
6363
- Comma-separated independent combos (e.g., `F3,Gamepad_LT+Gamepad_B`)
6464
- Named keys (`Ctrl`, `F3`, `Mouse1`, `Gamepad_A`), hex VK codes (`0x72`), and mixed formats
65+
- **Hot-reload** (see [Config Hot-Reload Guide](docs/config-hot-reload/README.md)):
66+
- `Config::reload()` re-runs every registered setter against the last-loaded INI without touching registrations; skips setters when the on-disk bytes are byte-identical to the last load (FNV-1a content hash)
67+
- `Config::enable_auto_reload()` starts a background `ConfigWatcher` (`config_watcher.hpp`) that debounces editor save-flurries and triggers `reload()` automatically; returns an `AutoReloadStatus` enum indicating outcome
68+
- `Config::register_reload_hotkey()` wires a user-configurable key combo to `reload()` via the kit `InputManager`; the press callback hands off to a dedicated reload-servicer thread so the input poll thread never blocks on INI parsing
6569

6670
</details>
6771

72+
### Config hot-reload
73+
74+
Two mechanisms share the same `Config::reload()` primitive - use either or both:
75+
76+
```cpp
77+
// 1. Initial load stashes the INI path.
78+
Config::load("mymod.ini");
79+
80+
// 2. Filesystem watcher: auto-reload on file change (250 ms debounce).
81+
// on_reload receives true when setters actually ran, false when the
82+
// content-hash short-circuit skipped the work.
83+
(void)Config::enable_auto_reload(std::chrono::milliseconds{250},
84+
[](bool content_changed)
85+
{
86+
if (content_changed)
87+
{
88+
Logger::get_instance().info("Config reloaded");
89+
}
90+
});
91+
92+
// 3. Hotkey: user presses Ctrl+F5 (or whatever the INI says) to force reload.
93+
Config::register_reload_hotkey("ReloadConfig", "Ctrl+F5");
94+
InputManager::get_instance().start();
95+
```
96+
97+
See the [Config Hot-Reload Guide](docs/config-hot-reload/README.md) for the thread-safety contract, debounce rationale, rename-swap-save handling, and the list of settings that are safe to hot-reload vs restart-required.
98+
6899
<details>
69100
<summary><strong>Logger</strong></summary>
70101
@@ -219,6 +250,7 @@ For detailed coverage analysis and test architecture, see the [Test Coverage Gui
219250
220251
* [AOB Signature Scanning Guide](docs/misc/aob-signatures.md) - Pattern syntax, RIP-relative resolution, and patch-proof signature practices
221252
* [Hot-Reload Development Guide](docs/hot-reload/README.md) - Development workflow for iterating on hooks with live reload
253+
* [Config Hot-Reload Guide](docs/config-hot-reload/README.md) - INI filesystem watcher and hotkey-triggered `Config::reload()`
222254
* [Test Coverage Guide](docs/tests/README.md) - Coverage analysis, test architecture, and module-level breakdown
223255
224256
## Prerequisites

0 commit comments

Comments
 (0)