Anti-cheat off, weapon sets 6-7 in hero files

- The game's cheater detection killed the hero ("Hase!") whenever its
  protected stat mirror was stale; the mirror is now refreshed from the hero
  whenever it is unlocked, stream checks never punish, loadHero no longer
  sets the cheater flag (docs/anticheat.md).
- saveHero wrote the items of weapon sets 1-5 only: its loop runs over all 7
  sets, so sets 6-7 survive a world loading the hero from the hero file.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-25 01:33:43 +02:00
1 parent 3978060954
commit 91bbc52cf0
7 files changed
+210

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
// The game's anti-cheat is off (request of 2026-10-25, after the Gladiator
// "randomly exploded" fighting a dragon on Silber).
//
// Measured: the game keeps a mirror of the local hero's attribute / skill
// fields on a protected page (pointers 0x182cddc and 0x182cde0, see
// RefreshMirror). Checks compare the hero with it - cCreature::Think about once
// in 101 ticks (0x5389be), VerifyHeroMirror 0x5905f0 (4 callers) and inline
// copies at 0x4659f6, 0x564c6f, 0x57e09c, 0x585bd0, 0x619d33 - 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, saved in the hero file (record +0xf3). The log showed the hero at
// full health dying with every skill level mismatching: the mirror was stale
// (any change the game does not route through its own mirror writers, e.g.
// loading the shared hero into another world, leaves it behind).
//
// The mod: every check and every mirror writer starts with
// VirtualProtect(mirror, 0x1000, PAGE_READWRITE); at that moment the mirror is
// refreshed from the hero, so checks compare the hero with itself and writers
// overwrite it as before. The stream checks of the quest manager's hero data
// (a cheat marker 0xfea4fea4 and three stored values) never punish, and loading
// a hero file no longer sets the cheater flag.
#include <windows.h>
#include <cstdint>
#include <cstring>
#include "game/engine/objects.h"
#include "re/hook.h"
#include "re/log.h"
#include "re/patch.h"
namespace mods::anticheat {
namespace {
// Mirror layout (writer 0x603d9c): +0 hero +0x3b4, +4 hero +0x3ee (gold),
// +8 hero +0x3fe (u16), each xor-ed with the hero's key byte (+0x38) repeated
// four times; +0xc a copy of hero +0x3b0..+0x49d. The second mirror uses the
// key shifted left by one.
constexpr uintptr_t kMirrorPointer = 0x0182cddc;
constexpr uintptr_t kSecondMirrorPointer = 0x0182cde0;
constexpr size_t kHeroKey = 0x38;
constexpr size_t kField0 = 0x3b4;
constexpr size_t kGold = 0x3ee;
constexpr size_t kField2 = 0x3fe; // u16
constexpr size_t kCopyStart = 0x3b0;
constexpr size_t kCopySize = 0x3b * 4 + 2; // rep movsd 0x3b + movsw
constexpr size_t kMirrorCopy = 0xc;
constexpr size_t kPageSize = 0x1000;
uint32_t U32(const uint8_t* p) {
uint32_t v;
std::memcpy(&v, p, 4);
return v;
}
void WriteMirror(uint8_t* mirror, const uint8_t* hero, uint32_t key) {
const uint32_t values[3] = {U32(hero + kField0) ^ key, U32(hero + kGold) ^ key,
(U32(hero + kField2) & 0xffff) ^ key};
std::memcpy(mirror, values, sizeof values);
std::memcpy(mirror + kMirrorCopy, hero + kCopyStart, kCopySize);
}
// The page is writable when this runs (inside VirtualProtect below).
void RefreshMirror(uint8_t* page) {
const auto* hero = static_cast<const uint8_t*>(game::GetPlayerHero());
if (!hero) {
return;
}
const uint32_t keyByte = hero[kHeroKey];
const uint32_t key = keyByte | keyByte << 8 | keyByte << 16 | keyByte << 24;
WriteMirror(page, hero, key);
auto* second = *reinterpret_cast<uint8_t**>(kSecondMirrorPointer);
if (second >= page && second + kMirrorCopy + kCopySize <= page + kPageSize) {
WriteMirror(second, hero, key << 1);
}
}
constexpr re::CodeReplacement kNoStreamPunishment[] = {
// cheat marker 0xfea4fea4 read: `jne` past the punishment -> `jmp`
{0x00467782, {0x0f, 0x85, 0xbd, 0x01, 0x00, 0x00}, {0xe9, 0xbe, 0x01, 0x00, 0x00, 0x90}, 6},
// stored values compared with the hero: `je` past the punishment -> `jmp`
{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},
};
void Startup() {
const int failed = re::ReplaceAll(kNoStreamPunishment);
re::Log("anticheat: off (%d patches failed)", failed);
}
RE_STARTUP(AntiCheatStartup, Startup);
} // namespace
} // namespace mods::anticheat
BOOL WINAPI VirtualProtectMirrorFresh(LPVOID address, SIZE_T size, DWORD protect, PDWORD old);
RE_HOOK_API(VirtualProtect, VirtualProtectMirrorFresh);
BOOL WINAPI VirtualProtectMirrorFresh(LPVOID address, SIZE_T size, DWORD protect, PDWORD old) {
const BOOL result = VirtualProtectMirrorFresh_original(address, size, protect, old);
auto* mirror = *reinterpret_cast<uint8_t**>(mods::anticheat::kMirrorPointer);
if (result && protect == PAGE_READWRITE && mirror && address == mirror) {
mods::anticheat::RefreshMirror(mirror);
}
return result;
}
+15
View File
@@ -32,6 +32,7 @@
#include "mods/sharedchest.h"
#include "mods/skillslots.h"
#include "re/hook.h"
#include "re/patch.h"
#include "re/log.h"
namespace mods::inventory_save {
@@ -327,6 +328,20 @@ void RegisterHeroData(const char* key, HeroDataWriteFn write, HeroDataReadFn rea
HeroDataList().push_back({key, write, read});
}
// saveHero writes the hero's items (chunk 0xc2) from its inventories, equipment
// and weapon sets; the weapon-set loop (GetWeaponSetItem over sets 0..4, `cmp
// esi, 5`) runs over all 7 so the items of sets 6-7 are in hero files too. Without
// them a world loading the hero from its hero file had sets 6-7 pointing at
// items that did not exist (2026-10-25).
constexpr uint32_t kSaveHeroWeaponSetLoop = 0x00605046;
void Startup() {
if (!re::PatchInstructionByte(kSaveHeroWeaponSetLoop, 2, kGameSlots, kSlots)) {
re::Log("inventory: saveHero weapon-set loop not patched");
}
}
RE_STARTUP(InventorySaveStartup, Startup);
} // namespace mods::inventory_save
namespace game {