// 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 #include #include "game/engine/arts.h" #include "game/engine/events.h" #include "game/engine/fx.h" #include "game/engine/objects.h" #include "game/engine/orders.h" #include "re/log.h" namespace mods::aura_restore { namespace { constexpr uint32_t kAuraEffectType = 842; // cObjectFX: aura following its owner constexpr uint16_t kAuraArts[] = {game::kArtHeldenmut, game::kArtKlingenblick}; constexpr int kTicksAfterLoad = 5; // wait for the load to finish (0.5 s) using namespace game; // effect layout (arts.h) int g_ticksUntilRestore = -1; void AttachAura(uint8_t* hero, uint16_t art, float remaining) { const uint32_t heroId = *reinterpret_cast(hero + kCreatureId); const uint32_t id = CreateObjectAt(kAuraEffectType, hero + 0x18); // the hero's position auto* effect = static_cast(game::GetObjectById(id)); if (!effect) { return; } uint32_t event[kFxEventDwords] = {}; event[0] = kFxEventVtable; event[1] = kFxEventAttachAura; event[2] = heroId; std::memcpy(&event[11], &remaining, 4); event[15] = art; SendEvent(effect, 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 lock) of `art`. float RemainingBuff(uint8_t* hero, uint16_t art) { uint8_t* arts = hero + kCreatureArts; auto* entry = *reinterpret_cast(arts + kEffectsBegin); auto* end = *reinterpret_cast(arts + kEffectsEnd); float longest = 0.0f; for (; entry && entry < end; entry += kEffectSize) { float remaining; std::memcpy(&remaining, entry + kEffectRemaining, 4); if (*reinterpret_cast(entry + kEffectKind) != kEffectKindArtLock && *reinterpret_cast(entry + kEffectArt) == art && remaining > longest) { longest = remaining; } } return longest; } void RestoreAuras() { for (uint32_t id = 1; id < kPlayerHeroIdEnd; ++id) { auto* hero = static_cast(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