The keys rewrite the hero's path every cCreature::Update: one segment along the held direction, the facing set to it, sliding along blocked cells. Starting from idle runs the game's own Think steps at once (StartPendingWalk, WalkAction) instead of waiting up to 110 ms, so weapon switches no longer stop the walk. Stops wait 50 ms after the keys are released, and a stop in progress is taken back when keys are held again. Names for movement, stats and text-key functions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
230 lines
9.9 KiB
C++
230 lines
9.9 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);
|
|
|
|
// Creature fields.
|
|
constexpr size_t kCreatureId = 0xc;
|
|
constexpr size_t kCreatureX = 0x1c;
|
|
constexpr size_t kCreatureY = 0x20;
|
|
|
|
// 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
|