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 <noreply@anthropic.com>
289 lines
13 KiB
C++
289 lines
13 KiB
C++
#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);
|
|
|
|
// 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;
|
|
constexpr size_t kCreatureY = 0x20;
|
|
|
|
// Escorts (measured 2026-10-26, docs/quests.md): an NPC told to follow
|
|
// (cCreature::ReceiveOrder 0x52e400, e.g. 0x52e64f) stores its leader id at
|
|
// +0x251, sets bit 4 of +0x1f4 (escorts of quest scripts have 0x100 instead,
|
|
// measured on "Jagd auf die Wilderer"; the leave order clears both) and is added to the leader's follower list
|
|
// (+0x39c..+0x3a0, 9-byte entries {u32 id, u32, u8}) with AddFollower.
|
|
// Quest scripts test "@npc" (0x498aa0 case 0x40) against that list.
|
|
constexpr size_t kCreatureLeader = 0x251;
|
|
constexpr size_t kCreatureStatus = 0x1f4;
|
|
constexpr uint32_t kStatusFollowing = 0x104;
|
|
constexpr size_t kCreatureFollowersBegin = 0x39c;
|
|
constexpr size_t kCreatureFollowersEnd = 0x3a0;
|
|
constexpr size_t kFollowerEntrySize = 9;
|
|
|
|
#pragma pack(push, 1)
|
|
struct FollowerEntry {
|
|
uint32_t id;
|
|
uint32_t value; // 1 for escorts, as the follow order adds them
|
|
uint8_t flag; // 1 for escorts
|
|
};
|
|
#pragma pack(pop)
|
|
static_assert(sizeof(FollowerEntry) == kFollowerEntrySize);
|
|
|
|
// cCreature::AddFollower (0x54b070, thiscall (const FollowerEntry*), ret 4):
|
|
// adds the entry to the leader's list unless its id is in it.
|
|
inline void AddFollower(void* leader, const FollowerEntry& entry) {
|
|
using Fn = void(__fastcall*)(void*, void*, const FollowerEntry*);
|
|
reinterpret_cast<Fn>(0x0054b070)(leader, nullptr, &entry);
|
|
}
|
|
|
|
// The ids on the leader's follower list.
|
|
template <typename Fn>
|
|
void ForEachFollower(const void* leader, Fn&& fn) {
|
|
const auto* creature = static_cast<const uint8_t*>(leader);
|
|
const auto* begin = *reinterpret_cast<const uint8_t* const*>(creature + kCreatureFollowersBegin);
|
|
const auto* end = *reinterpret_cast<const uint8_t* const*>(creature + kCreatureFollowersEnd);
|
|
for (const uint8_t* entry = begin; entry && entry < end; entry += kFollowerEntrySize) {
|
|
fn(*reinterpret_cast<const uint32_t*>(entry));
|
|
}
|
|
}
|
|
|
|
inline bool HasFollower(const void* leader, uint32_t id) {
|
|
bool found = false;
|
|
ForEachFollower(leader, [&](uint32_t follower) { found = found || follower == id; });
|
|
return found;
|
|
}
|
|
|
|
// cCreature::Teleport (0x54d840, thiscall (cell x, cell y, 0, 1), ret 0x10):
|
|
// as the portals move the hero (EE_USEPORTAL 0x6197c3); the game's log line
|
|
// "Teleport (type): <type>, ... (x,y)".
|
|
inline void Teleport(void* creature, int32_t cellX, int32_t cellY) {
|
|
using Fn = void(__fastcall*)(void*, void*, int32_t, int32_t, int32_t, int32_t);
|
|
reinterpret_cast<Fn>(0x0054d840)(creature, nullptr, cellX, cellY, 0, 1);
|
|
}
|
|
|
|
// The walk path (measured 2026-10-18): waypoints {x, y} at +0x2ed + i * 8,
|
|
// the current segment's start index at +0x36d, the count at +0x371, and the
|
|
// step's sub-unit remainder (x, y, 1/1024 units) at +0x37d / +0x381.
|
|
// cCreature::Update (0x594110, thiscall (u8), ret 4; about 40 times per second
|
|
// on the engine thread) moves the creature its step length along the
|
|
// segments into g_creatureNewPosition (0xad2368, {u16, x, y, u8 level}),
|
|
// writes the reached point back as the segment start, and commits it with
|
|
// cObjectManager::Place (0x5fcd10), which refuses blocked spots (the
|
|
// creature then stays where it was). At the last waypoint the walk ends.
|
|
constexpr size_t kCreaturePath = 0x2ed;
|
|
constexpr size_t kPathPointSize = 8;
|
|
constexpr size_t kCreaturePathIndex = 0x36d;
|
|
constexpr size_t kCreaturePathCount = 0x371;
|
|
// Facing: a float unit vector in world axes ({0.707, -0.707} facing right on
|
|
// screen). Walking turns it towards the path gradually (measured 2026-10-18:
|
|
// about 300 ms for a reversal, swinging through the side).
|
|
constexpr size_t kCreatureFacing = 0x70;
|
|
|
|
// +0xfc: the pending action (u16, the order received but not yet started;
|
|
// 2 a walk), +0xfe its phase, +0x100..+0x13f its parameters (walk target at
|
|
// +0x104 / +0x108). cCreature::Think (vf04, 0x537430, thiscall (float),
|
|
// ret 4; about every 110 ms per creature) starts it and then runs the current
|
|
// action's handler.
|
|
constexpr size_t kCreaturePendingAction = 0xfc;
|
|
constexpr uint16_t kPendingWalk = 2;
|
|
|
|
// cCreature::StartPendingWalk (0x4ede70, thiscall (), ret): the Think step
|
|
// for a pending walk: unless a status effect (+0x1f4) holds the creature or
|
|
// the target is over 2000 units away, the pending parameters become the
|
|
// action's (+0x154), action 1, phase 0 unless already walking.
|
|
inline void StartPendingWalk(void* creature) {
|
|
using Fn = void(__fastcall*)(void*, void*);
|
|
reinterpret_cast<Fn>(0x004ede70)(creature, nullptr);
|
|
}
|
|
|
|
// cCreature::WalkAction (0x4f45b0, thiscall (), ret): the Think step of the
|
|
// walk action; phase 0 plans the path, sets the facing, starts the walk
|
|
// animation and goes to phase 1.
|
|
inline void WalkAction(void* creature) {
|
|
using Fn = void(__fastcall*)(void*, void*);
|
|
reinterpret_cast<Fn>(0x004f45b0)(creature, nullptr);
|
|
}
|
|
|
|
// +0x150: low word the current action, high word its phase; 0x10000 when
|
|
// standing idle. Measured on the hero: 1 walking (also up to a target),
|
|
// 2 stopping (2026-10-18: the target-less walk order sets the walk's phase
|
|
// to 0x64 at 0x52f03a, WalkAction then switches to 2 with the stop
|
|
// animation at 0x4f6f8b, idle about 110 ms later at 0x546513),
|
|
// 3 weapon attack, 4 casting a spell (2026-10-16), 6 dying, 8 an art seen
|
|
// after walking up (2026-10-15, presumably a combat move), 5 a shot with a
|
|
// projectile weapon (bow, 2026-10-18).
|
|
constexpr size_t kCreatureAction = 0x150;
|
|
enum CreatureAction : int32_t {
|
|
kActionIdle = 0,
|
|
kActionWalk = 1,
|
|
kActionStop = 2,
|
|
kActionAttack = 3,
|
|
kActionCastSpell = 4,
|
|
kActionShoot = 5,
|
|
kActionDying = 6,
|
|
kActionCombatMove = 8,
|
|
};
|
|
inline int32_t CurrentAction(const void* creature);
|
|
constexpr size_t kCreatureActionPhase = 0x152; // u16
|
|
constexpr uint16_t kPhaseStopping = 0x64; // walk: stop requested
|
|
|
|
// 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);
|
|
}
|
|
|
|
inline int32_t CurrentAction(const void* creature) {
|
|
return CreatureField(creature, kCreatureAction) & 0xffff;
|
|
}
|
|
|
|
// A map position as objects keep it (object +0x18): area, x, y, level. In
|
|
// world units, or in map cells (world / kMapCellSize, truncated; 0x6225c0
|
|
// converts an object's position).
|
|
struct MapPosition {
|
|
uint16_t area = 0;
|
|
int32_t x = 0;
|
|
int32_t y = 0;
|
|
uint8_t level = 0;
|
|
};
|
|
static_assert(offsetof(MapPosition, x) == 4 && offsetof(MapPosition, level) == 0xc);
|
|
constexpr size_t kObjectPosition = 0x18;
|
|
constexpr size_t kObjectLevel = 0x24;
|
|
constexpr float kMapCellSize = 53.66563f; // 0x88f318 / 0x890044
|
|
|
|
// The map cell of world point (x, y) on the creature's area and level.
|
|
inline MapPosition CellAt(const void* creature, int x, int y) {
|
|
const auto* c = static_cast<const uint8_t*>(creature);
|
|
MapPosition cell;
|
|
cell.area = *reinterpret_cast<const uint16_t*>(c + kObjectPosition);
|
|
cell.x = static_cast<int32_t>(x / kMapCellSize);
|
|
cell.y = static_cast<int32_t>(y / kMapCellSize);
|
|
cell.level = c[kObjectLevel];
|
|
return cell;
|
|
}
|
|
|
|
// cCreature::IsCellWalkable (0x4f7b20, thiscall (cell*), ret 4): whether the
|
|
// creature may enter map cell `cell` (map sector lookups and its walk area
|
|
// +0x2c; reads only). The step test cCreature::CanStepTo (0x593c50) refuses a
|
|
// step into another cell that is not walkable.
|
|
inline bool IsCellWalkable(void* creature, MapPosition& cell) {
|
|
using Fn = char(__fastcall*)(void*, void*, MapPosition*);
|
|
return reinterpret_cast<Fn>(0x004f7b20)(creature, nullptr, &cell) != 0;
|
|
}
|
|
|
|
// The distance at which two creatures touch (0x4dbaa0 on one, (the other)):
|
|
// the mean of their body radii by object type (heroes 110 with a weapon, 80
|
|
// without; monsters 70..480). A melee attacker swings from about there
|
|
// (measured: heroes started swings at 117..149).
|
|
inline int ContactDistance(void* creature, void* other) {
|
|
using Fn = int(__fastcall*)(void*, void*, void*);
|
|
return reinterpret_cast<Fn>(0x004dbaa0)(creature, nullptr, other);
|
|
}
|
|
|
|
// The creature's weapon class (0x546df0, (0)): from the weapons in its hands
|
|
// (+0x1d4 / +0x1d8, item type +0x1e), in the order of the motion sets:
|
|
// 0 bare hands, 1 one-handed, 2 two-handed, 3 two-handed axe, 4 staff,
|
|
// 5 dagger, 6 throwing, 7 bow, 8 two weapons, 9 / 10 blades, 11 whip,
|
|
// 12 crossbow, 13 pistol.
|
|
inline int WeaponClass(void* creature) {
|
|
using Fn = short(__fastcall*)(void*, void*, int);
|
|
return reinterpret_cast<Fn>(0x00546df0)(creature, nullptr, 0);
|
|
}
|
|
inline bool HasRangedWeapon(void* creature) {
|
|
const int c = WeaponClass(creature);
|
|
return c == 6 || c == 7 || c == 12 || c == 13;
|
|
}
|
|
|
|
// The nearest living creature hostile to `attacker` within `maxDistance` of
|
|
// `from` (a creature), other than `except` and other player heroes; one not
|
|
// in `avoid` if there is any. 0 if none.
|
|
uint32_t NearestHostile(void* attacker, void* from, double maxDistance, uint32_t except = 0,
|
|
const uint32_t* avoid = nullptr, size_t avoidCount = 0);
|
|
|
|
} // namespace game
|