All mirror checks are skipped by code patches (the game's own "not the local hero" paths, or straight past the check), the level trap in Think and VerifyHeroMirror are off, the cheater flag is never set (stream checks, loadHero, character selection). The VirtualProtect mirror refresh is gone; it never refreshed the second mirror, a separate heap block, which killed the hero again. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
69 lines
3.7 KiB
C++
69 lines
3.7 KiB
C++
// 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 <cstdint>
|
|
|
|
#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
|