Restore aura visuals after loading a save

Saving marks effect objects dead, so after a load the Heldenmut /
Klingenblick buff was active but its aura effect (cObjectFX 842) was
gone. A few ticks after a save game has loaded, the effect is created
again for each such buff of a player hero (CreateAt + cEventFX 0x42,
as the art's cast effect does), with the buff's remaining time.

Shared game events (game/engine/events): engine tick and "save game
loaded" callbacks; the WASD mod uses the tick through it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-15 01:27:56 +02:00
1 parent 30c93fc87e
commit f721d2a486
8 files changed
+217 -26

No files matched your search

+120
View File
@@ -0,0 +1,120 @@
// Aura visuals after loading a save.
//
// Casting a lasting self aura (Heldenmut 0x3f7, Klingenblick 0x3f8) adds a
// buff to the hero's active effects and, separately, creates an aura effect
// object (cObjectFX type 842) that follows the hero: the art's cast effect
// (0x4feef0) creates it with cObjectManager::CreateAt at the hero and sends it
// a cEventFX 0x42 (owner, duration, art). Saving marks effect objects as dead;
// loading restores the buff but drops the effect, so the aura works but is
// invisible. A few ticks after a save game has loaded, the effect is created
// again for every such buff of a player hero, with the buff's remaining time.
#include <cstdint>
#include <cstring>
#include "game/engine/events.h"
#include "game/engine/objects.h"
#include "re/log.h"
namespace mods::aura_restore {
namespace {
constexpr uint32_t kAuraEffectType = 842; // cObjectFX: aura following its owner
constexpr uint16_t kAuraArts[] = {0x3f7, 0x3f8}; // Heldenmut, Klingenblick
constexpr uint32_t kPlayerIds = 0x11; // object ids below this are player heroes
constexpr int kTicksAfterLoad = 5; // wait for the load to finish (0.5 s)
// Active effects of a creature: vector of 0x20-byte entries at
// (creature + 0x3a8) + 0x11a / + 0x11e (cCreatureArts, AddEffect 0x582c10).
constexpr size_t kCreatureArts = 0x3a8;
constexpr size_t kEffectsBegin = 0x11a;
constexpr size_t kEffectsEnd = 0x11e;
constexpr size_t kEffectSize = 0x20;
constexpr size_t kEffectKind = 0x0;
constexpr size_t kEffectRemaining = 0x18; // float, seconds
constexpr size_t kEffectArt = 0x1c; // u16
constexpr uint32_t kKindCooldown = 0x22; // the art's cooldown (0x539ce0), not the buff
// cEventFX (0x7c bytes, built by 0x41e0a0).
constexpr uint32_t kEventFXVtable = 0x0088e9b4;
constexpr uint32_t kFxAttach = 0x42;
int g_ticksUntilRestore = -1;
// cObjectManager::CreateAt (0x5fbaa0): object `type` at `position`; its id.
uint32_t CreateAt(uint32_t type, const void* position) {
using Fn = uint32_t(__fastcall*)(void* manager, void* edx, uint32_t, const void*, uint32_t, uint32_t,
uint32_t);
void* manager = *reinterpret_cast<void**>(0x00ad3bc0);
return manager ? reinterpret_cast<Fn>(0x005fbaa0)(manager, nullptr, type, position, 0, 0, 0) : 0;
}
void AttachAura(uint8_t* hero, uint16_t art, float remaining) {
const uint32_t heroId = *reinterpret_cast<uint32_t*>(hero + 0xc);
const uint32_t id = CreateAt(kAuraEffectType, hero + 0x18); // the hero's position
auto* effect = static_cast<uint8_t*>(game::GetObjectById(id));
if (!effect) {
return;
}
uint32_t event[0x1f] = {};
event[0] = kEventFXVtable;
event[1] = kFxAttach;
event[2] = heroId;
std::memcpy(&event[11], &remaining, 4);
event[15] = art;
using ReceiveFn = int(__fastcall*)(void*, void*, void*);
reinterpret_cast<ReceiveFn>((*reinterpret_cast<void***>(effect))[6])(effect, nullptr, event);
re::Log("aura: restored art %x on hero %u (%.1f s left)", art, heroId, remaining);
}
// Longest remaining time of the buffs (any kind but the cooldown) of `art`.
float RemainingBuff(uint8_t* hero, uint16_t art) {
uint8_t* arts = hero + kCreatureArts;
auto* entry = *reinterpret_cast<uint8_t**>(arts + kEffectsBegin);
auto* end = *reinterpret_cast<uint8_t**>(arts + kEffectsEnd);
float longest = 0.0f;
for (; entry && entry < end; entry += kEffectSize) {
float remaining;
std::memcpy(&remaining, entry + kEffectRemaining, 4);
if (*reinterpret_cast<uint32_t*>(entry + kEffectKind) != kKindCooldown &&
*reinterpret_cast<uint16_t*>(entry + kEffectArt) == art && remaining > longest) {
longest = remaining;
}
}
return longest;
}
void RestoreAuras() {
for (uint32_t id = 1; id < kPlayerIds; ++id) {
auto* hero = static_cast<uint8_t*>(game::GetCreature(id));
if (!hero) {
continue;
}
for (uint16_t art : kAuraArts) {
const float remaining = RemainingBuff(hero, art);
if (remaining > 0.0f) {
AttachAura(hero, art, remaining);
}
}
}
}
void OnLoaded() {
g_ticksUntilRestore = kTicksAfterLoad;
}
void OnTick(uint8_t*) {
if (g_ticksUntilRestore >= 0 && g_ticksUntilRestore-- == 0) {
RestoreAuras();
}
}
struct Registration {
Registration() {
game::OnSaveGameLoaded(&OnLoaded);
game::OnEngineTick(&OnTick);
}
} g_registration;
} // namespace
} // namespace mods::aura_restore
+2
View File
@@ -24,6 +24,7 @@
#include <nlohmann/json.hpp>
#include "game/engine/events.h"
#include "mods/skillslots.h"
#include "re/hook.h"
#include "re/log.h"
@@ -376,6 +377,7 @@ int __fastcall InvManager_Serialize(void* self, void* edx, void* stream, int wri
--g_heroFileDepth;
if (!static_cast<char>(write)) {
re::Log("inventory: save game read, %d JSON / %d vanilla inventories", g_readJson, g_readVanilla);
NotifySaveGameLoaded();
}
return result;
}
+7 -14
View File
@@ -20,6 +20,7 @@
#include <windows.h>
#include "game/engine/events.h"
#include "game/engine/objects.h"
#include "mods/input.h"
#include "re/hook.h"
@@ -126,19 +127,11 @@ void Update(uint8_t* engine) {
g_move.moving = true;
}
namespace {
struct Registration {
Registration() { game::OnEngineTick(&Update); }
} g_registration;
} // namespace
} // namespace mods::wasd
namespace game {
// cEngine::Tick (0x60f1d0, 10 Hz): processes queued events, then the mouse's
// hold-to-move re-aim orders.
void __fastcall Engine_Tick(void* self, void* edx, void* event);
RE_HOOK(0x0060f1d0, Engine_Tick);
void __fastcall Engine_Tick(void* self, void* edx, void* event) {
Engine_Tick_original(self, edx, event);
if (self) {
mods::wasd::Update(static_cast<uint8_t*>(self));
}
}
} // namespace game