From 471b8b13d5ef73f7dbb38758a838c9c49837bb4a Mon Sep 17 00:00:00 2001 From: Torhan Bartel Date: Wed, 28 Oct 2026 01:28:07 +0100 Subject: [PATCH] Eingraben: 2 s move inside a barrier turning solid, 4 s duration The barrier's model alpha goes to opaque over 1 s, the dwarf is moved, and it clears over 1 s; orders to him are refused meanwhile (new game::FilterCreatureOrders). The art's duration in its definition is a fixed 4 s (was 7 + level), which the barrier, its bonus and the tooltip use. Co-Authored-By: Claude Opus 5.5 --- docs/dwarf-arts.md | 25 +++++++-- src/game/engine/orders.cpp | 14 +++++ src/game/engine/orders.h | 5 ++ src/mods/aurarecast.cpp | 2 +- src/mods/dwarfarts.cpp | 105 ++++++++++++++++++++++++++++++++----- src/mods/dwarfarts.h | 7 ++- tools/names.txt | 4 ++ 7 files changed, 139 insertions(+), 23 deletions(-) diff --git a/docs/dwarf-arts.md b/docs/dwarf-arts.md index 2cde91a..0d0966d 100644 --- a/docs/dwarf-arts.md +++ b/docs/dwarf-arts.md @@ -120,7 +120,24 @@ Mod (`mods/dwarfarts.cpp`): Eingraben's definition +0x3c becomes 3 (a point); after the original case the dwarf is teleported to the end of Phasenverschiebung's walk towards the point (the cursor keeps it within the visible area). The effect hook (`aurarecast.cpp`, `KeepEffect`) drops kind -0x3a for art 0x39 and cuts the art's other effects to 3 s. Confirmed -2026-10-28: aimed, stops before walls and cliffs, casts at once at the screen -edge, the barrier moves with him, no lock, bonus for 3 s. Open: clearing -slows and stuns, 1 s digging in and out. +0x3a for art 0x39. Confirmed 2026-10-28: aimed, stops before walls and +cliffs, casts at once at the screen edge, the barrier moves with him, no lock. + +Duration ("more like 8 seconds as described in the tooltip, i want that to go +down to 4 seconds"): an art's duration is 0x4d24f0 on its definition, ++0x76 + level x +0x78, at most +0x82 when set (Eingraben: 7 + level, at most +15). The barrier's lifetime (shield +0x20a8), its bonus (the effect applier, +0x5b56ab) and the tooltip all use it; the mod sets 4 / 0 / 0. + +Digging in and out: lowering the dwarf's height (creature +0xf0) moved only +his marker; lowering the terrain height the game asks for him (0x449b20) +moved the camera with him, so he never looked sunk. Instead ("change the +barrier animation to turn from transparent to solid and back"): the barrier +is `cParticleSystem_dwarf_shield` (constructor 0x7b8fc0, update vf08 +0x7b91a0, draw vf04 0x7b92b0; models fx_shield_phase1..4.grn). Its draw +passes the model with alpha pulse x 128 (pulse 0.4..1 at +0x20b8; __ftol at +0x7b9483), then again additively. The mod redirects that __ftol: during a dig +the alpha goes to 255 over 1 s, the dwarf is moved (engine tick), and it goes +back over 1 s. Orders to the dwarf are refused meanwhile +(`game::FilterCreatureOrders`). Confirmed 2026-10-28, barrier and bonus 4 s. +Open: clearing slows, roots and stuns. diff --git a/src/game/engine/orders.cpp b/src/game/engine/orders.cpp index 13f8465..c40f190 100644 --- a/src/game/engine/orders.cpp +++ b/src/game/engine/orders.cpp @@ -15,8 +15,17 @@ std::vector& Callbacks() { return callbacks; } +std::vector& Filters() { + static std::vector filters; + return filters; +} + } // namespace +void FilterCreatureOrders(CreatureOrderFilter filter) { + Filters().push_back(filter); +} + void OnCreatureOrder(CreatureOrderFn callback) { Callbacks().push_back(callback); } @@ -26,6 +35,11 @@ void __fastcall Creature_ReceiveOrder(void* self, void* edx, CreatureOrder* orde RE_HOOK(0x0052e400, Creature_ReceiveOrder); void __fastcall Creature_ReceiveOrder(void* self, void* edx, CreatureOrder* order) { if (self && order) { + for (CreatureOrderFilter filter : Filters()) { + if (!filter(self, *order)) { + return; + } + } void* caller = _ReturnAddress(); for (CreatureOrderFn callback : Callbacks()) { callback(self, *order, caller); diff --git a/src/game/engine/orders.h b/src/game/engine/orders.h index 71addd1..0dc6326 100644 --- a/src/game/engine/orders.h +++ b/src/game/engine/orders.h @@ -63,6 +63,11 @@ inline void* ControlledCreature(uint8_t* engine) { using CreatureOrderFn = void (*)(void* creature, const CreatureOrder& order, void* caller); void OnCreatureOrder(CreatureOrderFn callback); +// Asked before the callbacks: an order any filter refuses (returns false) is +// dropped, the creature never gets it. +using CreatureOrderFilter = bool (*)(void* creature, const CreatureOrder& order); +void FilterCreatureOrders(CreatureOrderFilter filter); + // Creature fields. constexpr size_t kCreatureId = 0xc; constexpr size_t kCreatureX = 0x1c; diff --git a/src/mods/aurarecast.cpp b/src/mods/aurarecast.cpp index d2a9da2..3675888 100644 --- a/src/mods/aurarecast.cpp +++ b/src/mods/aurarecast.cpp @@ -127,7 +127,7 @@ uint32_t __fastcall Arts_AddEffect(void* self, void* edx, uint32_t kind, uint32_ RE_HOOK(0x00582c10, Arts_AddEffect); uint32_t __fastcall Arts_AddEffect(void* self, void* edx, uint32_t kind, uint32_t a2, float value, float duration, uint32_t art, uint32_t a6, uint32_t a7, uint32_t a8) { - if (!mods::dwarfarts::KeepEffect(kind, art, duration)) { + if (!mods::dwarfarts::KeepEffect(kind, art)) { return 1; } if (self && mods::aurarecast::Renew(static_cast(self), kind, duration, art)) { diff --git a/src/mods/dwarfarts.cpp b/src/mods/dwarfarts.cpp index 0df8ec4..5d6cedb 100644 --- a/src/mods/dwarfarts.cpp +++ b/src/mods/dwarfarts.cpp @@ -50,9 +50,12 @@ // 10 while the cell is passable (world 0x636b60) and keeps the last point a // creature can stand on (0x636eb0); a wall in the way ends the walk, so the // target has to be in sight. The mod aims Eingraben at a point (definition -// +0x3c: 5 around the caster -> 3 a point), runs the original case (the bonus -// cut to kBarrierSeconds) and moves the dwarf to the end of that walk; the -// cursor limits it to the visible area. +// +0x3c: 5 around the caster -> 3 a point), runs the original case (the barrier +// and its bonus last kBarrierSeconds) and moves the dwarf to the end of that +// walk; the cursor limits it to the visible area. The move takes 2 s without +// orders: the barrier turns solid, he moves inside it, it clears again +// ("not instant ... from transparent to solid and back", 2026-10-28; sinking +// him into the ground failed: the camera follows his height). #include "mods/dwarfarts.h" @@ -430,7 +433,14 @@ void DrawMarker(const uint8_t* view, void* device, const Grenade& grenade) { // --- Eingraben ---------------------------------------------------------------- -constexpr float kBarrierSeconds = 3.0f; +// The art's duration (0x4d24f0: definition +0x76 + level × +0x78, at most +// +0x82 when set; Eingraben 7 + level, at most 15), which the barrier, its +// bonus and the tooltip use: a fixed kBarrierSeconds ("down to 4 seconds", +// 2026-10-28). +constexpr int16_t kBarrierSeconds = 4; +constexpr size_t kSpellDurationBase = 0x76; // i16 +constexpr size_t kSpellDurationPerLevel = 0x78; // i16 +constexpr size_t kSpellDurationMax = 0x82; // u16, 0: none // The barrier's effects (logged 2026-10-28, all with art 0x39): kind 0x3a // value 0 from its particles (0x7b9602), which holds the dwarf in place, and // kind 2 value 0.5 plus kinds 7..0xa value 30 from its update (0x5b5862..), @@ -486,6 +496,18 @@ game::MapPosition DigDestination(uint8_t* creature, int32_t x, int32_t y) { return best; } +// The dig takes kFadeMs twice: the barrier turns solid, the dwarf moves +// inside it, and it turns transparent again; he takes no orders meanwhile. +constexpr DWORD kFadeMs = 1000; + +struct Digging { + uint32_t id = 0; // 0: none + DWORD start = 0; // GetTickCount of the cast, or of the move once moved + game::MapPosition to; + bool moved = false; +}; +Digging g_dig; + void Dig(uint8_t* creature, int32_t x, int32_t y) { const auto& from = *reinterpret_cast(creature + game::kObjectPosition); const game::MapPosition to = DigDestination(creature, x, y); @@ -493,8 +515,59 @@ void Dig(uint8_t* creature, int32_t x, int32_t y) { if (to.x == from.x && to.y == from.y) { return; } - game::Teleport(creature, static_cast(to.x / game::kMapCellSize), - static_cast(to.y / game::kMapCellSize)); + g_dig = {*reinterpret_cast(creature + game::kCreatureId), GetTickCount(), to, false}; +} + +// Engine tick: once the barrier is solid, the move; the dig's end. +void OnTick(uint8_t*) { + auto* creature = g_dig.id ? static_cast(game::GetCreature(g_dig.id)) : nullptr; + if (!creature || GetTickCount() - g_dig.start < kFadeMs) { + g_dig.id = creature ? g_dig.id : 0; + return; + } + if (g_dig.moved) { + g_dig.id = 0; + return; + } + game::Teleport(creature, static_cast(g_dig.to.x / game::kMapCellSize), + static_cast(g_dig.to.y / game::kMapCellSize)); + g_dig.moved = true; + g_dig.start = GetTickCount(); +} + +bool AcceptOrder(void* creature, const game::CreatureOrder&) { + return !g_dig.id || *reinterpret_cast(static_cast(creature) + game::kCreatureId) != g_dig.id; +} + +void OnGameEnd() { + g_dig = {}; +} + +// The barrier (cParticleSystem_dwarf_shield, draw 0x7b92b0) draws its model +// with alpha pulse × 128 (pulse 0.4..1, +0x20b8; __ftol at 0x7b9483), then +// again additively. While a dig runs that alpha goes to opaque and back. +constexpr uint32_t kShieldAlphaToInt = 0x007b9483; +constexpr uint32_t kFloatToInt = 0x00848f94; // __ftol +constexpr int32_t kOpaque = 255; + +int32_t __cdecl ShieldAlpha(int32_t alpha) { + if (!g_dig.id) { + return alpha; + } + const float part = std::fmin(static_cast(GetTickCount() - g_dig.start) / kFadeMs, 1.0f); + const float solid = g_dig.moved ? 1.0f - part : part; + return alpha + static_cast(static_cast(kOpaque - alpha) * solid); +} + +__declspec(naked) void ShieldAlphaThunk() { + __asm { + mov eax, kFloatToInt + call eax + push eax + call ShieldAlpha + add esp, 4 + ret + } } bool AimEingraben() { @@ -503,13 +576,18 @@ bool AimEingraben() { return false; } def[kSpellLaunchType] = static_cast(game::LaunchType::Ground); + *reinterpret_cast(def + kSpellDurationBase) = kBarrierSeconds; + *reinterpret_cast(def + kSpellDurationPerLevel) = 0; + *reinterpret_cast(def + kSpellDurationMax) = 0; return true; } void Startup() { const bool aim = AimEingraben(); - re::Log("dwarfarts: Eingraben aimed at a point, barrier %g s%s", kBarrierSeconds, aim ? "" : " (patch FAILED)"); - const bool impact =re::RedirectCall(kImpactCreate, kCreateAt, reinterpret_cast(&CreateImpactThunk)); + re::Log("dwarfarts: Eingraben aimed at a point, barrier %d s%s", kBarrierSeconds, aim ? "" : " (patch FAILED)"); + const bool fade = re::RedirectCall(kShieldAlphaToInt, kFloatToInt, reinterpret_cast(&ShieldAlphaThunk)); + re::Log("dwarfarts: Eingraben barrier turns solid%s", fade ? "" : " (patch FAILED)"); + const bool impact = re::RedirectCall(kImpactCreate, kCreateAt, reinterpret_cast(&CreateImpactThunk)); re::Log("dwarfarts: Kanonenschuss impact only on stuns%s", impact ? "" : " (patch FAILED)"); uint8_t call[sizeof(kPushStepCode)]; std::memset(call, 0x90, sizeof(call)); @@ -534,18 +612,17 @@ RE_STARTUP(DwarfArtsStartup, Startup); struct Registration { Registration() { game::OnCreatureOrder(&OnOrder); + game::FilterCreatureOrders(&AcceptOrder); + game::OnGameEnd(&OnGameEnd); + game::OnEngineTick(&OnTick); game::AddTooltipFilter(game::kArtMoerser, &RewriteTooltip); } } g_registration; } // namespace -bool KeepEffect(uint32_t kind, uint32_t art, float& duration) { - if (art != game::kArtEingraben) { - return true; - } - duration = std::fmin(duration, kBarrierSeconds); - return kind != kEffectDugIn; +bool KeepEffect(uint32_t kind, uint32_t art) { + return art != game::kArtEingraben || kind != kEffectDugIn; } uint32_t* __fastcall GatherTargets(void* manager, void*, uint32_t* out, const void* position, uint32_t type, diff --git a/src/mods/dwarfarts.h b/src/mods/dwarfarts.h index 40a7437..add3c66 100644 --- a/src/mods/dwarfarts.h +++ b/src/mods/dwarfarts.h @@ -5,9 +5,8 @@ namespace mods::dwarfarts { -// Whether cCreatureArts::AddEffect adds an effect of `kind` for art `art`, -// and its duration: Eingraben no longer holds the dwarf in place, and its -// defensive bonus lasts only while the barrier does. -bool KeepEffect(uint32_t kind, uint32_t art, float& duration); +// Whether cCreatureArts::AddEffect adds an effect of `kind` for art `art`: +// Eingraben no longer holds the dwarf in place. +bool KeepEffect(uint32_t kind, uint32_t art); } // namespace mods::dwarfarts diff --git a/tools/names.txt b/tools/names.txt index a2f7246..112e99c 100644 --- a/tools/names.txt +++ b/tools/names.txt @@ -451,3 +451,7 @@ 0x005222c0 cCreature::CastSpell # thiscall (cast), ret 4: switch on the spell (cast +0x20); +0x08/+0x0c ground point; case 1 Phasenverschiebung, 0x39 Eingraben 0x00636b60 cWorld::IsPassable # thiscall (position), ret 4: world = *ObjectManager 0x00636eb0 cWorld::CanStand # thiscall (creature id, position), ret 8: may correct the position's area +0x004d24f0 cArtDef::Duration # thiscall (index, level): +0x76 + level * +0x78 (i16), at most +0x82 when set +0x007b8fc0 cParticleSystem_dwarf_shield::cParticleSystem_dwarf_shield # Eingraben's barrier (type 0x480) +0x007b91a0 cParticleSystem_dwarf_shield::Update # vf08 (dt), ret 4; pulse +0x20b8, phase +0x20ac, end +0x20a8 +0x007b92b0 cParticleSystem_dwarf_shield::Draw # vf04, ret 0x44; model alpha pulse * 128 (__ftol at 0x7b9483)