Skills, auto-attack, aura and chain lightning rework, Esc, cache fix

- Skill bar: no art selection; slot 0 is the right-click art, keys 0-6 cast
  slots directly; cooldown labels in exact seconds; slot 0 set apart.
- Arts regenerate on their own (casting no longer refills the group).
- Auto-attack after targeting or casting at a hostile creature, at the speed
  of a held button.
- Auras can be recast while active (renew, no second visual).
- Blicke wie Klingen hits every enemy in range, physical, with life leech.
- Blitzschlag steers the game's own chain: 3 + level/5 hits, 1 + level/10
  chains, re-hits a lone enemy.
- Weapon sets: click selects, drag only from the selected set; the leftmost
  set is clickable (signed hit tests).
- Esc closes open windows before opening the game menu.
- HUD: standalone buttons and texts are centred.
- Record cache sized for widened views (town lag while walking).
- Shared game code: orders, fx events, text drawing, cooldowns, effects.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-16 00:11:38 +02:00
1 parent f721d2a486
commit 1fd89524d3
41 files changed
+2347 -214

No files matched your search

+37
View File
@@ -205,6 +205,43 @@ constexpr re::BytePatch kTaskbarSlotLoopBounds[] = { // cmp reg, 5
{0x006dffd2, 3, 5}, // SetSlotCount: cmp si, 5
{0x006df7c5, 1, 5}, {0x006e2593, 1, 5}, {0x006dffd8, 1, 5}, // mov reg, 5 (count / clamp)
};
// Combat-art slots without a selection: slot 0 (the right mouse button's art)
// is drawn as the selected one, every slot's ready art with the selected icon.
constexpr re::CodeReplacement kTaskbarSkillSelectionLook[] = {
// UpdateSlotHighlight, slot background: `cmp ebx, eax` (slot == selected)
// -> `test ebx, ebx` (slot == 0)
{0x006dfa77, {0x3b, 0xd8}, {0x85, 0xdb}, 2},
// RenderArtSlots, frame: `cmp edi, eax` -> `test edi, edi`
{0x006e3813, {0x3b, 0xf8}, {0x85, 0xff}, 2},
// RenderArtSlots: a ready art is drawn with its third ("selected") icon
// only in the selected slot: `jne` skipped
{0x006e3575, {0x75, 0x54}, {0x90, 0x90}, 2},
};
// Taskbar code passes mouse positions to the slot controls' hit tests as
// `and reg, 0FFFFh` (zero-extended u16): with 7 slots the leftmost weapon set
// lies at x < 0 of the layout and never matched. Sign-extended instead:
// `and edx, 0FFFFh` -> `movsx edx, dx`, `and eax, 0FFFFh` -> `cwde`.
constexpr re::CodeReplacement kTaskbarSignedHitTests[] = {
{0x006dfcfe, {0x81, 0xe2, 0xff, 0xff, 0x00, 0x00}, {0x0f, 0xbf, 0xd2, 0x90, 0x90, 0x90}, 6},
{0x006dfd07, {0x81, 0xe2, 0xff, 0xff, 0x00, 0x00}, {0x0f, 0xbf, 0xd2, 0x90, 0x90, 0x90}, 6},
{0x006dfd5e, {0x81, 0xe2, 0xff, 0xff, 0x00, 0x00}, {0x0f, 0xbf, 0xd2, 0x90, 0x90, 0x90}, 6},
{0x006dfd67, {0x81, 0xe2, 0xff, 0xff, 0x00, 0x00}, {0x0f, 0xbf, 0xd2, 0x90, 0x90, 0x90}, 6},
{0x006e1a5c, {0x81, 0xe2, 0xff, 0xff, 0x00, 0x00}, {0x0f, 0xbf, 0xd2, 0x90, 0x90, 0x90}, 6},
{0x006e1a67, {0x81, 0xe2, 0xff, 0xff, 0x00, 0x00}, {0x0f, 0xbf, 0xd2, 0x90, 0x90, 0x90}, 6},
{0x006e2093, {0x81, 0xe2, 0xff, 0xff, 0x00, 0x00}, {0x0f, 0xbf, 0xd2, 0x90, 0x90, 0x90}, 6},
{0x006e209e, {0x81, 0xe2, 0xff, 0xff, 0x00, 0x00}, {0x0f, 0xbf, 0xd2, 0x90, 0x90, 0x90}, 6},
{0x006e20e7, {0x81, 0xe2, 0xff, 0xff, 0x00, 0x00}, {0x0f, 0xbf, 0xd2, 0x90, 0x90, 0x90}, 6},
{0x006e20f2, {0x81, 0xe2, 0xff, 0xff, 0x00, 0x00}, {0x0f, 0xbf, 0xd2, 0x90, 0x90, 0x90}, 6},
{0x006e1a0c, {0x25, 0xff, 0xff, 0x00, 0x00}, {0x98, 0x90, 0x90, 0x90, 0x90}, 5},
{0x006e1a16, {0x25, 0xff, 0xff, 0x00, 0x00}, {0x98, 0x90, 0x90, 0x90, 0x90}, 5},
{0x006e1e8e, {0x25, 0xff, 0xff, 0x00, 0x00}, {0x98, 0x90, 0x90, 0x90, 0x90}, 5},
{0x006e1e98, {0x25, 0xff, 0xff, 0x00, 0x00}, {0x98, 0x90, 0x90, 0x90, 0x90}, 5},
{0x006e1ee1, {0x25, 0xff, 0xff, 0x00, 0x00}, {0x98, 0x90, 0x90, 0x90, 0x90}, 5},
{0x006e1eeb, {0x25, 0xff, 0xff, 0x00, 0x00}, {0x98, 0x90, 0x90, 0x90, 0x90}, 5},
};
// SetSlotCount: the decorative strip right of the centre starts at
// 0x23a + n * 0x40 (`add ecx, 23Ah`).
constexpr uint32_t kTaskbarRightStripStart = 0x006e01cc;
// cCreature::AssignArtToSlot (0x55fac0) rejects slots > 4 (`cmp eax, 4`).
constexpr re::BytePatch kAssignArtMaxSlot[] = {{0x0055fae3, 2, 4}};
// SlotCountForLevel (stdcall, level): (level, count) table at 0x95503c, max 5.
+117
View File
@@ -1,6 +1,7 @@
#pragma once
// Combat arts: the definition tables and how they are targeted.
#include <cstddef>
#include <cstdint>
namespace game {
@@ -39,6 +40,122 @@ inline LaunchType ArtLaunchType(uint16_t id) {
return LaunchType::Unknown;
}
// cCreature::ResolveArtId (0x5521e0, (ref, 0) as the taskbar calls it): the
// art id of an art slot's reference (cInventoryEntry::GetArtSlotRef) for this
// creature.
inline uint16_t ResolveArtId(void* creature, uint32_t slotRef) {
using Fn = uint32_t(__fastcall*)(void*, void*, uint32_t, uint32_t);
return static_cast<uint16_t>(reinterpret_cast<Fn>(0x005521e0)(creature, nullptr, slotRef, 0));
}
// A creature's arts state (cCreatureArts, at creature +0x3a8).
constexpr size_t kCreatureArts = 0x3a8;
// Active effects (buffs, auras, art locks): 0x20-byte entries at +0x11a /
// +0x11e (begin / end) of cCreatureArts: kind +0, remaining float +0x18
// (seconds), art u16 +0x1c. Kind 0x22 is the lock cCreature::StartArtCooldown
// (0x539ce0) adds; cCreatureArts::AddEffect 0x582c10, HasEffectOfArt 0x5829c0.
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;
constexpr size_t kEffectArt = 0x1c;
constexpr uint32_t kEffectKindArtLock = 0x22;
constexpr uint16_t kNoArt = 0xffff; // art field of effects without an art
// Arts named in the code (icon names from g_spellDefs / g_combatMoveDefs).
constexpr uint16_t kArtHeldenmut = 0x3f7;
constexpr uint16_t kArtKlingenblick = 0x3f8;
constexpr uint16_t kArtEnergieblitze = 0x3fb;
constexpr uint16_t kArtFlammenwerfer = 0x43d;
constexpr uint16_t kArtMeteor = 0x43e;
constexpr uint16_t kArtKanone = 0x43f;
// cCreatureArts fields.
constexpr size_t kArtsCreature = 0x4; // cCreature*: the owner
constexpr size_t kArtsComboRunning = 0x59; // u8: combo step in progress
constexpr size_t kArtsFlags = 0x148;
constexpr uint32_t kArtsFlagFasterMagicRegen = 0x8; // group 1 regeneration at 80 %
constexpr uint32_t kArtsFlagFasterCombatRegen = 0x4; // group 2 regeneration at 80 %
// cCreature: the combo step being executed (u8), 0 outside combos.
constexpr size_t kCreatureComboStep = 0x2b4;
// Regeneration groups of cooldown entries (+0).
constexpr uint32_t kRegenGroupMagic = 1;
constexpr uint32_t kRegenGroupCombat = 2;
constexpr uint32_t kRegenGroupCombo = 3; // handled by the combo branch
// Arts regenerating in the magic group whatever their entry says
// (cCreatureArts::AddGroupRegen; the countdown only knows Energieblitze).
inline bool InMagicRegenGroup(uint32_t group, uint16_t art) {
return group == kRegenGroupMagic || art == kArtEnergieblitze || art == kArtFlammenwerfer || art == kArtMeteor ||
art == kArtKanone;
}
// Art cooldowns ("regeneration"): a vector of 0x16-byte entries at +0x102 /
// +0x106 (begin / end) of cCreatureArts:
// +0 u32 regeneration group (kRegenGroup*)
// +4 u16 art id
// +6 u8 level, +7 u8 bonus levels (the total follows from both)
// +0xa float total
// +0xe float speed factor
// +0x12 float remaining (ready at <= 0.01, cArtCooldowns::IsReady 0x580cb0)
// Every creature update (0x562998) lowers remaining by
// speed factor * rate * seconds,
// rate = cCreatureArts +0xee for the magic group and Energieblitze, +0xf2
// otherwise.
constexpr size_t kCooldownsBegin = 0x102;
constexpr size_t kCooldownsEnd = 0x106;
constexpr size_t kCooldownSize = 0x16;
constexpr size_t kCooldownGroup = 0x0;
constexpr size_t kCooldownArt = 0x4;
constexpr size_t kCooldownLevel = 0x6; // u8
constexpr size_t kCooldownBonusLevels = 0x7; // u8, from items etc.
constexpr size_t kCooldownTotal = 0xa;
constexpr size_t kCooldownSpeed = 0xe;
constexpr size_t kCooldownRemaining = 0x12;
constexpr size_t kArtsRegenRateMagic = 0xee; // float, units per second
constexpr size_t kArtsRegenRateCombat = 0xf2;
constexpr float kArtReadyCooldown = 0.01f;
inline float& Float(uint8_t* p, size_t offset) {
return *reinterpret_cast<float*>(p + offset);
}
// The cooldown entry of art `art` (cArtCooldowns::Find 0x581c00, which also
// matches an art's variants) in cCreatureArts `arts`, or null.
inline uint8_t* FindCooldownEntry(uint8_t* arts, uint16_t art) {
using FindFn = uint32_t(__fastcall*)(void*, void*, uint32_t);
const uint32_t index = reinterpret_cast<FindFn>(0x00581c00)(arts, nullptr, art);
auto* begin = *reinterpret_cast<uint8_t**>(arts + kCooldownsBegin);
auto* end = *reinterpret_cast<uint8_t**>(arts + kCooldownsEnd);
if (!begin || index >= static_cast<uint32_t>(end - begin) / kCooldownSize) {
return nullptr;
}
return begin + index * kCooldownSize;
}
struct ArtCooldown {
float remaining = 0.0f; // in the game's units
float total = 0.0f;
float seconds = 0.0f; // remaining at the current regeneration rate
};
inline ArtCooldown GetArtCooldown(void* creature, uint16_t art) {
auto* arts = static_cast<uint8_t*>(creature) + kCreatureArts;
uint8_t* entry = FindCooldownEntry(arts, art);
if (!entry) {
return {};
}
const bool magic = *reinterpret_cast<uint32_t*>(entry + kCooldownGroup) == kRegenGroupMagic ||
*reinterpret_cast<uint16_t*>(entry + kCooldownArt) == kArtEnergieblitze;
const float perSecond =
Float(entry, kCooldownSpeed) * Float(arts, magic ? kArtsRegenRateMagic : kArtsRegenRateCombat);
const float remaining = Float(entry, kCooldownRemaining);
return {remaining, Float(entry, kCooldownTotal), perSecond > 0.0f ? remaining / perSecond : 0.0f};
}
// cCreature::IsAlly (0x548f20): whether object `id` is an ally of `creature`.
inline bool IsAlly(void* creature, uint32_t id) {
using Fn = char(__fastcall*)(void*, void*, uint32_t);
+53
View File
@@ -0,0 +1,53 @@
#include "game/engine/fx.h"
#include <vector>
#include "re/hook.h"
namespace game {
namespace {
std::vector<FxEventFn>& Callbacks() {
static std::vector<FxEventFn> callbacks;
return callbacks;
}
std::vector<FxEventFn>& BeforeCallbacks() {
static std::vector<FxEventFn> callbacks;
return callbacks;
}
} // namespace
void OnFxEvent(FxEventFn callback) {
Callbacks().push_back(callback);
}
void BeforeFxEvent(FxEventFn callback) {
BeforeCallbacks().push_back(callback);
}
uint32_t CreateObjectAt(uint32_t type, const void* position, uint32_t link) {
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, link, 0, 0) : 0;
}
// cObjectFX::ReceiveEvent (0x5a37b0, vf18).
void __fastcall ObjectFX_ReceiveEvent(void* self, void* edx, uint32_t* event);
RE_HOOK(0x005a37b0, ObjectFX_ReceiveEvent);
void __fastcall ObjectFX_ReceiveEvent(void* self, void* edx, uint32_t* event) {
if (self && event) {
for (FxEventFn callback : BeforeCallbacks()) {
callback(static_cast<uint8_t*>(self), event);
}
}
ObjectFX_ReceiveEvent_original(self, edx, event);
if (self && event) {
for (FxEventFn callback : Callbacks()) {
callback(static_cast<uint8_t*>(self), event);
}
}
}
} // namespace game
+42
View File
@@ -0,0 +1,42 @@
#pragma once
// Effect objects (cObjectFX) and the events that set them up (cEventFX).
#include <cstddef>
#include <cstdint>
namespace game {
constexpr uint32_t kObjectFXVtable = 0x0088f99c;
constexpr size_t kObjectType = 0x10; // cObject: the type CreateAt was given
// cEventFX (0x7c bytes, cEventFX::Construct 0x41e0a0): dwords
// [1] code, [2] owner / caster id, [3] target id, [4..7] position,
// [8..10] and [11..13] two strings (by value), [14], [15],
// [16..29] a copied 14-dword block, [30].
// Codes seen: 0x42 attach an aura to its owner (art at [15]); 0x60 a
// Blitzschlag hit (type 872 object: caster, target, damage [14], damage block
// [16..29]; [14] the hits left in its chain, counted down per jump).
constexpr size_t kFxEventDwords = 0x1f;
constexpr uint32_t kFxEventVtable = 0x0088e9b4;
constexpr uint32_t kFxEventAttachAura = 0x42;
constexpr uint32_t kFxEventBoltHit = 0x60;
// Called after cObjectFX::ReceiveEvent (0x5a37b0) handled `event` (while its
// strings are still alive). Register at startup (static initialisation).
using FxEventFn = void (*)(uint8_t* fx, uint32_t* event);
void OnFxEvent(FxEventFn callback);
// Called before the object handles `event` (may change it).
void BeforeFxEvent(FxEventFn callback);
// cObjectManager::CreateAt (0x5fbaa0): a new object of `type` at `position`
// (4 dwords), linked to object `link` (e.g. the previous lightning segment);
// its id.
uint32_t CreateObjectAt(uint32_t type, const void* position, uint32_t link = 0);
// Sends `event` to object `object` (its vf18).
inline void SendEvent(void* object, void* event) {
using Fn = void(__fastcall*)(void*, void*, void*);
reinterpret_cast<Fn>((*reinterpret_cast<void***>(object))[0x18 / 4])(object, nullptr, event);
}
} // namespace game
+29
View File
@@ -1,10 +1,31 @@
#pragma once
// Game objects, RTTI casts and the event kernel: calls shared by the mods.
#include <cstddef>
#include <cstdint>
namespace game {
// Object ids 1..0x10 are the player heroes.
constexpr uint32_t kPlayerHeroIdEnd = 0x11;
inline bool IsPlayerHeroId(uint32_t id) {
return id != 0 && id < kPlayerHeroIdEnd;
}
// cObject +0x14: flags; dead objects (bit 0x10) are dropped by the object
// manager (saving marks effect objects dead, cObjectManager::Place refuses them).
constexpr size_t kObjectFlags = 0x14;
constexpr uint32_t kObjectDead = 0x10;
// The creatures near the player (g_nearbyCreatures 0xad1c60: {object id, ?}
// x g_nearbyCreatureCount 0xad1b48), as the game uses them for aura targets.
inline int NearbyCreatureCount() {
return *reinterpret_cast<const int*>(0x00ad1b48);
}
inline uint32_t NearbyCreatureId(int index) {
return *reinterpret_cast<const uint32_t*>(0x00ad1c60 + index * 8);
}
// MSVC RTTI type descriptors (for DynamicCast).
constexpr uint32_t kTypeObject = 0x008e9648;
constexpr uint32_t kTypeCreature = 0x008e9660;
@@ -29,6 +50,14 @@ inline void* GetCreature(uint32_t id) {
return DynamicCast(GetObjectById(id), kTypeObject, kTypeCreature);
}
// cObjectManager::GetPlayerHero (0x603e90): the local player's hero (a
// cCreature), or null.
inline void* GetPlayerHero() {
using Fn = void*(__fastcall*)(void* manager, void* edx);
void* manager = *reinterpret_cast<void**>(0x00ad3bc0);
return manager ? reinterpret_cast<Fn>(0x00603e90)(manager, nullptr) : nullptr;
}
// cKernel::PostEvent (0x808930) on Kernel_Instance() (0x808490): the kernel
// copies the event and dispatches it (key events right away).
inline void PostEvent(void* event) {
+36
View File
@@ -0,0 +1,36 @@
#include "game/engine/orders.h"
#include <intrin.h>
#include <vector>
#include "re/hook.h"
namespace game {
namespace {
std::vector<CreatureOrderFn>& Callbacks() {
static std::vector<CreatureOrderFn> callbacks;
return callbacks;
}
} // namespace
void OnCreatureOrder(CreatureOrderFn callback) {
Callbacks().push_back(callback);
}
// cCreature::ReceiveOrder (0x52e400, vf18): every order a creature gets.
void __fastcall Creature_ReceiveOrder(void* self, void* edx, CreatureOrder* order);
RE_HOOK(0x0052e400, Creature_ReceiveOrder);
void __fastcall Creature_ReceiveOrder(void* self, void* edx, CreatureOrder* order) {
if (self && order) {
void* caller = _ReturnAddress();
for (CreatureOrderFn callback : Callbacks()) {
callback(self, *order, caller);
}
}
Creature_ReceiveOrder_original(self, edx, order);
}
} // namespace game
+99
View File
@@ -0,0 +1,99 @@
#pragma once
// Creature orders (cEvent_creature) and the engine fields around them
// (docs/wasd.md, docs/auto-attack.md).
#include <cstddef>
#include <cstdint>
#include "game/engine/objects.h"
namespace game {
constexpr uint32_t kEventCreatureVtable = 0x0088e95c;
// cEngine fields.
constexpr size_t kEngineFlags = 0x54; // 0x10 mouse button held (hold-to-move), 0x4000 input blocked
constexpr size_t kEngineControlledId = 0x13c; // object id of the controlled creature
// Order commands.
constexpr uint32_t kOrderWalk = 4;
constexpr uint32_t kOrderAttack = 7; // left click on a creature: one attack
constexpr uint32_t kOrderUseArt = 0x105;
// cEvent_creature (0x44 bytes). The meaning of +0x10..+0x20 depends on the
// command:
// walk (4): +0x14 2 = ground point at +0x18 / +0x1c (0: stop),
// +0x20 1 = re-aim while a mouse button is held
// attack (7): +0x18 target object id, +0x1c / +0x20 its position
// use art (105): +0x10 target object id (0: ground / self), +0x18 / +0x1c
// ground point, +0x30 art id
// hit (5): +8 sender, +0xc / +0x10 target, +0x1c attacker id (the
// damage code credits it: life leech, ...; aura hits leave
// it 0), +0x20 four u16 damage slots (physical, ...),
// +0x30 float damage factor
struct CreatureOrder {
uint32_t vtable = kEventCreatureVtable;
uint32_t command = 0;
uint32_t unknown08[2] = {};
uint32_t target = 0; // +0x10
uint32_t groundTarget = 0; // +0x14
int32_t a = 0; // +0x18
int32_t b = 0; // +0x1c
int32_t c = 0; // +0x20
uint32_t unknown24[3] = {};
uint32_t art = 0; // +0x30
uint32_t unknown34[4] = {};
};
static_assert(sizeof(CreatureOrder) == 0x44);
static_assert(offsetof(CreatureOrder, art) == 0x30);
// cEngine::SendCreatureOrder (0x6170d0): snaps ground targets, then
// creature->vf18 (cCreature::ReceiveOrder 0x52e400).
inline void SendCreatureOrder(uint8_t* engine, void* creature, CreatureOrder& order) {
using Fn = void(__fastcall*)(void* engine, void* edx, void* creature, void* event);
reinterpret_cast<Fn>(0x006170d0)(engine, nullptr, creature, &order);
}
inline void* ControlledCreature(uint8_t* engine) {
return GetCreature(*reinterpret_cast<uint32_t*>(engine + kEngineControlledId));
}
// Called before cCreature::ReceiveOrder (0x52e400) handles an order (any
// creature; `caller` is the code that sent it). Register at startup.
using CreatureOrderFn = void (*)(void* creature, const CreatureOrder& order, void* caller);
void OnCreatureOrder(CreatureOrderFn callback);
// Creature fields.
constexpr size_t kCreatureId = 0xc;
constexpr size_t kCreatureX = 0x1c;
constexpr size_t kCreatureY = 0x20;
// +0x150: low word the current action (0 none, 1 art, 3 attack, 8 ...), high
// word its phase; 0x10000 when standing idle.
constexpr size_t kCreatureAction = 0x150;
// cCreature::IsAlive (0x548ef0): not dying (+0x150 == 6), not dead (+0xfc ==
// 9), health (+0x4d8) left.
inline bool IsAlive(void* creature) {
using Fn = char(__fastcall*)(void*, void*);
return reinterpret_cast<Fn>(0x00548ef0)(creature, nullptr) != 0;
}
// cCreature::IsHostileTo (0x548dd0, (creature)): `other` is alive and its
// faction is hostile to this creature's (0x4226f0 / 0x423450). NPCs such as
// traders are not hostile (and not allies either).
inline bool IsHostileTo(void* creature, void* other) {
using Fn = char(__fastcall*)(void*, void*, void*);
return other && reinterpret_cast<Fn>(0x00548dd0)(creature, nullptr, other) != 0;
}
// cCreature::DistanceTo (0x536f20, (object id)).
inline uint32_t DistanceTo(void* creature, uint32_t id) {
using Fn = uint32_t(__fastcall*)(void*, void*, uint32_t);
return reinterpret_cast<Fn>(0x00536f20)(creature, nullptr, id);
}
inline int32_t CreatureField(const void* creature, size_t offset) {
return *reinterpret_cast<const int32_t*>(static_cast<const uint8_t*>(creature) + offset);
}
} // namespace game
+66
View File
@@ -0,0 +1,66 @@
#pragma once
// UI controls' screen rects and text drawing with the game's fonts.
#include <cstdint>
namespace game {
// Screen rect as the UI uses it (cUI_Control2::GetScreenRect 0x731c70,
// constructed by 0x6a55f0).
struct UiRect {
int32_t x;
int32_t y;
int16_t w;
int16_t h;
};
static_assert(sizeof(UiRect) == 0xc);
inline UiRect ControlScreenRect(void* control) {
using Fn = UiRect*(__fastcall*)(void*, void*, UiRect*);
UiRect rect = {};
reinterpret_cast<Fn>(0x00731c70)(control, nullptr, &rect);
return rect;
}
// Text flags of the fonts' draw method (cFontTTF2::vf18 0x64eb20; the text
// starts at the top of the rect, there is no vertical alignment).
constexpr uint32_t kTextCenter = 1; // horizontally
constexpr uint32_t kTextRight = 4;
// FontManager::Instance (0x649d10) -> GetFont (0x649de0): font `font`, or null.
inline void* GetFont(uint16_t font) {
using InstanceFn = void*(__cdecl*)();
using GetFontFn = void*(__fastcall*)(void*, void*, uint32_t);
void* fonts = reinterpret_cast<InstanceFn>(0x00649d10)();
return fonts ? reinterpret_cast<GetFontFn>(0x00649de0)(fonts, nullptr, font) : nullptr;
}
// The height of a line of font `font`, 0 if unknown.
// TODO(measure): cFontTTF2 vf28 (0x6518e0) returns +0x200c6, which may be it;
// +0x200ca (set by vf44) looked like 0. Logged by the diagnostics.
inline int FontLineHeight(uint16_t font) {
auto* f = static_cast<uint8_t*>(GetFont(font));
return f ? *reinterpret_cast<const uint16_t*>(f + 0x200c6) : 0;
}
// `rect` with its top moved so one line of font `font` sits in its middle.
inline UiRect CenterLineVertically(uint16_t font, UiRect rect) {
rect.y += (rect.h - FontLineHeight(font)) / 2;
return rect;
}
// Draws `text` into `rect` with font `font` (font vf18 (renderer, rect, text,
// ARGB colour, flags), as cUI_StaticText64::vf14 does). `renderer` is the
// argument of the UI's render methods (vf14).
inline void DrawText(void* renderer, uint16_t font, UiRect rect, const wchar_t* text, uint32_t color,
uint32_t flags) {
void* f = GetFont(font);
if (!f) {
return;
}
using DrawFn = void(__fastcall*)(void*, void*, void* renderer, UiRect*, const wchar_t*, uint32_t, uint32_t);
auto draw = reinterpret_cast<DrawFn>((*reinterpret_cast<void***>(f))[0x18 / 4]);
draw(f, nullptr, renderer, &rect, text, color, flags);
}
} // namespace game