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:
1 parent
30c93fc87e
commit
f721d2a486
8 files changed
+217
-26
No files matched your search
@@ -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
|
||||
@@ -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
@@ -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
|
||||
Reference in new issue
Block a user