// The game's anti-cheat is gone (requests of 2026-10-25: the Gladiator // "randomly exploded" fighting a dragon on Silber; then "i want all that weird // cheater detection code gone"). docs/anticheat.md. // // Measured: the game keeps two xor-ed mirrors of the local hero's attribute / // skill / gold fields (0x182cddc on a protected page, 0x182cde0 a heap block), // written after every legitimate change. Checks compare the hero with them, // reset every mismatching field (attributes to 1, skill levels to 0) and on // any mismatch kill the hero ("*** Cheater detected! *** Hase!") and set the // cheater flag 0x182cdd0, which the hero file keeps. Any change the game does // not route through its mirror writers (e.g. loading the shared hero into // another world) made the checks fire. // // The mod: every check is skipped through the game's own path for "not the // local hero" (or straight to the code after the check, with the registers that // code reads), so no field is reset and no hero killed; the cheater flag is // never set. The mirror writers stay (they only fill the unused mirrors). #include #include "re/hook.h" #include "re/log.h" #include "re/patch.h" namespace mods::anticheat { namespace { constexpr re::CodeReplacement kNoAntiCheat[] = { // cCreature::Think: the trap for a level more than 3 above the experience's // (raises fields behind the mirrors' back): `jle` past it -> `jmp` {0x005385ca, {0x0f, 0x8e, 0xd8, 0x03, 0x00, 0x00}, {0xe9, 0xd9, 0x03, 0x00, 0x00, 0x90}, 6}, // cCreature::Think: the check in 1 of 101 ticks: `jne` past it -> `jmp` {0x005389b8, {0x0f, 0x85, 0x0d, 0x06, 0x00, 0x00}, {0xe9, 0x0e, 0x06, 0x00, 0x00, 0x90}, 6}, // inline checks behind "is this the local hero": `jne` past them -> `jmp` {0x00564c50, {0x0f, 0x85, 0x89, 0x05, 0x00, 0x00}, {0xe9, 0x8a, 0x05, 0x00, 0x00, 0x90}, 6}, {0x0057e07d, {0x0f, 0x85, 0xd8, 0x05, 0x00, 0x00}, {0xe9, 0xd9, 0x05, 0x00, 0x00, 0x90}, 6}, {0x00585bb0, {0x0f, 0x85, 0x55, 0x06, 0x00, 0x00}, {0xe9, 0x56, 0x06, 0x00, 0x00, 0x90}, 6}, // the quest manager's hero data writer: past the check to 0x46606b (it // writes registers before reading them; the stream keeps its clean marker) {0x004659dc, {0x8b, 0x0d, 0xc0, 0x3b, 0xad, 0x00}, {0xe9, 0x8a, 0x06, 0x00, 0x00, 0x90}, 6}, // 0x619d33: past the check to 0x61a2de with ebp as the check leaves it // (`mov ebp, [esp+0x34]`; edi already holds 0x88e874) {0x00619d33, {0x8b, 0x0d, 0xdc, 0xcd, 0x82, 0x01}, {0x8b, 0x6c, 0x24, 0x34, 0x90, 0x90}, 6}, {0x00619d39, {0x8d, 0x44, 0x24, 0x14, 0x50}, {0xe9, 0xa0, 0x05, 0x00, 0x00}, 5}, // VerifyHeroMirror (0x5905f0, plain ret, 4 callers test al): `mov al, 1; ret` {0x005905f0, {0x51, 0x8b, 0x0d}, {0xb0, 0x01, 0xc3}, 3}, // stream checks of the quest manager's hero data: the cheat marker // 0xfea4fea4 read (`jne` past the punishment -> `jmp`) and three stored // values compared with the hero (`je` -> `jmp`) {0x00467782, {0x0f, 0x85, 0xbd, 0x01, 0x00, 0x00}, {0xe9, 0xbe, 0x01, 0x00, 0x00, 0x90}, 6}, {0x0046a60b, {0x74, 0x18}, {0xeb, 0x18}, 2}, {0x0046a64e, {0x74, 0x18}, {0xeb, 0x18}, 2}, {0x0046a696, {0x74, 0x18}, {0xeb, 0x18}, 2}, // loadHero: the hero file's cheater flag (`mov byte [0x182cdd0], 1` -> 0) {0x006066eb, {0xc6, 0x05, 0xd0, 0xcd, 0x82, 0x01, 0x01}, {0xc6, 0x05, 0xd0, 0xcd, 0x82, 0x01, 0x00}, 7}, // character selection: the flag from the hero record's mark (+0x3ef): // `setne cl` -> `xor cl, cl` {0x0070da8c, {0x0f, 0x95, 0xc1}, {0x32, 0xc9, 0x90}, 3}, }; void Startup() { const int failed = re::ReplaceAll(kNoAntiCheat); re::Log("anticheat: removed (%d patches failed)", failed); } RE_STARTUP(AntiCheatStartup, Startup); } // namespace } // namespace mods::anticheat