diff --git a/CMakeLists.txt b/CMakeLists.txt index ce229a2..66f72cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/docs/todo.md b/docs/todo.md index 836768c..98e3697 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -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) diff --git a/src/game/engine/events.cpp b/src/game/engine/events.cpp new file mode 100644 index 0000000..8be8e5c --- /dev/null +++ b/src/game/engine/events.cpp @@ -0,0 +1,49 @@ +#include "game/engine/events.h" + +#include + +#include "re/hook.h" + +namespace game { +namespace { + +std::vector& TickCallbacks() { + static std::vector callbacks; + return callbacks; +} + +std::vector& LoadedCallbacks() { + static std::vector 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(self)); + } + } +} + +} // namespace game diff --git a/src/game/engine/events.h b/src/game/engine/events.h new file mode 100644 index 0000000..da21abb --- /dev/null +++ b/src/game/engine/events.h @@ -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 + +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 diff --git a/src/mods/aura_restore.cpp b/src/mods/aura_restore.cpp new file mode 100644 index 0000000..46f36ba --- /dev/null +++ b/src/mods/aura_restore.cpp @@ -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 +#include + +#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(0x00ad3bc0); + return manager ? reinterpret_cast(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(hero + 0xc); + const uint32_t id = CreateAt(kAuraEffectType, hero + 0x18); // the hero's position + auto* effect = static_cast(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((*reinterpret_cast(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(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) != kKindCooldown && + *reinterpret_cast(entry + kEffectArt) == art && remaining > longest) { + longest = remaining; + } + } + return longest; +} + +void RestoreAuras() { + for (uint32_t id = 1; id < kPlayerIds; ++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 diff --git a/src/mods/inventory_save.cpp b/src/mods/inventory_save.cpp index 28634b4..a0cc0c0 100644 --- a/src/mods/inventory_save.cpp +++ b/src/mods/inventory_save.cpp @@ -24,6 +24,7 @@ #include +#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(write)) { re::Log("inventory: save game read, %d JSON / %d vanilla inventories", g_readJson, g_readVanilla); + NotifySaveGameLoaded(); } return result; } diff --git a/src/mods/wasd.cpp b/src/mods/wasd.cpp index d951bd8..e8a4c77 100644 --- a/src/mods/wasd.cpp +++ b/src/mods/wasd.cpp @@ -20,6 +20,7 @@ #include +#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(self)); - } -} - -} // namespace game diff --git a/tools/names.txt b/tools/names.txt index d394d45..5afbeee 100644 --- a/tools/names.txt +++ b/tools/names.txt @@ -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