Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
485 lines
21 KiB
C++
485 lines
21 KiB
C++
// The dwarf's Kanonenschuss (0x43f) and Mörsergranate (0x43e) (request of
|
||
// 2026-10-25, docs/dwarf-arts.md).
|
||
//
|
||
// Kanonenschuss: "remove the knockback, instead it can be just a stun". On a
|
||
// successful stun roll the dwarf's combat move code (0x5145d0, case 0x43f)
|
||
// plays MOTION_HIT_A on the target (0x5936e0 at 0x517709) and sends it the
|
||
// stun order (cEvent_creature 3 from 0x5177aa, +0x14 = 100). The game's stun
|
||
// is a push (measured: about 100 units away from the attacker in 0.3 s): the
|
||
// order sets creature flag 0x20000, the push left (+0x24d) and the attacker
|
||
// (+0x259); while the flag is set, cCreature::Update (branch 0x594c82) skips
|
||
// everything else and moves the creature away from the attacker, lowering
|
||
// the push left by each step, and clears the flag at 0. The mod skips the
|
||
// motion, and for creatures the cannon stunned the push step (0x594deb)
|
||
// moves them by 0 and keeps the push left above 0 for kStunMs: they stand
|
||
// still, doing nothing, then carry on. The shot's ray (25 steps of 40 = 1000) gets 45 steps (1800),
|
||
// for the widened view ("range needs to be increased to accommodate for the
|
||
// increased viewport", 2026-10-26).
|
||
//
|
||
// Mörsergranate: "the radius of the explosion should increase every 5 skill
|
||
// levels". The grenade (type 0x3fa) creates an impact object
|
||
// (TYPE_FX_GRENADEIMPACT, 0x3fc) where it lands; that object gathers its
|
||
// targets with cObjectManager 0x5ff820 (called from 0x5ab400 at 0x5ab63d): a
|
||
// creature is hit when its distance is below the impact type's size (150)
|
||
// plus the creature type's size (object type table 0x428bb0). The mod passes
|
||
// that call a radius of size × (1 + kRadiusGrowth × (level / 5)), with the
|
||
// level of the owner's Mörsergranate.
|
||
//
|
||
// Impact marker (asked 2026-10-25): where the grenade will land and its
|
||
// explosion radius, a red circle with low alpha on the ground. The landing
|
||
// point is decided at launch (0x5145d0 case 0x43e: the target's position, or
|
||
// 200 units from the dwarf with +-15 jitter when closer) and sent to the new
|
||
// grenade with cEventFX 0x96 (constructed at 0x5169fa: code, owner, 0,
|
||
// position area / x / y / level). The construct call notes the grenade and
|
||
// the point; the circle is drawn after the world view's ground (0x62d530,
|
||
// before the objects) until the impact gathers its targets (the grenade
|
||
// object itself stays about 30 s longer).
|
||
|
||
#include <windows.h>
|
||
|
||
#include <cstdint>
|
||
#include <cstring>
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <map>
|
||
#include <vector>
|
||
|
||
#include "game/engine/art.h"
|
||
#include "game/engine/arts.h"
|
||
#include "game/engine/objects.h"
|
||
#include "game/engine/orders.h"
|
||
#include "game/engine/worldview.h"
|
||
#include "game/ui/draw.h"
|
||
#include "mods/widescreen.h"
|
||
#include "re/hook.h"
|
||
#include "re/log.h"
|
||
#include "re/patch.h"
|
||
|
||
namespace mods::dwarfarts {
|
||
|
||
uint32_t* __fastcall GatherTargets(void* manager, void*, uint32_t* out, const void* position, uint32_t type,
|
||
uint32_t only, int32_t radius, const uint8_t* fx);
|
||
void DrawMarkers(const uint8_t* view);
|
||
|
||
namespace {
|
||
|
||
// The stunned target's motion: push 3.0f, push 0.25f... (0x5176f8) up to the
|
||
// call of 0x5936e0 (0x517709, 5 bytes): jumped over.
|
||
constexpr uint32_t kCannonStunMotion = 0x005176f8;
|
||
constexpr uint8_t kStunMotionPush[] = {0x68, 0x00}; // push 0x3e800000 (first two bytes)
|
||
constexpr uint8_t kSkipStunMotion[] = {0xeb, 0x14}; // jmp 0x51770e, after the call
|
||
|
||
// The ray's step count: cmp edi, 19h (0x517389, imm8 at +2); steps of 40.
|
||
constexpr uint32_t kCannonRaySteps = 0x00517389;
|
||
constexpr uint8_t kRayStepsOriginal = 25;
|
||
constexpr uint8_t kRaySteps = 45;
|
||
|
||
// The cannon's stun order: the return address of its call [eax+18h].
|
||
constexpr uint32_t kCannonStunOrderReturn = 0x005177ad;
|
||
constexpr uint32_t kOrderStun = 3;
|
||
constexpr DWORD kStunMs = 1500;
|
||
// The push step in cCreature::Update: lea edx,[ebp+18h]; mov edi,[esp+60h]
|
||
// (y offset; x offset in ebx); mov [ebp+24dh],eax (push left).
|
||
constexpr uint32_t kPushStep = 0x00594deb;
|
||
constexpr uint8_t kPushStepCode[] = {0x8d, 0x55, 0x18, 0x8b, 0x7c, 0x24, 0x60, 0x89, 0x85, 0x4d, 0x02, 0x00, 0x00};
|
||
|
||
std::map<uint32_t, DWORD> g_stunEnds; // creature id -> GetTickCount at which its stun ends
|
||
|
||
constexpr uint32_t kGrenadeImpactType = 0x3fc; // TYPE_FX_GRENADEIMPACT
|
||
constexpr uint32_t kImpactGatherCall = 0x005ab63d; // in 0x5ab400
|
||
constexpr uint32_t kGatherInRange = 0x005ff820; // cObjectManager: objects within type sizes
|
||
constexpr size_t kFxOwner = 0x54; // u32: the creature the effect hits for
|
||
constexpr unsigned kLevelsPerStep = 5;
|
||
constexpr float kRadiusGrowth = 0.1f; // of the impact's own size, per step
|
||
|
||
// The size of object type `type` (0x428bb0 on the type table *0xaa9564,
|
||
// 50 when the table has none).
|
||
int32_t TypeSize(uint32_t type) {
|
||
using Fn = int32_t(__fastcall*)(void* table, void* edx, uint32_t type);
|
||
return reinterpret_cast<Fn>(0x00428bb0)(*reinterpret_cast<void**>(0x00aa9564), nullptr, type);
|
||
}
|
||
|
||
// The explosion radius of a grenade thrown by `owner` (world units; a
|
||
// creature is hit when within it plus its own size).
|
||
int32_t ImpactRadiusAt(unsigned level) {
|
||
const int32_t size = TypeSize(kGrenadeImpactType);
|
||
return static_cast<int32_t>(static_cast<float>(size) * (1.0f + kRadiusGrowth * (level / kLevelsPerStep)));
|
||
}
|
||
|
||
int32_t ImpactRadius(void* owner) {
|
||
return ImpactRadiusAt(owner ? game::ArtLevel(owner, game::kArtMoerser) : 0);
|
||
}
|
||
|
||
// The tooltip (game::Art::Tooltip filter, as for Kettenblitz in
|
||
// mods/chainlightning.cpp): sections start with the level line ("Aktuelle
|
||
// Stufe:", in the skill book also "Nächste erlernte Stufe:") followed by
|
||
// "Name: value" lines; each section gets "Explosionsradius: N" after its last
|
||
// value, in that line's colour.
|
||
constexpr const wchar_t* kCurrentLevelLine = L"Aktuelle Stufe:";
|
||
constexpr const wchar_t* kNextLevelLine = L"Nächste erlernte Stufe:";
|
||
constexpr const wchar_t* kValueSeparator = L": ";
|
||
constexpr size_t kColourCodeLength = 10; // "\cAARRGGBB"
|
||
|
||
std::wstring LeadingColourCodes(const std::wstring& line) {
|
||
size_t end = 0;
|
||
while (line.compare(end, 2, L"\\c") == 0 && end + kColourCodeLength <= line.size()) {
|
||
end += kColourCodeLength;
|
||
}
|
||
return line.substr(0, end);
|
||
}
|
||
|
||
void RewriteTooltip(const game::Art& art, void* creature, std::wstring& text) {
|
||
std::vector<std::wstring> lines;
|
||
for (size_t start = 0; start < text.size();) {
|
||
size_t end = text.find(L'\n', start);
|
||
end = end == std::wstring::npos ? text.size() : end + 1;
|
||
lines.push_back(text.substr(start, end - start));
|
||
start = end;
|
||
}
|
||
const unsigned current = game::ArtLevel(creature, art.Id());
|
||
std::vector<std::wstring> out;
|
||
unsigned level = current;
|
||
bool inSection = false;
|
||
size_t lastValue = SIZE_MAX; // index in `out`
|
||
const auto closeSection = [&] {
|
||
if (!inSection || lastValue == SIZE_MAX) {
|
||
return;
|
||
}
|
||
std::wstring& after = out[lastValue];
|
||
const bool last = after.empty() || after.back() != L'\n';
|
||
if (last) {
|
||
after += L'\n';
|
||
}
|
||
out.insert(out.begin() + static_cast<ptrdiff_t>(lastValue) + 1,
|
||
LeadingColourCodes(after) + L"Explosionsradius: " + std::to_wstring(ImpactRadiusAt(level)) +
|
||
(last ? L"" : L"\n"));
|
||
};
|
||
for (const std::wstring& line : lines) {
|
||
const bool currentHeader = line.find(kCurrentLevelLine) != std::wstring::npos;
|
||
const bool nextHeader = line.find(kNextLevelLine) != std::wstring::npos;
|
||
if (currentHeader || nextHeader) {
|
||
closeSection();
|
||
inSection = true;
|
||
level = nextHeader ? current + 1 : current;
|
||
lastValue = SIZE_MAX;
|
||
} else if (inSection && line.find(kValueSeparator) != std::wstring::npos) {
|
||
lastValue = out.size();
|
||
}
|
||
out.push_back(line);
|
||
}
|
||
closeSection();
|
||
text.clear();
|
||
for (const std::wstring& line : out) {
|
||
text += line;
|
||
}
|
||
}
|
||
|
||
// 0x5ab400 calls 0x5ff820 (thiscall (out, position, type, only, radius),
|
||
// ret 0x14) with the effect object in ebx.
|
||
__declspec(naked) void GatherTargetsThunk() {
|
||
__asm {
|
||
push ebx // the effect object
|
||
push dword ptr [esp+18h] // radius (-1: the type's size)
|
||
push dword ptr [esp+18h] // only objects of this type (0: any)
|
||
push dword ptr [esp+18h] // type
|
||
push dword ptr [esp+18h] // position
|
||
push dword ptr [esp+18h] // out vector
|
||
call GatherTargets
|
||
ret 14h
|
||
}
|
||
}
|
||
|
||
void OnOrder(void* creature, const game::CreatureOrder& order, void* caller) {
|
||
if (order.command == kOrderStun && reinterpret_cast<uint32_t>(caller) == kCannonStunOrderReturn) {
|
||
g_stunEnds[*reinterpret_cast<uint32_t*>(static_cast<uint8_t*>(creature) + game::kCreatureId)] =
|
||
GetTickCount() + kStunMs;
|
||
}
|
||
}
|
||
|
||
// The push step of `creature` with `left` push left after it: the push left
|
||
// to store (low dword) and whether to stay in place (high dword).
|
||
uint64_t __cdecl PushStep(uint8_t* creature, int32_t left) {
|
||
const auto it = g_stunEnds.find(*reinterpret_cast<uint32_t*>(creature + game::kCreatureId));
|
||
if (it == g_stunEnds.end()) {
|
||
return static_cast<uint32_t>(left);
|
||
}
|
||
const bool stunned = static_cast<int32_t>(GetTickCount() - it->second) < 0;
|
||
if (!stunned) {
|
||
g_stunEnds.erase(it);
|
||
}
|
||
return (uint64_t{1} << 32) | static_cast<uint32_t>(stunned ? 1 : 0);
|
||
}
|
||
|
||
__declspec(naked) void PushStepThunk() {
|
||
__asm {
|
||
push eax // push left after this step
|
||
push ebp // the creature
|
||
call PushStep
|
||
add esp, 8
|
||
test edx, edx
|
||
jz move
|
||
xor ebx, ebx // x offset
|
||
mov dword ptr [esp+64h], 0 // y offset (the caller's [esp+60h])
|
||
move:
|
||
mov edi, [esp+64h]
|
||
mov [ebp+24dh], eax
|
||
lea edx, [ebp+18h]
|
||
ret
|
||
}
|
||
}
|
||
|
||
// The cannon creates its impact effect (TYPE_FX_DWARF_SHOOT_IMPACT 0x47f,
|
||
// attached to the target with cEventFX 0x9c) on every target it hits; it stays
|
||
// on targets that were not pushed. CreateAt (0x5fbaa0, thiscall (type,
|
||
// position, link, 0, 0), ret 0x14) at 0x517855, the target in ebx: only for
|
||
// targets in the stun state (the order was not resisted); 0 otherwise, which
|
||
// the game skips (GetObject(0) is null).
|
||
constexpr uint32_t kImpactCreate = 0x00517855;
|
||
constexpr uint32_t kCreateAt = 0x005fbaa0;
|
||
constexpr size_t kCreatureFlags = 0x200;
|
||
constexpr uint32_t kCreaturePushed = 0x20000; // the stun's push state (cCreature::Update 0x594c82)
|
||
|
||
uint32_t __fastcall CreateImpact(void* manager, void*, uint32_t type, const void* position, uint32_t link, uint32_t a,
|
||
uint32_t b, const uint8_t* target) {
|
||
if (!target || !(*reinterpret_cast<const uint32_t*>(target + kCreatureFlags) & kCreaturePushed)) {
|
||
return 0;
|
||
}
|
||
using Fn = uint32_t(__fastcall*)(void*, void*, uint32_t, const void*, uint32_t, uint32_t, uint32_t);
|
||
return reinterpret_cast<Fn>(kCreateAt)(manager, nullptr, type, position, link, a, b);
|
||
}
|
||
|
||
__declspec(naked) void CreateImpactThunk() {
|
||
__asm {
|
||
push ebx // the target
|
||
push dword ptr [esp+18h]
|
||
push dword ptr [esp+18h]
|
||
push dword ptr [esp+18h]
|
||
push dword ptr [esp+18h]
|
||
push dword ptr [esp+18h]
|
||
call CreateImpact
|
||
ret 14h
|
||
}
|
||
}
|
||
|
||
// --- impact marker ---------------------------------------------------------
|
||
|
||
constexpr uint32_t kGrenadeEventConstruct = 0x005169fa; // call cEventFX::Construct
|
||
constexpr uint32_t kFxEventConstruct = 0x0041e0a0; // thiscall, ret 0x44
|
||
constexpr uint32_t kFxEventLand = 0x96;
|
||
constexpr size_t kObjectId = 0xc;
|
||
constexpr uint32_t kMarkerFill = 0x40ff0000; // red, low alpha
|
||
constexpr uint32_t kMarkerRim = 0xa0ff2020;
|
||
constexpr float kRimWidth = 2.0f; // screen pixels
|
||
constexpr int kCircleSegments = 48;
|
||
|
||
// A grenade object outlives its explosion by about 30 s (measured
|
||
// 2026-10-26), so its marker ends with the explosion: the impact gathering
|
||
// its targets (GatherTargets; it does so on several frames, at exactly the
|
||
// noted landing point).
|
||
constexpr float kImpactMatchDistance = 100.0f; // world units; the landing jitter is +-15
|
||
|
||
struct Grenade {
|
||
uint8_t* object;
|
||
uint32_t id;
|
||
uint32_t owner;
|
||
float x, y; // landing point, world units
|
||
float radius;
|
||
};
|
||
std::vector<Grenade> g_grenades;
|
||
|
||
// The explosion of `owner`'s grenade at `position` (world units): ends the
|
||
// marker of that grenade, the owner's one closest to it.
|
||
void OnImpact(uint32_t owner, float x, float y) {
|
||
auto closest = g_grenades.end();
|
||
float best = kImpactMatchDistance;
|
||
for (auto it = g_grenades.begin(); it != g_grenades.end(); ++it) {
|
||
const float distance = std::hypot(it->x - x, it->y - y);
|
||
if (it->owner == owner && distance < best) {
|
||
best = distance;
|
||
closest = it;
|
||
}
|
||
}
|
||
if (closest != g_grenades.end()) {
|
||
g_grenades.erase(closest);
|
||
}
|
||
}
|
||
|
||
// Construct's arguments: code, owner id, target id, position (area, x, y,
|
||
// level), ...; the new grenade object is in esi.
|
||
void __cdecl NoteGrenade(const uint32_t* args, uint8_t* grenade) {
|
||
if (args[0] != kFxEventLand || !grenade) {
|
||
return;
|
||
}
|
||
const auto x = static_cast<float>(static_cast<int32_t>(args[4]));
|
||
const auto y = static_cast<float>(static_cast<int32_t>(args[5]));
|
||
const auto radius = static_cast<float>(ImpactRadius(game::GetCreature(args[1])));
|
||
g_grenades.push_back({grenade, *reinterpret_cast<uint32_t*>(grenade + kObjectId), args[1], x, y, radius});
|
||
}
|
||
|
||
__declspec(naked) void GrenadeEventThunk() {
|
||
__asm {
|
||
pushad
|
||
lea eax, [esp+24h] // the arguments (above pushad and the return address)
|
||
push esi // the grenade
|
||
push eax
|
||
call NoteGrenade
|
||
add esp, 8
|
||
popad
|
||
mov eax, kFxEventConstruct
|
||
jmp eax
|
||
}
|
||
}
|
||
|
||
bool Alive(const Grenade& grenade) {
|
||
return game::GetObjectById(grenade.id) == grenade.object;
|
||
}
|
||
|
||
using SetRenderStateFn = HRESULT(__stdcall*)(void*, DWORD, DWORD);
|
||
using GetRenderStateFn = HRESULT(__stdcall*)(void*, DWORD, DWORD*);
|
||
|
||
// Render states for a translucent overlay without depth, restored after.
|
||
class OverlayStates {
|
||
public:
|
||
explicit OverlayStates(void* device) : device_(device) {
|
||
void** vtable = *static_cast<void***>(device);
|
||
set_ = reinterpret_cast<SetRenderStateFn>(vtable[game::kDeviceSetRenderState]);
|
||
const auto get = reinterpret_cast<GetRenderStateFn>(vtable[game::kDeviceGetRenderState]);
|
||
for (size_t i = 0; i < kCount; ++i) {
|
||
get(device_, kStates[i], &saved_[i]);
|
||
set_(device_, kStates[i], kValues[i]);
|
||
}
|
||
}
|
||
~OverlayStates() {
|
||
for (size_t i = 0; i < kCount; ++i) {
|
||
set_(device_, kStates[i], saved_[i]);
|
||
}
|
||
}
|
||
OverlayStates(const OverlayStates&) = delete;
|
||
OverlayStates& operator=(const OverlayStates&) = delete;
|
||
|
||
private:
|
||
// D3DRENDERSTATE ZENABLE, ZWRITEENABLE off; alpha blending SRCALPHA /
|
||
// INVSRCALPHA.
|
||
static constexpr size_t kCount = 5;
|
||
static constexpr DWORD kStates[kCount] = {7, 14, game::kStateAlphaBlendEnable, game::kStateSourceBlend,
|
||
game::kStateDestinationBlend};
|
||
static constexpr DWORD kValues[kCount] = {FALSE, FALSE, TRUE, 5, 6};
|
||
void* device_;
|
||
SetRenderStateFn set_ = nullptr;
|
||
DWORD saved_[kCount] = {};
|
||
};
|
||
|
||
game::ScreenVertex Vertex(const game::Point2& p, uint32_t colour) {
|
||
return {p.x, p.y, 0, 1, colour, 0, 0, 0};
|
||
}
|
||
|
||
// The circle on the ground: a translucent disc and a brighter rim.
|
||
void DrawMarker(const uint8_t* view, void* device, const Grenade& grenade) {
|
||
const float halfWidth = static_cast<float>(mods::widescreen::RenderWidth()) / 2;
|
||
const auto screen = [&](float x, float y) {
|
||
return game::PlaneToScreen(view, game::WorldToPlane(x, y), halfWidth);
|
||
};
|
||
const game::Point2 centre = screen(grenade.x, grenade.y);
|
||
game::Point2 rim[kCircleSegments + 1];
|
||
for (int i = 0; i <= kCircleSegments; ++i) {
|
||
const float angle = 6.2831853f * static_cast<float>(i % kCircleSegments) / kCircleSegments;
|
||
rim[i] = screen(grenade.x + grenade.radius * std::cos(angle), grenade.y + grenade.radius * std::sin(angle));
|
||
}
|
||
// The rim band reaches inwards from the circle by kRimWidth.
|
||
const auto inner = [&](const game::Point2& p) {
|
||
const float dx = centre.x - p.x, dy = centre.y - p.y;
|
||
const float length = std::hypot(dx, dy);
|
||
return length > kRimWidth ? game::Point2{p.x + dx / length * kRimWidth, p.y + dy / length * kRimWidth}
|
||
: centre;
|
||
};
|
||
static std::vector<game::ScreenVertex> vertices;
|
||
vertices.clear();
|
||
for (int i = 0; i < kCircleSegments; ++i) {
|
||
vertices.insert(vertices.end(),
|
||
{Vertex(centre, kMarkerFill), Vertex(rim[i], kMarkerFill), Vertex(rim[i + 1], kMarkerFill)});
|
||
}
|
||
for (int i = 0; i < kCircleSegments; ++i) {
|
||
const game::Point2 a = rim[i], b = rim[i + 1], c = inner(rim[i]), d = inner(rim[i + 1]);
|
||
vertices.insert(vertices.end(), {Vertex(a, kMarkerRim), Vertex(b, kMarkerRim), Vertex(c, kMarkerRim),
|
||
Vertex(c, kMarkerRim), Vertex(b, kMarkerRim), Vertex(d, kMarkerRim)});
|
||
}
|
||
game::FillTriangles(device, vertices.data(), vertices.size());
|
||
}
|
||
|
||
void Startup() {
|
||
const bool impact = re::RedirectCall(kImpactCreate, kCreateAt, reinterpret_cast<const void*>(&CreateImpactThunk));
|
||
re::Log("dwarfarts: Kanonenschuss impact only on stuns%s", impact ? "" : " (patch FAILED)");
|
||
uint8_t call[sizeof(kPushStepCode)];
|
||
std::memset(call, 0x90, sizeof(call));
|
||
call[0] = 0xe8;
|
||
const int32_t rel = reinterpret_cast<int32_t>(&PushStepThunk) - static_cast<int32_t>(kPushStep + 5);
|
||
std::memcpy(call + 1, &rel, sizeof(rel));
|
||
const bool hold = re::ReplaceCode(kPushStep, kPushStepCode, call, sizeof(call));
|
||
re::Log("dwarfarts: Kanonenschuss stun holds in place%s", hold ? "" : " (patch FAILED)");
|
||
const bool stun = re::ReplaceCode(kCannonStunMotion, kStunMotionPush, kSkipStunMotion, sizeof(kSkipStunMotion));
|
||
re::Log("dwarfarts: Kanonenschuss without stun motion%s", stun ? "" : " (patch FAILED)");
|
||
const bool range = re::PatchInstructionByte(kCannonRaySteps, 2, kRayStepsOriginal, kRaySteps);
|
||
re::Log("dwarfarts: Kanonenschuss range %d%s", kRaySteps * 40, range ? "" : " (patch FAILED)");
|
||
const bool radius =
|
||
re::RedirectCall(kImpactGatherCall, kGatherInRange, reinterpret_cast<const void*>(&GatherTargetsThunk));
|
||
re::Log("dwarfarts: Mörsergranate radius%s", radius ? "" : " (patch FAILED)");
|
||
const bool marker = re::RedirectCall(kGrenadeEventConstruct, kFxEventConstruct,
|
||
reinterpret_cast<const void*>(&GrenadeEventThunk));
|
||
re::Log("dwarfarts: Mörsergranate impact marker%s", marker ? "" : " (patch FAILED)");
|
||
}
|
||
RE_STARTUP(DwarfArtsStartup, Startup);
|
||
|
||
struct Registration {
|
||
Registration() {
|
||
game::OnCreatureOrder(&OnOrder);
|
||
game::AddTooltipFilter(game::kArtMoerser, &RewriteTooltip);
|
||
}
|
||
} g_registration;
|
||
|
||
} // namespace
|
||
|
||
uint32_t* __fastcall GatherTargets(void* manager, void*, uint32_t* out, const void* position, uint32_t type,
|
||
uint32_t only, int32_t radius, const uint8_t* fx) {
|
||
if (type == kGrenadeImpactType && radius < 0 && fx) {
|
||
const uint32_t owner = *reinterpret_cast<const uint32_t*>(fx + kFxOwner);
|
||
radius = ImpactRadius(game::GetCreature(owner));
|
||
// The position: area (u16), x, y (int32, world units), level.
|
||
const auto* at = static_cast<const uint8_t*>(position);
|
||
OnImpact(owner, static_cast<float>(*reinterpret_cast<const int32_t*>(at + 4)),
|
||
static_cast<float>(*reinterpret_cast<const int32_t*>(at + 8)));
|
||
}
|
||
using Fn = uint32_t*(__fastcall*)(void*, void*, uint32_t*, const void*, uint32_t, uint32_t, int32_t);
|
||
return reinterpret_cast<Fn>(kGatherInRange)(manager, nullptr, out, position, type, only, radius);
|
||
}
|
||
|
||
// The impact markers of the grenades in flight, on the ground of `view`.
|
||
void DrawMarkers(const uint8_t* view) {
|
||
g_grenades.erase(std::remove_if(g_grenades.begin(), g_grenades.end(), [](const Grenade& g) { return !Alive(g); }),
|
||
g_grenades.end());
|
||
void* device = game::RenderDevice();
|
||
if (g_grenades.empty() || !device) {
|
||
return;
|
||
}
|
||
OverlayStates states(device);
|
||
for (const Grenade& grenade : g_grenades) {
|
||
DrawMarker(view, device, grenade);
|
||
}
|
||
}
|
||
|
||
} // namespace mods::dwarfarts
|
||
|
||
namespace game {
|
||
|
||
// cWorldView0::DrawGroundQuads (0x62d530, thiscall (renderer), ret 4): the
|
||
// ground; the objects are drawn after it.
|
||
void __fastcall WorldView_DrawGroundQuads(uint8_t* self, void* edx, void* renderer);
|
||
RE_HOOK(0x0062d530, WorldView_DrawGroundQuads);
|
||
void __fastcall WorldView_DrawGroundQuads(uint8_t* self, void* edx, void* renderer) {
|
||
WorldView_DrawGroundQuads_original(self, edx, renderer);
|
||
if (self) {
|
||
mods::dwarfarts::DrawMarkers(self);
|
||
}
|
||
}
|
||
|
||
} // namespace game
|