A free, open-source Windows game trainer — an offline memory editor and cheat engine for PC games, for when you want a Cheat Engine/WeMod alternative that's transparent about what it's doing, doesn't phone home, and doesn't require a closed-source client to use cheats someone else built. Electron + React + TypeScript over a C++ N-API addon that talks to a target process's memory directly — no server, no telemetry, no always-online requirement.
Two ways to cheat:
- Value cheats — find an address, write it repeatedly (a "freeze"), or
write it once ("one-shot"). A cheat can also be anchored: a small capture
patch records the game object it belongs to, and the cheat writes a field of
that object (optionally following one pointer field first,
derefOffset). That keeps cheats working across restarts in games with no readable metadata, such as Unity IL2CPP and Unreal. - Code patches — rewrite the instruction that writes a value, so the game itself never puts the old value back. NOP it out, replace it, force a fixed result, or skip a method entirely for one object.
Ships with cheat sets for five games, one profile each in games/:
| Game | Cheats | Engine / notes |
|---|---|---|
| Valheim | 18 | Mono JIT (valheim.json) |
| Elden Ring | 14 | Native, pointer-chain value cheats (start_protected_game.json, named for its EAC-protected executable) |
| Palworld | 8 | Unreal (Palworld-Win64-Shipping.json) |
| Aviassembly | 12 | Unity/Mono (aviassembly.json) |
| Schedule I | 26 | Unity IL2CPP (Schedule I.json) — see games/schedule-i-notes.md for how each was found |
The newest Schedule I cheats (invisibility, no arrest, clone items, the instant timers and others) were built from the game's code and checked against a recording of the game build, but not every one has been confirmed in-game yet; treat any you have not tried as unproven. The engine isn't tied to any of these games — see Sharing cheats for adding your own, or Using the cheat factory to have an AI agent draft a Unity game's cheat list for you.
Windows only. The native addon's injection path is Win32; Linux is stubbed out but not implemented (see
native/src/platform/platform_linux.cc).
Grab the latest Apprentice-Setup-<version>.exe from Releases
and run it — no admin rights needed, it installs to your user profile.
Prefer to build it yourself, or want to hack on it? Keep reading.
Requirements: Node.js 26 to run the tests (they load a TypeScript worker and the compiled
.node addon natively), a recent npm, and the
Visual Studio Build Tools
(C++ workload) for compiling the native addon. The addon builds under Node 22 (what CI uses)
and, with the current node-gyp 13, under Node 26 as well; it is N-API, so a build from either
loads under both.
npm install
cd native && npx node-gyp configure && npx node-gyp build && cd ..
npm run buildpackage.json's allowScripts lists the exact package versions whose install scripts may
run (Electron's binary download and esbuild). When you upgrade Electron or esbuild, update
that entry too, or npm skips the script and the binary is never downloaded.
Run it with Apprentice.cmd at the repo root, or package a real installer:
npm run distproduces release\Apprentice-Setup-<version>.exe (NSIS, no admin required).
Stop Apprentice before rebuilding the addon — a running instance locks
memory_addon.node and the link fails with permission denied.
npx vitest run # unit + native-harness tests
npx tsc --noEmit # type check
npm run build # production bundle actually compiles.github/workflows/ci.yml runs on every push and pull request: it builds the native
addon, builds the MCP server, type-checks, compiles the production bundle, and runs the
whole test suite (native-harness tests included) on a Windows runner.
To publish a release, bump version in package.json and push it to master. If
there is no release for that version yet, CI builds the installer, creates the tag
v<version> and a GitHub Release (a prerelease when the version has a -, like
0.1.2-beta), attaches Apprentice-Setup-<version>.exe plus a SHA256SUMS.txt, and lets
GitHub generate the notes. Those list the merged pull requests since the previous release
and link the full comparison, so work pushed straight to master shows up only as that
comparison link. A push that doesn't change the version publishes nothing.
To check that a build works without publishing, run Actions → CI → Run workflow on
any branch other than master: it builds the installer and attaches it to the run as an
artifact (kept for 3 days) instead of releasing it.
The installer is not code-signed, so Windows SmartScreen will warn on first run. The workflow builds the addon on the Windows 2022 image with Python 3.11 and Node 22, and runs the tests on Node 26. Those pins were chosen for the old node-gyp 9; the comments in the workflow explain why. node-gyp is now 13, so some may no longer be needed, but that has not been tried in CI, so they are left as they are.
Opening Apprentice shows your installed Steam games as cover tiles, with the ones it has cheats for first. Pick a game to see its cheats; if it is not running, Play starts it through Steam and Apprentice attaches by itself when it opens. Nothing is changed in a game until you switch a cheat on.
Steam is found automatically, wherever it is installed: the registry entry first,
then the usual folders, then every library folder Steam lists (so a second drive
such as D:\SteamLibrary is picked up with no setup). Cover art comes from Steam's
own on-disk cache, so the library works offline and Apprentice makes no network
requests for it; a game with no cached art gets a coloured placeholder. Games from
other launchers are not listed, but any running process can still be attached from
Tools -> Attach to process.
Signature building and patch relocation are where real games broke things the
harness never could (identical JIT copies, code near a region edge). Fixture
replay records a game's executable memory once, then re-runs the real signature
builder and PatchEngine.locate() against that recording on any machine.
# with the game running; each trailing argument is one patch site to test
node mcp-server/scripts/recordSnapshot.js <pid> valheim <build-label> \
fixtures/snapshots/valheim-<build>.snap <patchId>:<address>:<length>Add the site to tests/fixtures/manifest.json (signature, offset, original
bytes, snapshot SHA-256), then npx vitest run tests/replay. Snapshots contain
the game's code, so they stay local (fixtures/snapshots/ is gitignored and the
recorder refuses to write elsewhere in the repo); a site whose snapshot is not on
your machine is skipped, and CI runs only the synthetic tier
(tests/main/replay.synthetic.test.ts). A snapshot is a best-effort copy of a
running process, so record with the game idle at a menu, not mid-load.
tests/native/*.test.ts don't touch a real game — they drive
test-harness/harness.exe, a small standalone Windows binary
(test-harness/harness.c) built for exactly this: a real process with known
values at known addresses to scan for, freeze, patch, and watch. Each test
file spawn()s it directly (spawn(path.resolve('test-harness/harness.exe')))
and talks to it over stdin/stdout — npx vitest run handles all of this for
you, there's no separate step to start it yourself.
The protocol is one command per line in, one OK <result> (or an error) per
line out. A few of the commands, for a sense of what's being exercised:
| Command | What it does |
|---|---|
drainloop |
Spin-writes a counter down — patch_ops.test.ts NOPs the write and checks it stops draining. |
forceloop / shieldloop |
Keep re-asserting a value — proves a force/guard-mode injection actually pins it. |
tight_write |
A real movss [reg], xmm store, back-to-back with no slack — the tightest case write_watch has to decode correctly. |
bigalloc / bigcode (+free variants) |
Allocate an 8 MiB data/executable region, replying OK <0xbase> — used to prove chunked region reads never drop a value straddling a 4 MiB chunk boundary. |
loaddll / loaddll2 / unloaddll |
Load/unload a real DLL (probe.dll/probe2.dll) into the harness process — what module_info.test.ts uses to prove module-anchored patches survive a DLL reloading at a different base. |
Full command set and exact reply formats live in harness.c itself — it's
short and worth skimming before adding a test that needs a new one.
Two hazards if you're adding tests around it (see CODEBASE_MAP.md for
the full explanation): cave_ops.test.ts and module_info.test.ts must each
keep exactly one top-level beforeAll — a second one spawning its own
AsyncWorker reliably segfaults the vitest worker — and scans in the native
tests are one-shot, keyed off a field's initial value, so the first test to
touch a given field "wins" it.
If you change harness.c itself, rebuild it from PowerShell, not Bash
(Bash won't run vcvars and fails silently):
& cmd.exe /c 'call "...\vcvars64.bat" >nul 2>&1 && cl.exe /nologo /Fe:test-harness\harness.exe test-harness\harness.c'then delete harness.obj and confirm the timestamp on harness.exe changed.
PRs welcome — bug fixes, new cheats for existing games, or support for a new game's Mono/IL2CPP layout.
- Fork, branch off
master. - Read
CODEBASE_MAP.mdfirst. It's written for someone picking this up cold — the layer map, the eight patch modes, the non-negotiable safety rules (never displace a RIP-relative instruction, never guess on an ambiguous signature, always restore on quit), and why each of those rules exists, are all there. Read it before touchingnative/srcorpatchEngine.tsespecially — several of those rules were learned by crashing a live game. - Make the change. Match the surrounding code's comment density and idiom — this codebase explains why, not just what, and PRs are expected to keep that up.
- Run the full test suite (
npx vitest run,npx tsc --noEmit,npm run build) before opening the PR. The native test harness is real but limited — it's a static MSVC binary, not a Mono JIT target, so passing there is necessary but not sufficient for anything touching signatures, injection, or code-cave layout. Say in the PR if you validated a change in-game and against which game/build. - Open the PR against
masterwith a clear "what and why." Small, focused PRs review faster than one that bundles an unrelated refactor with a feature.
Found a bug but not fixing it yourself? Open an issue — include the game, the cheat (if applicable), and what you expected vs. what happened.
mcp-server/ is a standalone, read-only MCP
server that exposes the same native memory-introspection primitives
Apprentice itself is built on — attach, scan, Mono class/field/method
resolution, read, disassemble, write-watch — as tools an AI coding agent
(Claude Code, etc.) can call directly against a live game process. It never
writes to the game's memory by design: it's for finding the
address/offset/signature a new cheat needs, not for installing one. (Its one
tool that writes anything, author_cheats, writes a draft profile file on
disk, and touches game memory only through a scratch buffer; see below.) See
docs/superpowers/specs/2026-08-22-mcp-memory-server-design.md for the full
design rationale.
This is what makes "find this field, resolve that class, watch this write" a conversation instead of a manual Cheat Engine session — useful both for building out a new game's cheat set and for debugging why an existing patch stopped locating.
Setup:
cd mcp-server && npm install # also builds dist/ via the prepare hookThe native addon must already be built (native/build/Release/memory_addon.node
— see Building from source above); this package
require()s it directly rather than shipping its own copy. .mcp.json at
the repo root registers the server (game-memory) pointing at
mcp-server/dist/index.js — an agent working in this repo picks it up
automatically. Full tool list, dependency-pinning notes (there's a real
reason @modelcontextprotocol/sdk is pinned exactly, not ranged), and more
detail live in mcp-server/README.md.
For a Unity Mono or Unity IL2CPP game, or a native game that has an entry in
mcp-server/src/factory/nativeGames.ts (currently Elden Ring), author_cheats turns a
wishlist of categories into draft cheats and an in-game checklist, instead of a
reverse-engineering session per cheat. It is an MCP tool, so you drive it through an AI agent
(Claude Code picks up the game-memory server from .mcp.json when you work in this repo), or
with a script (below).
Before you start: build the native addon and the MCP server (see Setup above), start the game, and load into a save or world. The factory reads live objects, and a game sitting at its main menu has none yet. Leave the game idle rather than mid-load.
1. Run it. Ask the agent something like "attach to Schedule I and draft health, stamina and bank cheats". It calls, in order:
list_processes -> find the game's pid
attach(pid) -> a handle
fingerprint_process(handle) -> unity-mono, unity-il2cpp, or native-unknown (native games in nativeGames.ts only)
author_cheats(handle, ["health","stamina","bank"], "games/<Game>.json")
profilePath is where the live profile is (or will be); the output goes beside it as
<Game>.draft.json. You can skip the agent with the script for the game's engine:
node mcp-server/scripts/authorLive.js <pid> "games/<Game>.json" health,stamina,bank # Unity IL2CPP
node mcp-server/scripts/authorMonoLive.js <pid> "games/<Game>.json" health,stamina,money # Unity Mono
node mcp-server/scripts/authorNativeLive.js <pid> "games/<Game>.json" health,mana,money # known native gameThese scripts need cd mcp-server && npm run build first, and write
<Game>.draft.json even if you never wrote a profile. Note that it replaces the
whole draft file each run, so write to a different profilePath if you want to keep an
earlier draft.
2. Read the result. Every drafted cheat has an entry in checklist:
| Field | Meaning |
|---|---|
verified |
A live instance was read and gave a plausible value (not zero, not garbage). false means only the in-game test can confirm it. |
liveValue |
What was read, e.g. 100 for a full health bar. Something odd here (a huge or microscopic number) usually means the wrong object. |
multiInstanceRisk |
Several objects of that class exist and the capture records whichever runs last. |
lookFor |
What to do in the game to see it work. |
Three more lists explain what did not become a cheat: unresolved (matched a field but
found no plausible value or no hookable method), notFound (no field matched — fall back
to the analysis scripts below), and manual (see the table).
3. Try each one in the game, then promote it. A draft is never loaded by the app.
To use one, copy its entry from <Game>.draft.json into the cheats array of the live
<Game>.json — together with the capture patch it names (the patchId in its
anchor target; those entries have "internal": true) — then restart Apprentice: it
can overwrite a profile you edited while it was running the next time it saves. Toggle it, do what lookFor says, and delete the
ones that don't work. An anchored cheat reads 0/1 live until its capture patch has
caught the object; it changes to 1/1 live by itself once the game has used it.
Categories (mcp-server/src/factory/categories.ts):
| Drafted automatically | Reported as manual, with the reason |
|---|---|
health, stamina, mana, money, bank, water, curfew, freezetime, godmode |
cash (an item object: reach it from the inventory that holds it with an anchor derefOffset), runspeed and speed (rate multipliers: zeroing or freezing them breaks other systems), nosearch (the police decision is per-officer, not a player flag), hunger (a decaying stat loses a freeze race) |
To teach it a new game's naming, add a category (or extra nameHints / classHints) in that
file: name and class regexes, the data types to try, freeze or one-shot, the value to write,
and a plausible range for the live read. Give a category a manualReview message when
its obvious mechanism is a known trap.
What the factory cannot do:
- Code patches (instant timers, invisibility, item cloning, no-damage guards) and Lua script cheats. It drafts value cheats only. Those take the analysis scripts below.
- Games it has not been taught. Unity Mono and IL2CPP work generically. Any other engine only
works if someone has written that game's entry in
nativeGames.tsby hand: the signatures that find its static roots and the offset chain from each root to each stat. The factory then verifies them live and drafts the cheats. Writing the entry is the real work; seedocs/superpowers/specs/2026-09-20-native-cheat-factory-design.md. A native game with no entry gets an error saying so; Unreal and other engines get an error pointing at that engine's playbook. - Tell a plausible field from the right one. A draft is chosen by field name and a live value in
a sane range, and
verifiedonly means that read succeeded. It can still be the wrong field: the factory once drafted a Valheim jump's stamina cost as stamina, and its Valheim health and water picks are doubtful (health is not a plain field there; there is no watering). Read thelookForline and the field name, and check each cheat before trusting it. - Overloaded methods. A Mono patch names a method, not a signature, so the app and the analysis scripts can only reach the first overload of a name.
What it does under the hood:
- Mono: enumerates classes and fields in every game assembly (
Assembly-CSharpand theassembly_*family, since Valheim keeps its code inassembly_valheim), ranks them by name, finds the live singleton root (including aSingleton<T>root inherited from a base class, as Aviassembly uses), and keeps the first candidate whose live read is plausible. An exact stat name outranks a variant that only contains it, and fields that name what an action costs are skipped. - Native (known games): finds each static root by a unique signature on the instruction that loads
it (
mov reg,[rip+rel32]), walks the offset chain to each stat, reads it live, and drafts a chain cheat by RVA. Bit flags and flag bytes are supported. - IL2CPP: reads the runtime's class, field and method structs directly
(no per-item remote call), ranks fields by name and type, verifies through
a live singleton or a filtered instance scan, and emits the same pair Tamper
already uses: a
capturepatch on a method prologue (found by a unique signature) plus ananchorvalue cheat. - Output goes to
games/<exe>.draft.json, beside the profile and never over it (a profile is looked up by exact exe name, so a draft is not loaded by accident). Every drafted cheat carries averifiedflag and amultiInstanceRiskflag; confirm each one in-game before promoting it. - Categories that are the wrong shape for a field cheat (continuously decaying stats, rate multipliers, NPC-decided behaviour, item-held currency) come back as manual, with the reason, instead of a draft that looks right and isn't.
When a cheat does nothing, or a mechanism isn't obvious, mcp-server/scripts/
has live-analysis scripts (survey classes, disassemble a method, list
callers and inlined field readers, watch a field change while the game does
something, generate a replace/capture patch with a unique signature). Beyond the
IL2CPP set shown below there are Mono ones (surveyMono.js, monoDisasm.js,
monoReaders.js, and checkPatches.js, which re-checks a profile's patches after a game
update) and, for native games, offline ones that work on a recorded snapshot of the game's code
(recordSnapshot.js, mineRoots.js, flagXrefs.js, traceSnap.js, deriveFlagRoot.js).
Two cautions. Compiling many Mono methods at once has crashed Valheim and Aviassembly, so
monoReaders.js refuses to run unless you name the methods (METHODS=<regex>); keep the set small.
And for an anti-tamper game, record snapshots with MODULE=<game exe> so only the game's own
module is read (a full-process dump crashed Elden Ring). The method they support is written up in
.claude/skills/authoring-tamper-cheats/SKILL.md and
mcp-server/scripts/README.md.
A typical hunt for a code patch (this one is IL2CPP; run cd mcp-server && npm run build
first; every script takes the game's pid first and only reads):
cd mcp-server/scripts
node survey.js <pid> out.txt "^PlayerCrimeData$" # fields and methods of a class
node findCallers.js <pid> "^(?!.*Network).*$" Cls.method # who calls it? (empty = inlined)
node findFieldReaders.js <pid> "^Cls$" "+0x148]" # who touches this field offset?
CLASSES="^Cls$" ROWS=60 BYTES=400 node disasm.js <pid> Cls.method # what does it do?
node callTree.js <pid> "^Cls$" Cls.method # what does it call?
# once you know the exact instruction, emit a patch with a signature that matches once:
MINLEN=2 AT=0x<address> ROWS=900 BYTES=4000 node makeReplace.js <pid> "^Cls$" method "." 31c0 my-id "My cheat"The output of makeReplace.js is a ready-to-paste patch entry. Two things that
trip people up: CLASSES takes one class per call, and a long method needs ROWS and
BYTES raised or the script reports "instruction not found". Prefer a patch at the
function every route shares over one per route, and check that a getter you want to
patch actually has callers (IL2CPP inlines trivial ones).
Design docs:
docs/superpowers/specs/2026-09-19-cheat-factory-design.md,
docs/superpowers/specs/2026-09-19-il2cpp-cheat-factory-design.md and
docs/superpowers/specs/2026-09-20-native-cheat-factory-design.md.
Every cheat lives in a per-game profile at games/<exe-name>.json — plain
JSON, easy to read, easy to hand to someone else.
Apprentice can import a Cheat Engine .CT table directly (Cheats screen →
Import Cheat Table (.CT)) — it recognizes the common "replace one write
with a fixed value" shape most CT entries use, and skips (with a reason)
anything it can't safely translate. Going the other way, Export to Cheat
Table (.CT) turns your cheats into a .CT file anyone with Cheat Engine
can open — not just a select few: nop, replace, and force-mode patches
all export as Auto Assembler scripts, and any value cheat resolved through a
plain module+offset(+pointer chain) address exports as an ordinary Cheat
Engine address entry, no script needed. What's left out is only what
genuinely has no Cheat Engine equivalent — capture/guard/immune/
scale/copy-mode patches (relocated code-cave injections, some with an
object pointer resolved fresh every install from live Mono metadata Cheat
Engine has no way to replicate), a value cheat resolved via Mono metadata or
a capture patch's tracked pointer rather than a fixed address, and a
single-bit cheat (freezing the whole byte in Cheat Engine would clobber
other flags packed into it) — each reported with its specific reason rather
than silently dropped.
This is the fastest way to hand a friend a single cheat or a small set
without either of you touching a games/*.json file by hand.
If you've built out a solid set of cheats for a game — especially a new
game this repo doesn't support yet — consider opening a PR to add or extend
its games/<exe>.json:
- Get your cheats working and verified in-game first. A patch that only
"looks right" in the JSON but was never actually tested against the game
is worse than no PR — see
docs/superpowers/follow-ups/2026-07-28-valheim-session.mdfor exactly how many ways a code patch can look fine and still be wrong. A*.draft.jsonproduced byauthor_cheatsis a starting point, not a verified profile: toggle each entry in-game and check what it does before promoting it. - Keep the file schema-2 shaped (
{ schema, exe, modules, cheats }) — every cheat you add should be something the app itself saved, not hand-typed from scratch, so it's already validated against the app's own types. - Name cheats the way the existing ones are named: short, in-game terminology ("Infinite Weapon Durability," not "InfDur" or "cheat_12").
- If a cheat is a code patch anchored to a module (not Mono-resolved), note in the PR description which game version/build it was captured against — module-anchored patches verify a fingerprint before trusting their saved address, but that only helps if someone knows what build to expect it against in the first place.
- Mention any cheat that's build-specific or known to break on other difficulty/mode settings, so the next person doesn't have to rediscover that the hard way.
A game update can shift a patch's exact bytes even when nothing about the cheat changed — that's expected, not a sign something's broken. Apprentice re-verifies and re-locates on every attach, and Mono-anchored cheats (resolved by class/method name rather than a byte signature) mostly ride through updates without needing any of this.
When Apprentice exits normally it puts the game back: code patches are restored when you disable a cheat, when the game closes, and when you quit; frozen values are put back to what they held when you turned the cheat on (or to the cheat's own off value, where it sets one); and a hardware write-watch breakpoint is cleared. That cleanup only runs while Apprentice is running. If Apprentice itself is force-closed (Task Manager, a crash, a power cut), nothing can restore the game: any patch or frozen value you had switched on stays in it until you re-attach and switch it off, or restart the game (re-attaching adopts patches left behind this way). A find-what-writes capture in progress uses a hardware breakpoint, and if Apprentice is killed mid-capture the game can crash. So quit Apprentice from its window rather than ending the process. If you see something still patched after a normal quit, that's a bug — please report it with repro steps.
This tool touches only the process you explicitly attach it to, and does nothing to a game until you turn a cheat on (attaching and browsing the game library only read). The app makes no network requests of its own: Cheat Table import and export read and write a file you choose, and the game library reads Steam's files on disk.
GPL-3.0. Free to use, study, modify, and redistribute — including commercially — as long as anything you distribute that's built on this code stays open source under the same license.