Skip to content

refactor: move the backend-independent infrastructure into core - #21

Closed
ResurrectedTrader wants to merge 1 commit into
mainfrom
shared-core
Closed

ResurrectedTrader wants to merge 1 commit into
mainfrom
shared-core

Conversation

@ResurrectedTrader

@ResurrectedTrader ResurrectedTrader commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Shared infrastructure lifted out of the 1.14d backend, plus fixes to code that was already there. No behaviour change to the bot, and 1.14d keeps the same hooks in the same order at the same install points.

Hooking

Nine call sites each opened their own Detours transaction, kept their own "real function" pointer and their own installed flag, in three different shapes. They now share d2bs::detour in src/core/detour/:

  • Target says where a function lives, a named export of a module or an address the caller already has, resolved at attach time.
  • Hook<Fn> is one typed slot. Its target and replacement are checked against each other at compile time, and Real() reaches the original whether or not the slot is attached, so a caller outside the replacement can use it as a plain handle.
  • Batch is one transaction. It de-duplicates exports that are thunks into a shared body, aborts rather than committing an empty transaction, and only ever touches the slots it queued.

That last property is what fixes a real defect. A failed commit used to leave a site believing it had attached, and the matching Remove() then detached hooks that were never installed: eighteen of them in speedhack, two in the 1.14d hook manager. Realms had two more of its own, dropping the error from its detach commit and nulling its real-function pointers immediately after detaching, while in-flight hook bodies on the Battle.net connect worker could still be calling through them.

InlinePatch and the VMT hook stay as they are; neither is a Detours transaction.

Shared infrastructure moved into core

  • src/core/input/ - the WH_GETMESSAGE hook and the game-window WndProc subclasses, taking their callbacks from the backend through a small struct rather than reaching into it.
  • src/core/proxy/ - the SOCKS5 connect hook.
  • src/core/config/OptionParser - launch-option parsing.

None of this is 1.14d-specific, and none of it belonged in a backend.

Command-line hygiene

OptionParser now takes the switches it parsed back out of the process command line. A CD key passed as -d2c was readable for the rest of the run by anything that can see the PEB: Task Manager, WMI's Win32_Process.CommandLine, a remote read. The switch and its value are cut out and what follows shifts down over the gap, so neither the value nor its length survives and the line still parses as a well-formed command line holding only the arguments the game itself was given.

The cut is skipped entirely when our tokeniser and CommandLineToArgvW disagree on the token count, so an input we do not model is left alone rather than guessed at. RemoveOptions is the buffer-level half, split out so it can be tested; sixteen cases cover quoted values, escaped quotes, bare flags, repeats, a trailing switch with no value, and a program name that spells an option.

It cannot reach the argv array the game's own CRT built at its startup from its own private copy. That is readable only by something already walking the process heap, which is a different threat from anything that reads a process's command line.

Logging

utils::GetLogger(name) hands out named loggers over one shared fan-out sink, so where output goes is decided once by AddLogSink rather than depending on when a logger happened to be created. That also fixes a routing bug: loggers created before the sinks were installed used to copy the default logger's sink list at creation and were orphaned forever once the real sinks arrived. Raw spdlog:: calls are gone.

Two small accessors make the levels controllable later: SetLogLevel(name, level) sets one logger by exact name, applying immediately if it exists and remembering the level if it does not yet, and Loggers() lists the loggers that exist with the level each is emitting at, growing as components log for the first time. Between them a settings panel can render the list and adjust an individual component. Nine tests cover the pair, including that a level set before a logger exists is applied when it is created, and that matching is exact rather than by prefix.

Default volume is deliberately unchanged from before this branch: the framework's own d2bs logger runs at debug, every other logger keeps spdlog's default of info. The only change to flushing is that a logger now flushes from debug rather than warn, so entries are on disk before a crash rather than sitting in a buffer.

A rotating file log now sits alongside the console sink, capped and rolled, under logs/ next to the ini. Failing to open it is not fatal.

Contract

InstallHooks returns bool, and the frontend aborts initialisation when a backend cannot bring its hooks up, instead of running on half-installed ones. game::console::BackendPanel lets a backend contribute console tabs as an abstract class rather than a list of function pointers.

Platform

utils, contract, core and js contain no game-version-specific code, so they now build for x64 as well as Win32 and CI compiles both. The 1.14d backend, its glue and the tests stay Win32, and the solution maps that.

Verification

Check Result
build.ps1 Release succeeded, 0 warnings
build.ps1 Release -Platform x64 succeeded, 0 warnings
build.ps1 test 152 passed (25 new)
build.ps1 check-format clean
build.ps1 lint 118 passed, 0 failed

@ResurrectedTrader
ResurrectedTrader force-pushed the shared-core branch 5 times, most recently from 70ee684 to fa9b692 Compare September 15, 2026 14:53
@ResurrectedTrader
ResurrectedTrader changed the base branch from main to deps-baseline September 15, 2026 14:53
@ResurrectedTrader
ResurrectedTrader force-pushed the deps-baseline branch 2 times, most recently from b5239dd to 60c3aea Compare September 15, 2026 16:19
Base automatically changed from deps-baseline to main September 16, 2026 19:57
Everything here is either shared infrastructure lifted out of the 1.14d
backend, or a fix to code that was already there.

Hooking. `src/core/detour/` is one typed Detours API: `Target` says where a
function lives, `Hook<Fn>` is a slot whose target and replacement are
checked against each other at compile time and whose `Real()` reaches the
original whether or not it is attached, and `Batch` is one transaction that
de-duplicates exports sharing a body and aborts rather than committing an
empty update. The call sites had each rolled their own, and two of them
carried the same defect: a failed commit left the site believing it had
attached, so the matching remove detached hooks that were never installed -
eighteen of them in speedhack. Realms additionally dropped its detach error
and nulled its real-function pointers while in-flight hook bodies could
still be running. A failed `DetourTransactionBegin` is now aborted rather
than left pending: one of its two failure modes claims the process-wide
transaction slot before returning the error, and leaving that claimed
wedges every later attach and detach in the process.

Shared infrastructure. The `WH_GETMESSAGE` input hook and the game-window
WndProc subclasses move to `src/core/input/`, taking their callbacks from
the backend rather than reaching into it. Those callbacks are plain
function pointers, like the ones in `GameCallbacks` they are fed from, and
each dispatch loads its pointer once - `Remove()` clears the table from a
different thread than the one dispatching, which under `std::function`
destroyed a callable while it was executing. The SOCKS5 connect hook moves
to `src/core/proxy/`. Command-line parsing moves to
`src/core/config/OptionParser`, which also now removes the switches it
parsed from the process command line, so a CD key passed as `-d2c` stops
being readable through the PEB, through `GetCommandLineW` and through the
ANSI copy once it has been consumed. Twenty-three tests cover the removal.

Logging. `utils::GetLogger(name)` hands out named loggers over one shared
sink, so where output goes is decided once by `AddLogSink` rather than by
when a logger happened to be created - which also fixes loggers made before
the sinks were installed being orphaned from them. Default volume is
unchanged: the framework's own logger at debug, everything else at
spdlog's default. Loggers flush themselves from warn, with a one-second
periodic flush underneath, so a crash loses at most the last second rather
than forcing an fflush on every line inside the shared sink's lock. Raw
`spdlog::` calls are gone.

Contract. `InstallHooks` returns bool, and the frontend aborts init when a
backend cannot bring its hooks up rather than running on half-installed
ones. `game::console::BackendPanel` lets a backend contribute console tabs
as an interface instead of a list of function pointers. A rotating file log
sits alongside the console sink, under logs/ next to the ini.

Platform. `utils`, `contract`, `core` and `js` hold no game-version
specific code, so they now build for x64 as well as Win32 and CI compiles
both. The 1.14d backend, its glue and the tests stay Win32.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ResurrectedTrader

Copy link
Copy Markdown
Owner Author

Superseded. This branch was split into #23, #24, #25, #26 and #28, all of which are now merged to main. Closing rather than merging a duplicate.

@ResurrectedTrader
ResurrectedTrader deleted the shared-core branch September 16, 2026 22:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant