Files
sacred_k/docs/anticheat.md
T
khallaandClaude Opus 5.5 34cacefcfc Anti-cheat checks removed
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>
2026-10-25 11:36:42 +01:00

4.2 KiB

Anti-cheat removed

Request (2026-10-25)

The Gladiator (Character03, level 106) "randomly exploded" fighting an underleveled dragon on Silber. The user: the cheater detection is not necessary, remove it. After a second explosion: "i want all that weird cheater detection code gone".

Measured

A temporary damage log showed the hero at full health (17200) going to 0 in one tick, with the game logging cheat statBonus 0/3, cheat SkillLevel 0..7, Hase! and cObjectManager::destroy: tried to delete local hero #1!; the hero came back with 1199 health and died to the next hit.

  • The game mirrors the local hero's fields (pointers 0x182cddc, on a protected page, and 0x182cde0, a separate heap block: HeapAlloc 0x1000 at 0x816541): +0 hero +0x3b4, +4 hero +0x3ee (gold), +8 hero +0x3fe (u16), xor-ed with the hero's key byte (+0x38) repeated four times (the second copy: key << 1); +0xc a raw copy of hero +0x3b0..+0x49d (attributes, skill levels). Writer: 0x603d9c and about 30 inline copies after legitimate changes, all starting with VirtualProtect(page, 0x1000, PAGE_READWRITE) and ending with 0x201.
  • Checks: cCreature::Think about once in 101 ticks (rand() % 101 == 10, 0x5389b5), VerifyHeroMirror 0x5905f0 (callers 0x57fc71, 0x580001, 0x580331, 0x61ce22), inline copies at 0x4659f6, 0x564c6f, 0x57e09c, 0x585bd0, 0x619d33, and a trap in Think (0x5385a6: a hero whose level exceeds its experience's by more than 3 gets +0x3b4 / +0x3ee / +0x3fe raised behind the mirrors' back). A mismatch resets the field (attributes 1, skill levels 0); any mismatch, or no hero at all, kills the hero ("*** Cheater detected! ***", health 0, action dying) and sets the cheater flag 0x182cdd0, saved in the hero file (record +0xf3, 0x604409) and read back by loadHero (0x6066eb) and the character selection (0x70da99, from the record's mark +0x3ef).
  • Stream checks in the quest manager's hero data (0x465980): a marker 0xfea4fea4 (0x467782) and three stored values compared with the hero (0x46a60b, 0x46a64e, 0x46a696).
  • In the failing log every skill level mismatched: the mirror was stale (changes outside the game's mirror writers, e.g. the shared hero loaded into another world).

History

  • First mod (confirmed): a VirtualProtect hook rewrote the mirror from the hero whenever a check opened its page, so checks compared the hero with itself.
  • The hero exploded again (Hase! twice, no cheat statBonus / SkillLevel line): the hook refreshed the second mirror only if it lay on the protected page, so never; the checks compare its three xor-ed fields after the first mirror (e.g. 0x585f83) without a log line. Refreshing both fixed it (user: "i no longer explode").
  • Then the checks were removed entirely (below), the hook is gone.

Mod (src/mods/anticheat.cpp): checks removed

Every check is skipped by a code patch (re::ReplaceAll): no field is reset, no hero killed, the cheater flag never set.

Check Patch
Think trap 0x5385a6 0x5385ca jle -> jmp
Think, 1 tick in 101 (0x5389be) 0x5389b8 jne -> jmp
0x564c6f, 0x57e09c, 0x585bd0 the "not the local hero" jne before each (0x564c50, 0x57e07d, 0x585bb0) -> jmp: the game's own path past the check
quest manager hero data 0x4659f6 0x4659dc jmp 0x46606b (the code there writes esi / edi before reading them; the stream keeps its clean marker 0xfeedf00d)
0x619d33 mov ebp, [esp+0x34]; jmp 0x61a2de, the registers as the check leaves them (edi already 0x88e874)
VerifyHeroMirror 0x5905f0 (callers test al) mov al, 1; ret
stream checks 0x467782, 0x46a60b, 0x46a64e, 0x46a696 jumps past the punishment unconditional
loadHero 0x6066eb writes 0 to the cheater flag
character selection 0x70da8c setne cl -> xor cl, cl

All field resets (mov [reg+0x3b4], 1 ...) lie inside these checks; all writes of the flag 0x182cdd0 (0x465ebe, 0x538e53, 0x56508a, 0x57e528, 0x57fcfd, 0x58008d, 0x5803bd, 0x586090, 0x61a17e, 0x61ceac) are their punishments. The mirror writers stay: they only fill the now unread mirrors.