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:
1 parent
f721d2a486
commit
1fd89524d3
41 files changed
+2347
-214
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user