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
@@ -31,6 +31,7 @@ add_library(sacred_re SHARED
|
||||
src/re/sacred_re.def
|
||||
src/game/engine/log.cpp
|
||||
src/game/engine/cmdline.cpp
|
||||
src/game/engine/events.cpp
|
||||
src/mods/borderless.cpp
|
||||
src/mods/widescreen.cpp
|
||||
src/mods/hud.cpp
|
||||
@@ -41,6 +42,7 @@ add_library(sacred_re SHARED
|
||||
src/mods/inventory_save.cpp
|
||||
src/mods/skillbar.cpp
|
||||
src/mods/arttarget.cpp
|
||||
src/mods/aura_restore.cpp
|
||||
)
|
||||
target_include_directories(sacred_re PRIVATE src)
|
||||
target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS})
|
||||
|
||||
+6
-12
@@ -1,18 +1,12 @@
|
||||
# To do
|
||||
|
||||
## Bug: skill "aura" effects are not drawn
|
||||
## Fixed: aura visuals missing after loading (2026-10-15)
|
||||
|
||||
Reported 2026-10-14: the aura effects of some skills are missing. Not
|
||||
investigated yet. Things to check first:
|
||||
|
||||
- whether vanilla (`[widescreen] Enabled=0`) draws them, i.e. whether the
|
||||
widescreen or HUD patches cause it;
|
||||
- whether they are missing everywhere or only away from the screen centre;
|
||||
if the latter, a clip test still using 1024x768 bounds, like the creature
|
||||
clip (`0x4c8`/`0x3c8`) was;
|
||||
- effect objects (`cObjectFX`) in the object passes (`cWorldView0::DrawObjects`
|
||||
`0x62e580` / `DrawObjects2` `0x6300d0`) and the light/effect list
|
||||
(`+0xce14`, `0x41b1xx`).
|
||||
Saving marks effect objects (cObjectFX) dead; loading keeps the buff but drops
|
||||
the aura effect (type 842) that follows the hero. `src/mods/aura_restore.cpp`
|
||||
recreates it after a load for Heldenmut (0x3f7) and Klingenblick (0x3f8). Spell
|
||||
auras (Steinhaut, Lichtschild, ...) use other effects and are not covered
|
||||
yet; check them if their visuals go missing too.
|
||||
|
||||
## Better pathfinder (mouse clicks and monsters)
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "game/engine/events.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "re/hook.h"
|
||||
|
||||
namespace game {
|
||||
namespace {
|
||||
|
||||
std::vector<EngineTickFn>& TickCallbacks() {
|
||||
static std::vector<EngineTickFn> callbacks;
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
std::vector<SaveGameLoadedFn>& LoadedCallbacks() {
|
||||
static std::vector<SaveGameLoadedFn> callbacks;
|
||||
return callbacks;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void OnEngineTick(EngineTickFn callback) {
|
||||
TickCallbacks().push_back(callback);
|
||||
}
|
||||
|
||||
void OnSaveGameLoaded(SaveGameLoadedFn callback) {
|
||||
LoadedCallbacks().push_back(callback);
|
||||
}
|
||||
|
||||
void NotifySaveGameLoaded() {
|
||||
for (SaveGameLoadedFn callback : LoadedCallbacks()) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
for (EngineTickFn callback : TickCallbacks()) {
|
||||
callback(static_cast<uint8_t*>(self));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
// Game events several mods react to. The hooks behind them live in
|
||||
// events.cpp; mods register callbacks at startup (static initialisation).
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace game {
|
||||
|
||||
// cEngine::Tick (0x60f1d0, 10 Hz), after the game's own handling.
|
||||
using EngineTickFn = void (*)(uint8_t* engine);
|
||||
void OnEngineTick(EngineTickFn callback);
|
||||
|
||||
// A save game finished loading its objects (cInventoryManager::Serialize
|
||||
// read, the last part of the load); callbacks run on the loading thread.
|
||||
using SaveGameLoadedFn = void (*)();
|
||||
void OnSaveGameLoaded(SaveGameLoadedFn callback);
|
||||
void NotifySaveGameLoaded();
|
||||
|
||||
} // namespace game
|
||||
@@ -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
|
||||
@@ -141,3 +141,15 @@
|
||||
0x00548e70 cCreature::IsEnemy # (object id)
|
||||
0x00611f10 cEngine::UpdateCursorPick # cursor target for the selected art
|
||||
0x00548060 cCreature::ChooseAiArt # monsters: art per creature class
|
||||
0x00582c10 cCreatureArts::AddEffect # (kind, value, factor, duration, art, notify, ...): active effects at +0x11a
|
||||
0x00539ce0 cCreature::StartArtCooldown # AddEffect(0x22, ...)
|
||||
0x004feef0 cCreature::ArtCastEffect # per-art effect of using an art (switch on art id)
|
||||
0x005fbaa0 cObjectManager::CreateAt # (type, position, ...): object placed in the world
|
||||
0x005fb590 cObjectManager::CreateFX
|
||||
0x005fb040 cObjectManager::Allocate
|
||||
0x004514d0 ObjectFactory::Create # (class, ...)
|
||||
0x005fcd10 cObjectManager::Place # (id, position); refuses dead objects (+0x14 & 0x10)
|
||||
0x00602d70 cObjectManager::Load # objects from a save stream
|
||||
0x005a0980 cObjectFX::Construct
|
||||
0x005a37b0 cObjectFX::ReceiveEvent # vf18; cEventFX 0x42 attaches it to an owner
|
||||
0x0041e0a0 cEventFX::Construct
|
||||
Reference in new issue
Block a user