# Two Ways to Wear a Character

https://jaredrhodes.com/blog/two-ways-to-wear-a-character/

There are two ways to make a character in a virtual world do something. You can move the hands that hold the controller, or you can be the controller.

Westworld of Warcraft does both, on purpose, and the tension between them is the most productive constraint in the codebase.

## Pick One and You Lose Something

**Inject into the real client only.** Every bot needs a `WoW.exe` process, a window, a GPU context, and roughly a gigabyte of address space. Thirty bots is a heroic machine. Three thousand is a data center. Continuous integration on a headless Linux runner is off the table permanently.

**Emulate the protocol only.** Now a bot is cheap - hundreds per machine, no GUI, trivially scriptable in CI. But you have inherited the entire client. Movement physics, collision, transport state, spell timing, update-field semantics: all of it now lives in code you wrote, and the only way to know whether you got it right is to compare against the thing you were trying to avoid running.

So both ship. The foreground runtime is the **oracle**. The background runtime is the **fleet**.

## One Engine, Two Bodies

<figure class="diagram">
  <a href="/assets/diagrams/westworld-of-warcraft/execution-modes.svg" target="_blank" rel="noopener" title="Open the full-size diagram"><img src="/assets/diagrams/westworld-of-warcraft/execution-modes.svg" width="1000" height="570" alt="Foreground and background execution modes side by side: foreground runs a real WoW client with an injected loader, CLR bootstrap, and direct memory access, background runs headless with a C# protocol client, movement controller, and ported physics engine, both sitting on a shared core of game interfaces, behavior engine, profiles, and protobuf transport, with a parity rule stating foreground wins disagreements" loading="lazy" decoding="async" data-theme-filter="off" /></a>
</figure>

| | Foreground | Background |
| --- | --- | --- |
| Process | Inside a live `WoW.exe` | Its own headless process |
| Game state | Direct memory reads and writes, plus the client's own Lua | Parsed from `SMSG_*` packets into an object manager |
| Movement | The client's real physics | `PhysicsEngine.dll`, ported from the client binary |
| Cost per bot | One full game client | A socket and a state machine |
| Runs in CI | No | Yes |
| Authority | Ground truth | Must match ground truth |

What they share is everything above the seam: `GameData.Core` interfaces, the `BotRunner` behavior engine, the class and spec rotation profiles, and the protobuf transport. A task like `GoToTask` has no idea which runtime it is executing in. That is the whole design goal - one behavior engine, two ways of reaching the world.

## Getting Inside the Client

The foreground path is process injection with a .NET twist, and the twist is the interesting part.

<figure class="diagram">
  <a href="/assets/diagrams/westworld-of-warcraft/foreground-injection-pipeline.svg" target="_blank" rel="noopener" title="Open the full-size diagram"><img src="/assets/diagrams/westworld-of-warcraft/foreground-injection-pipeline.svg" width="1000" height="430" alt="Foreground injection pipeline: the state manager launches or locates the client, waits for the window, opens the process and allocates remote memory, writes the loader path, and creates a remote thread on LoadLibrary; inside the client the loader entry point runs, hostfxr resolves the .NET runtime, the CLR loads the bot runner, Warden is disabled on legacy clients, and the bot connects back over protobuf on TCP" loading="lazy" decoding="async" data-theme-filter="off" /></a>
</figure>

The host side is conventional Windows work:

1. `WoWStateManager` launches or locates the client process.
2. It waits for a real window and a real world state - no fixed sleeps, per the no-blind-sequences rule.
3. `OpenProcess`, then allocate memory inside the target.
4. Write the path to `Loader.dll` into that allocation.
5. Point `CreateRemoteThread` at `LoadLibrary` with that path.

The in-process side is where .NET 8 changes the old recipe. The classic injection tutorial uses `mscoree.dll` and the .NET Framework hosting API. That API still exists on Windows; what it cannot do is host .NET 8. `Loader.dll` instead uses **hostfxr**:

```text
Loader.dll entry point
  -> resolve hostfxr via nethost
  -> hostfxr_initialize_for_runtime_config(ForegroundBotRunner.runtimeconfig.json)
  -> get_function_pointer(load_assembly_and_get_function_pointer)
  -> load ForegroundBotRunner.dll
  -> invoke ForegroundBotRunner.Loader::Load
```

Three consequences fall out of that, and each one bit me before it got written down:

1. **A `runtimeconfig.json` is mandatory.** Hosting a modern runtime means initializing it from a declared configuration. Ship the config next to the assembly or nothing happens.
2. **The entry point signature is fixed.** A static method with the exact expected shape. Get it wrong and you get a null function pointer with no diagnostic.
3. **Bitness is not negotiable.** The 1.12.1 client is 32-bit, so `Loader` and `FastCall` build as x86. The native navigation and physics library builds x64 because it lives in the services. Two toolchains, one solution, and the build script tells you which one is missing.

Bootstrap runs on its own thread rather than in `DllMain`, because doing real work under the loader lock is how you deadlock a game client. A shutdown event is signaled on process detach so teardown is deterministic.

Once managed code is live inside the process, the bot has what no protocol client can have: the client's own object manager, the client's own Lua state, and the client's own physics already computed. It reads the player's position out of memory rather than deriving it, and calls game functions directly through structured-exception-wrapped thunks so a bad call surfaces as an error instead of taking the process with it.

Warden, the legacy anti-cheat, is disabled on injection. On a private research server with no competitive stake this is housekeeping, not evasion - the alternative is the client terminating itself mid-experiment.

### Two Operational Rules Learned the Hard Way

**Kill the client before you build.** The injector loads native DLLs from the build output directory. A running client holds a lock on them, and MSBuild reports it as a file-copy error that looks nothing like the actual cause. I lost more time to that one message than I care to admit.

**Version the offsets.** Memory offsets are specific to exact client builds - 1.12.1 build 5875, 2.4.3 build 8606, 3.3.5a build 12340. A bot that logs in and then does nothing intelligible is almost always a client-build mismatch.

## Being the Client Instead

The background runtime never touches a game client. `WoWSharpClient` is a pure C# implementation of the wire protocol: well over a hundred distinct opcodes handled in each direction.

Here is the stack it has to reproduce:

- **Auth** - SRP6 challenge and proof against the realm daemon, then the realm list.
- **World handshake** - session-key proof and header encryption on the world connection.
- **Object updates** - parse `SMSG_UPDATE_OBJECT` and its update masks into a live object graph.
- **Movement** - emit `MSG_MOVE_*` with correct flags, and pair server acknowledgements with the state transitions that caused them.
- **Transport** - track boat, zeppelin, and elevator state so a bot on a moving object has coherent coordinates.

The movement layer is where the difficulty concentrates, because movement is the one thing the server actively checks. The parity contract is explicit:

- The background runtime sends the same opcode, at the same flag state, with the same payload the real client would send.
- Timing tolerance is +/-100 ms for self-initiated movement and +/-10 ms for server-initiated movement such as a forced root, a forced speed change, or a teleport.
- Server acknowledgements are paired by opcode and state transition. A mismatched acknowledgement is a bug.

Ten milliseconds for server-initiated movement sounds severe until you watch a bot get rooted and answer with the wrong acknowledgement. The server's correction fights the client's state, and the character stutters in place like a bad connection. Which, from the server's point of view, is exactly what it is.

## The Parity Discipline

Two implementations of the same behavior will drift. The only question is whether you find out from a test or from a screenshot.

The rule is short: **when the runtimes disagree, the foreground is right.** The background implementation is a port of the client's behavior, so a difference is by definition a porting defect.

That rule needs teeth, and the teeth are what counts as proof. A parity row closes on decompilation evidence naming a specific routine in the client binary, plus a canary that fails before the fix and passes after it. A recorded session that replays without visible error, a test asserting that a named route completes, and a screenshot of a bot standing in the right place can all be true while the port is still wrong. The split gets its row-by-row treatment in [Teaching a Bot to Walk](/blog/teaching-a-bot-to-walk/), because the physics port is where it has to hold.

Packet capture and movement recording from the foreground runtime are still valuable, as **observation**. A capture tells you something differs. It does not tell you which routine differs, and it cannot close an implementation row on its own.

This distinction is the difference between a port that converges and a port that oscillates forever. Replay harnesses feel like proof because they are red and then green. They are actually a very expensive way to notice that something changed.

## The Slope That Looked Like Success

The divergence that made me write the parity rule down never raised anything: a background bot climbed a Redridge slope the real client refuses, arrived, verified its task, and left a clean snapshot, and the disagreement only surfaced weeks later when a foreground bot on the same route took the long way around and blew a group-form timeout. I tell that story properly in [Teaching a Bot to Walk](/blog/teaching-a-bot-to-walk/), where the physics argument lives.

What it settles here is the direction of blame. The cheap runtime was wrong in a way that looked like success, so widening the timeout would have buried the only signal I had. The disagreement itself is the artifact worth keeping: reduce it to a slope-threshold canary against the client's own collision behavior, fix the native side, and let the timeout stand.

Most of the operational rules on this project fall out of that same instinct. When the background moves where the foreground cannot, the fix goes into the background physics rather than into the mesh that would hide it. When foreground offsets stop working, confirm the exact client build before touching a line of logic. When security software blocks injection, allow the build output rather than weakening the loader. When the native DLL copy fails during a build, find the specific client process holding the lock and kill that one. And when the background's acknowledgement stops matching after a forced root, it is a movement-protocol bug, not server flakiness.

## When the Two Bodies Agree

Most of what I check is plumbing. A behavior task compiles and runs unchanged in both bodies. Foreground injection is reliable against all three supported client builds. A background bot logs in, enters the world, moves, fights, and logs out with no client present, and its movement holds the packet parity contract inside the stated timing tolerances. The whole background suite runs in continuous integration with no GUI anywhere near it.

Two of the criteria are the ones that decide whether the oracle is worth having. Every closed physics parity row has to cite a specific routine in the client binary and carry a canary that fails without the fix, because a row closed on a passing replay is a row that will quietly reopen. And a disagreement between the runtimes always opens a defect against the background implementation. That is a promise about where blame goes, and keeping it is what makes the foreground worth trusting.

## Related Posts

The behavior engine both runtimes share is the subject of [Activity, Objective, Task, Action](/blog/activity-objective-task-action/). The physics port introduced here gets its own treatment in [Teaching a Bot to Walk](/blog/teaching-a-bot-to-walk/). For the wider system, start with [A Server That Plays Itself](/blog/westworld-of-warcraft-a-server-that-plays-itself/).

## References

- [Write a custom .NET host to control the .NET runtime](https://learn.microsoft.com/en-us/dotnet/core/tutorials/netcore-hosting)
- [.NET runtime configuration files](https://learn.microsoft.com/en-us/dotnet/core/runtime-config/)
- [CreateRemoteThread function](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createremotethread)
- [Dynamic-link library best practices](https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices)
- [Secure Remote Password protocol](https://datatracker.ietf.org/doc/html/rfc2945)
