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>
77 lines
4.2 KiB
Markdown
77 lines
4.2 KiB
Markdown
# 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.
|