WASD: direction movement steers the walk path directly

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>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-18 03:18:51 +02:00
1 parent d296e2850b
commit 84c50d8c33
6 files changed
+421 -103

No files matched your search

+87 -11
View File
@@ -34,15 +34,87 @@ Mouse, measured with an order log:
## What the mod does ## What the mod does
Every `cEngine::Tick`, while W/A/S/D are held (and no text field has the ### Direction movement (2026-10-18)
focus, no mouse button is held): a plain walk order to hero position +
direction x 300 world units (about a cursor's distance). Screen directions in
world axes: up (-1,-1), down (1,1), left (-1,1), right (1,-1); combinations are
summed and normalised. All keys released: the target-less stop order.
Dead ends, for the record: re-aim orders (+0x20 = 1) on their own stop the Request: "lets go all in with directional vector movement". The keys steer
hero after two orders; far targets (2000 units) are rejected or followed the hero directly instead of sending walk orders 10 times per second, which
badly; targets near the stepwise-updated position make it arrive and stop. stuttered and stopped on weapon switches.
Measured (`Place` hook, watchpoints on the hero's x and on
`g_creatureNewPosition`):
- The only writer of a creature's position is `cObjectManager::Place`
(`0x5fcd10`, thiscall (id, position*), ret 8). For walking it is called from
`cCreature::Update` (`0x594110`, thiscall (u8), ret 4) at `0x59653a`, about
40 times per second on the engine thread.
- `Update` steps the creature along its path:
- waypoints {x, y} at +0x2ed + i * 8, segment index +0x36d, count +0x371,
sub-unit remainder +0x37d / +0x381;
- speed +0x375 (337 for the battlemage) times the elapsed time.
- The step goes into `g_creatureNewPosition` (`0xad2368`, {u16 area, x, y,
u8 level}; written at `0x5953da` and `0x5955f7`). It then passes through
`cCreature::CanStepTo` (`0x593c50`, thiscall (x, y, position*), ret 0xc),
which refuses a step into another cell that `cCreature::IsCellWalkable`
(`0x4f7b20`, thiscall (cell*), ret 4, read-only) rejects. On refusal the new
position is reset to the current one (`0x59631a`). Cells are 53.66563 world
units.
- Walking straight into a fence, 27 of 28 steps were refused: the hero ran on
the spot at 2 units per update.
Now (`src/mods/wasd.cpp`, hook on `cCreature::Update` for the controlled
creature, before the original):
- Held keys give a unit direction in world axes: up (-1,-1), down (1,1),
left (-1,1), right (1,-1); combinations are summed and normalised.
- Walking: the path is replaced by one segment from the hero's position,
1000 units along the direction (index 0, count 2). The game's own step moves
the hero at its own speed and animation, and it never arrives.
- Obstacles: the way 1.25 cells ahead is probed at quarter cells with
`IsCellWalkable`. If it is blocked, the direction turned by 30, 60 and 90
degrees is tried, the side last slid to first, so the hero slides along
walls. If all are blocked: stop order.
- The facing (+0x70 / +0x74, float unit vector in world axes) is set to the
direction each update. The game turns it towards the path gradually; on a
reversal that took about 300 ms and swung through the side, so the hero
walked backwards for a moment (user report: "direction change east/west is
not instant").
- Idle (start, after an attack, cast or weapon switch): a walk order 300
units ahead, and right after it the two steps `cCreature::Think` (vf04,
`0x537430`, thiscall (float), ret 4) would take on its next run:
`cCreature::StartPendingWalk` (`0x4ede70`: pending action +0xfc 2 with the
parameters at +0x100 becomes action 1 with +0x154) and
`cCreature::WalkAction` (`0x4f45b0`, phase 0 → 1: path, walk animation).
Measured: Think runs only about every 110 ms per creature, so the walk
started 16..125 ms after the order. Now it starts in the same update. If a
status effect holds the hero, the order is retried at most every 150 ms.
- Other actions (attack, cast, shot): left alone until they end.
- Rapid key changes (user report: "not 100% smooth when rapidly changing
directions"). Measured: a moment without any key (30 ms in the test)
sent the stop order. The hero then went through the game's stop: walk
phase 0x64, action 2 with the stop animation, idle about 110 ms later,
and only then started again (about 125 ms standing per change). Now:
- the stop order waits until the keys have been released for 50 ms;
- a stop requested but not begun (walk, phase 0x64) goes back to phase 1
when keys are held again;
- a running stop (action 2) restarts like standing idle.
Mouse walking has the same uneven steps (4/6 units per update) and paired
updates, so those are the game's own rhythm.
- Keys released: the target-less walk order (stops in place), unless a mouse
button is held.
Autotest (battlemage, each key 1.5 s): about 250 units per second and almost
no refused steps. The hero faces the key direction and plays the walk
animation.
Superseded: the walk order per `cEngine::Tick` (10 Hz). Dead ends, for the
record:
- re-aim orders (+0x20 = 1) on their own stop the hero after two orders;
- far targets (2000 units) are rejected or followed badly;
- targets near the stepwise-updated position make the hero arrive and stop;
- setting the walk mode +0xfc to 2 (as while a mouse button is held) does
not change the speed.
## Hotkeys ## Hotkeys
@@ -91,6 +163,10 @@ sets the walk goes on, between a melee and a bow set the hero drops to idle
for one tick: the weapon class changed, and the switch resets the hero's for one tick: the weapon class changed, and the switch resets the hero's
animation state (`0x54e540(1, 0)`). The next WASD tick restarts the walk. animation state (`0x54e540(1, 0)`). The next WASD tick restarts the walk.
Now: `SelectWeaponSet` is hooked (`skillbar.cpp`) and calls Measured again with a watchpoint on the action word: on a weapon class
`mods::wasd::ResumeWalk`, which sends the walk for the held keys at once. change `cCreature::ResetToIdle` (`0x54e540`) plays the idle animation and
Not yet confirmed in-game. sets action 0, phase 1, pending action 0. A user report ("still little
stops, weapon to no weapon and back") came after the restart through a walk
order, which waited for the next Think (up to 110 ms). Now direction movement
starts the walk in the same update (see above). The `SelectWeaponSet` hook
with `ResumeWalk` was removed.
+84
View File
@@ -67,8 +67,55 @@ void OnCreatureOrder(CreatureOrderFn callback);
constexpr size_t kCreatureId = 0xc; constexpr size_t kCreatureId = 0xc;
constexpr size_t kCreatureX = 0x1c; constexpr size_t kCreatureX = 0x1c;
constexpr size_t kCreatureY = 0x20; 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 // +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), // 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 // 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 // after walking up (2026-10-15, presumably a combat move), 5 a shot with a
// projectile weapon (bow, 2026-10-18). // projectile weapon (bow, 2026-10-18).
@@ -76,6 +123,7 @@ constexpr size_t kCreatureAction = 0x150;
enum CreatureAction : int32_t { enum CreatureAction : int32_t {
kActionIdle = 0, kActionIdle = 0,
kActionWalk = 1, kActionWalk = 1,
kActionStop = 2,
kActionAttack = 3, kActionAttack = 3,
kActionCastSpell = 4, kActionCastSpell = 4,
kActionShoot = 5, kActionShoot = 5,
@@ -83,6 +131,8 @@ enum CreatureAction : int32_t {
kActionCombatMove = 8, kActionCombatMove = 8,
}; };
inline int32_t CurrentAction(const void* creature); 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 == // cCreature::IsAlive (0x548ef0): not dying (+0x150 == 6), not dead (+0xfc ==
// 9), health (+0x4d8) left. // 9), health (+0x4d8) left.
@@ -113,6 +163,40 @@ inline int32_t CurrentAction(const void* creature) {
return CreatureField(creature, kCreatureAction) & 0xffff; 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 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 // 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 // without; monsters 70..480). A melee attacker swings from about there
-13
View File
@@ -36,7 +36,6 @@
#include "game/engine/objects.h" #include "game/engine/objects.h"
#include "game/ui/text.h" #include "game/ui/text.h"
#include "mods/skillslots.h" #include "mods/skillslots.h"
#include "mods/wasd.h"
#include "re/hook.h" #include "re/hook.h"
#include "re/log.h" #include "re/log.h"
#include "re/patch.h" #include "re/patch.h"
@@ -308,18 +307,6 @@ void __fastcall UiTaskbar_SelectSkillSlot(void* self, void* edx, uint32_t slot,
} }
} }
// cUI_Taskbar2::SelectWeaponSet (0x6e1440, ret 8): sends the hero the
// weapon set order (0x114) and, when the weapon class changed, resets its
// animation state (0x54e540), which drops a walk for a tick (measured
// 2026-10-18: action walk -> idle -> walk on switches between melee and bow
// sets, none between melee sets). A WASD walk goes on at once.
void __fastcall UiTaskbar_SelectWeaponSet(void* self, void* edx, uint32_t set, int notify);
RE_HOOK(0x006e1440, UiTaskbar_SelectWeaponSet);
void __fastcall UiTaskbar_SelectWeaponSet(void* self, void* edx, uint32_t set, int notify) {
UiTaskbar_SelectWeaponSet_original(self, edx, set, notify);
mods::wasd::ResumeWalk();
}
// cUI_Taskbar2::RenderArtSlots (0x6e31e0): then the cooldown labels. // cUI_Taskbar2::RenderArtSlots (0x6e31e0): then the cooldown labels.
void __fastcall UiTaskbar_RenderArtSlots(void* self, void* edx, void* renderer); void __fastcall UiTaskbar_RenderArtSlots(void* self, void* edx, void* renderer);
RE_HOOK(0x006e31e0, UiTaskbar_RenderArtSlots); RE_HOOK(0x006e31e0, UiTaskbar_RenderArtSlots);
+216 -75
View File
@@ -1,13 +1,29 @@
// WASD movement for the player character. // WASD movement for the player character: the keys steer the hero directly
// (request of 2026-10-18: direction movement instead of walk orders).
// //
// While W, A, S or D is held, the controlled creature gets a plain walk order // The game walks a creature along a path of waypoints in cCreature::Update
// (cEvent_creature 4 to a ground point) every engine tick, aimed a cursor's // (0x594110, about 40 times per second; path layout in game/engine/orders.h).
// distance from it in the pressed screen direction. That is what the mouse // While W, A, S or D is held, each update of the controlled creature first
// does while its button is held: every mouse event re-sends a walk to the point // replaces its path with one straight segment from where it stands, far into
// under the cursor (cEngine::HandleMouseButton), which keeps the creature // the pressed screen direction: the game's own step then moves it at its own
// walking smoothly. (The "held" re-aim orders cEngine::Tick sends as well stop // speed, with its walk animation, and its step test refuses blocked cells. No
// it when sent on their own.) Releasing the keys sends the target-less walk // pathfinding, no arrival, and a new direction counts from the next update.
// order a released mouse button sends, which stops it in place. //
// Obstacles: the cell one step ahead is probed with cCreature::IsCellWalkable
// (measured: walking straight into a fence, 27 of 28 steps were refused).
// When it is blocked, the direction turned by up to 90 degrees is tried,
// the side the hero last slid to first, so it slides along walls instead of
// running on the spot; with everything blocked it stands.
//
// The facing is set to the direction too: the game turns it towards the path
// over about 300 ms, which made reversals look like walking backwards.
//
// The path only drives a creature that walks. Standing idle (at the start, or
// after an attack, a cast, a weapon switch, which resets the hero to idle) it
// gets a walk order, and the two steps of cCreature::Think that would start it
// up to 110 ms later run at once: the walk goes on in the same update.
// Releasing the keys sends the target-less walk order a released mouse button
// sends, which stops it in place.
// //
// W, A and S are game hotkeys; the key bindings (src/mods/input.cpp) keep // W, A and S are game hotkeys; the key bindings (src/mods/input.cpp) keep
// W/A/S/D away from the game and move those actions to other keys. Typing into // W/A/S/D away from the game and move those actions to other keys. Typing into
@@ -34,9 +50,25 @@ using game::ControlledCreature;
using game::CreatureOrder; using game::CreatureOrder;
using game::kEngineFlags; using game::kEngineFlags;
// Walk orders aim this far (world units) from the creature, about where the // The walk order that starts walking aims this far (world units).
// cursor would be. constexpr double kStartDistance = 300.0;
constexpr double kTargetDistance = 300.0; // Between two start orders while the walk has not begun yet.
constexpr DWORD kStartOrderMs = 150;
// Keys released this long before the hero stops: changing keys often leaves
// a moment without any (measured: a stop order there cost about 125 ms
// standing).
constexpr DWORD kStopGraceMs = 50;
// The steered segment's length: farther than any step, so it never arrives.
constexpr double kSegmentLength = 1000.0;
// How far ahead a direction is probed: a bit more than one map cell.
constexpr double kProbeDistance = game::kMapCellSize * 1.25;
constexpr double kProbeStep = game::kMapCellSize * 0.25;
// Directions tried around a blocked one (degrees, both sides).
constexpr double kSlideAngles[] = {30.0, 60.0, 90.0};
// Engine flags (+0x54): a mouse button is held / input is blocked.
constexpr uint32_t kEngineMouseHeld = 0x10;
constexpr uint32_t kEngineInputBlocked = 0x4000;
bool KeyDown(int key) { bool KeyDown(int key) {
return (GetAsyncKeyState(key) & 0x8000) != 0; return (GetAsyncKeyState(key) & 0x8000) != 0;
@@ -59,10 +91,125 @@ void SendStop(uint8_t* engine, void* creature) {
game::SendCreatureOrder(engine, creature, order); game::SendCreatureOrder(engine, creature, order);
} }
struct MoveState { std::atomic<uint8_t*> g_engine{nullptr};
bool moving = false; bool g_moving = false;
DWORD g_releasedAt = 0; // when the keys were released (0: held)
DWORD g_startOrderAt = 0;
int g_slideSide = 1; // +1 / -1: the side the hero last turned to
struct Direction {
double x;
double y;
}; };
MoveState g_move;
// The held movement keys as a unit screen direction in world axes: (-1,-1)
// up, (1,1) down, (-1,1) left, (1,-1) right. False when none (or the keys
// belong to the mouse, blocked input or a text field).
bool HeldDirection(const uint8_t* engine, Direction& dir) {
int x = 0;
int y = 0;
const uint32_t flags = *reinterpret_cast<const uint32_t*>(engine + kEngineFlags);
if ((flags & (kEngineMouseHeld | kEngineInputBlocked)) == 0 && !mods::input::TypingIntoTextField()) {
if (KeyDown('W')) { x -= 1; y -= 1; }
if (KeyDown('S')) { x += 1; y += 1; }
if (KeyDown('A')) { x -= 1; y += 1; }
if (KeyDown('D')) { x += 1; y -= 1; }
}
if (x == 0 && y == 0) {
return false;
}
const double length = std::sqrt(static_cast<double>(x * x + y * y));
dir = {x / length, y / length};
return true;
}
Direction Rotated(Direction dir, double degrees) {
const double a = degrees * 3.14159265358979 / 180.0;
return {dir.x * std::cos(a) - dir.y * std::sin(a), dir.x * std::sin(a) + dir.y * std::cos(a)};
}
// Whether the creature can go a probe distance along `dir`: every cell the
// way crosses (sampled at quarter cells, so none is skipped) is walkable.
bool Open(uint8_t* creature, Direction dir) {
const int x = game::CreatureField(creature, game::kCreatureX);
const int y = game::CreatureField(creature, game::kCreatureY);
game::MapPosition last = game::CellAt(creature, x, y);
for (double d = kProbeStep; d <= kProbeDistance; d += kProbeStep) {
game::MapPosition cell =
game::CellAt(creature, x + static_cast<int>(dir.x * d), y + static_cast<int>(dir.y * d));
if (cell.x == last.x && cell.y == last.y) {
continue;
}
if (!game::IsCellWalkable(creature, cell)) {
return false;
}
last = cell;
}
return true;
}
// The direction to go for keys pointing along `dir`: itself, or the nearest
// open one within 90 degrees (the last slide side first). False if blocked.
bool OpenDirection(uint8_t* creature, Direction dir, Direction& out) {
if (Open(creature, dir)) {
out = dir;
return true;
}
const int sides[] = {g_slideSide, -g_slideSide};
for (const double angle : kSlideAngles) {
for (const int side : sides) {
const Direction turned = Rotated(dir, side * angle);
if (Open(creature, turned)) {
g_slideSide = side;
out = turned;
return true;
}
}
}
return false;
}
// One straight segment from the creature's position along `dir`.
void Steer(uint8_t* creature, Direction dir) {
const int x = game::CreatureField(creature, game::kCreatureX);
const int y = game::CreatureField(creature, game::kCreatureY);
auto* path = reinterpret_cast<int32_t*>(creature + game::kCreaturePath);
path[0] = x;
path[1] = y;
path[2] = x + static_cast<int>(dir.x * kSegmentLength);
path[3] = y + static_cast<int>(dir.y * kSegmentLength);
*reinterpret_cast<int32_t*>(creature + game::kCreaturePathIndex) = 0;
*reinterpret_cast<int32_t*>(creature + game::kCreaturePathCount) = 2;
auto* facing = reinterpret_cast<float*>(creature + game::kCreatureFacing);
facing[0] = static_cast<float>(dir.x);
facing[1] = static_cast<float>(dir.y);
}
// Starts walking along `dir` now: the walk order, then the two Think steps
// that would otherwise follow up to 110 ms later (start the pending walk, the
// walk action's first phase). True if the creature walks.
bool StartWalk(uint8_t* engine, uint8_t* creature, Direction dir) {
SendWalk(engine, creature, game::CreatureField(creature, game::kCreatureX) + static_cast<int>(dir.x * kStartDistance),
game::CreatureField(creature, game::kCreatureY) + static_cast<int>(dir.y * kStartDistance));
if (*reinterpret_cast<uint16_t*>(creature + game::kCreaturePendingAction) == game::kPendingWalk) {
game::StartPendingWalk(creature);
}
if (game::CurrentAction(creature) != game::kActionWalk) {
return false; // held by a status effect: Think decides later
}
if (game::CreatureField(creature, game::kCreatureAction) >> 16 == 0) {
game::WalkAction(creature);
}
return true;
}
void OnTick(uint8_t* engine) {
g_engine = engine;
}
struct Registration {
Registration() { game::OnEngineTick(&OnTick); }
} g_registration;
} // namespace } // namespace
@@ -70,78 +217,72 @@ bool IsMovementKey(int vk) {
return (vk == 'W' || vk == 'A' || vk == 'S' || vk == 'D'); return (vk == 'W' || vk == 'A' || vk == 'S' || vk == 'D');
} }
namespace { // Before each update of a creature: steer the controlled one.
void BeforeCreatureUpdate(uint8_t* creature) {
// The held movement keys as a screen direction in world axes: (-1,-1) up, uint8_t* engine = g_engine.load();
// (1,1) down, (-1,1) left, (1,-1) right; 0,0 when none (or the keys belong to if (!engine || creature != ControlledCreature(engine)) {
// the mouse, blocked input or a text field). return;
void HeldDirection(uint8_t* engine, int& dx, int& dy) {
dx = 0;
dy = 0;
const uint32_t flags = *reinterpret_cast<uint32_t*>(engine + kEngineFlags);
if ((flags & 0x10) == 0 && (flags & 0x4000) == 0 && !mods::input::TypingIntoTextField()) {
if (KeyDown('W')) { dx -= 1; dy -= 1; }
if (KeyDown('S')) { dx += 1; dy += 1; }
if (KeyDown('A')) { dx -= 1; dy += 1; }
if (KeyDown('D')) { dx += 1; dy -= 1; }
} }
Direction keys;
if (!HeldDirection(engine, keys)) {
const DWORD now = GetTickCount();
if (g_moving && !g_releasedAt) {
g_releasedAt = now;
} }
if (g_moving && now - g_releasedAt >= kStopGraceMs) {
void Walk(uint8_t* engine, void* creature, int dx, int dy) { if ((*reinterpret_cast<uint32_t*>(engine + kEngineFlags) & kEngineMouseHeld) == 0) {
const double length = std::sqrt(static_cast<double>(dx * dx + dy * dy));
const int x = game::CreatureField(creature, game::kCreatureX) + static_cast<int>(dx / length * kTargetDistance);
const int y = game::CreatureField(creature, game::kCreatureY) + static_cast<int>(dy / length * kTargetDistance);
SendWalk(engine, creature, x, y);
}
std::atomic<uint8_t*> g_engine{nullptr};
// Called once per engine tick (10 Hz), after the game's own input handling.
void Update(uint8_t* engine) {
g_engine = engine;
int dx;
int dy;
HeldDirection(engine, dx, dy);
const bool mouseMoving = (*reinterpret_cast<uint32_t*>(engine + kEngineFlags) & 0x10) != 0;
if (dx == 0 && dy == 0) {
if (g_move.moving && !mouseMoving) {
if (void* creature = ControlledCreature(engine)) {
SendStop(engine, creature); SendStop(engine, creature);
} }
g_moving = false;
} }
g_move.moving = false;
return; return;
} }
void* creature = ControlledCreature(engine); g_moving = true;
if (!creature) { g_releasedAt = 0;
int32_t action = game::CurrentAction(creature);
if (action == game::kActionStop) {
action = game::kActionIdle; // stopping: start again like from standing
}
if (action != game::kActionWalk && action != game::kActionIdle) {
return; // attacking, casting, dying: the action finishes first
}
Direction dir;
const bool open = OpenDirection(creature, keys, dir);
if (action == game::kActionWalk) {
auto* phase = reinterpret_cast<uint16_t*>(creature + game::kCreatureActionPhase);
if (open) {
if (*phase == game::kPhaseStopping) {
*phase = 1; // a stop requested but not begun: walk on
}
Steer(creature, dir);
} else if (*phase != game::kPhaseStopping) {
SendStop(engine, creature); // stand instead of running on the spot
}
return; return;
} }
Walk(engine, creature, dx, dy); const DWORD now = GetTickCount();
g_move.moving = true; if (open && now - g_startOrderAt >= kStartOrderMs) {
} g_startOrderAt = now;
if (StartWalk(engine, creature, dir)) {
} // namespace g_startOrderAt = 0;
Steer(creature, dir);
void ResumeWalk() {
uint8_t* engine = g_engine.load();
void* creature = engine && g_move.moving ? ControlledCreature(engine) : nullptr;
int dx;
int dy;
if (creature) {
HeldDirection(engine, dx, dy);
if (dx != 0 || dy != 0) {
Walk(engine, creature, dx, dy);
} }
} }
} }
namespace {
struct Registration {
Registration() {
game::OnEngineTick(&Update);
}
} g_registration;
} // namespace
} // namespace mods::wasd } // namespace mods::wasd
namespace game {
// cCreature::Update (0x594110, thiscall (u8), ret 4): moves the creature
// along its path, among others.
void __fastcall Creature_Update(void* self, void* edx, uint32_t arg);
RE_HOOK(0x00594110, Creature_Update);
void __fastcall Creature_Update(void* self, void* edx, uint32_t arg) {
if (self) {
mods::wasd::BeforeCreatureUpdate(static_cast<uint8_t*>(self));
}
Creature_Update_original(self, edx, arg);
}
} // namespace game
-4
View File
@@ -5,8 +5,4 @@ namespace mods::wasd {
// Whether `vk` is one of the movement keys W/A/S/D. // Whether `vk` is one of the movement keys W/A/S/D.
bool IsMovementKey(int vk); bool IsMovementKey(int vk);
// Sends the walk for the held movement keys now, after something stopped the
// hero mid-walk (a weapon set switch).
void ResumeWalk();
} // namespace mods::wasd } // namespace mods::wasd
+34
View File
@@ -258,3 +258,37 @@
0x005819a0 cCreatureArts::FindCombo # (id) -> index, 4 if none 0x005819a0 cCreatureArts::FindCombo # (id) -> index, 4 if none
0x00581410 cCreatureArts::ComboTooltip # (index, text, mode) 0x00581410 cCreatureArts::ComboTooltip # (index, text, mode)
0x004d2970 FindComboDef # (0x200..0x204) -> 0x30-byte entry at 0x953348 0x004d2970 FindComboDef # (0x200..0x204) -> 0x30-byte entry at 0x953348
# --- movement (WASD direction movement, 2026-10-18) ---------------------------
0x00594110 cCreature::Update # (u8), ret 4; ~40/s: walks the path (+0x2ed) into g_creatureNewPosition, Place
0x00593c50 cCreature::CanStepTo # (x, y, position*), ret 0xc; refuses an unwalkable other cell; side effects
0x004f7b20 cCreature::IsCellWalkable # (cell position*), ret 4; read-only
0x00593290 cCreature::BlendToAnimation # (anim, duration, speed, scale): Granny cross-fade
0x0054e540 cCreature::ResetToIdle # (notify, any level): idle animation, action 0 phase 1, pending 0 (weapon class change)
0x00622570 MapPosition_Init # {u16 area, x, y, u8 level}
0x00622590 MapPosition_Set # from (area, x, y, level)
0x006225c0 MapPosition_ToCell # world units -> cells (/ 53.66563)
0x00ad2368 g_creatureNewPosition # MapPosition the walk step computes before Place
0x0065f010 cTextureCache::Get # (id): loads on demand, touches the LRU
# --- UI edit control ---------------------------------------------------------
0x00737ae0 cUI_EditControl::Render # vf14, ret 4; returns at once while hidden
0x007372b0 cUI_EditControl::HandleEvent # vf10
0x00546df0 cCreature::WeaponClass # (0) -> motion set index
0x004dbaa0 cCreature::ContactDistance # (other): mean of the body radii
0x00537430 cCreature::Think # vf04 (float elapsed), ret 4; ~every 110 ms: pending action (+0xfc), then the action's handler
0x004ede70 cCreature::StartPendingWalk # (): pending walk (+0xfc 2) -> action 1, params +0x100 -> +0x154
0x004f45b0 cCreature::WalkAction # (): walk action handler; phase 0 plans the path, facing, animation
# --- statistics / survival bonus (2026-10-18) ----------------------------------
0x00425160 cStatsManager::Instance # cdecl (player id, ignored) -> record at 0xaa3518
0x00424930 cStatsManager::AddTime # (float s, alive): play time (+0x56f8 h, +0x56fc min, +0x5700 ms), survival +0x570c ms
0x004245a0 cStatsManager::OnHeroDeath # deaths +0x56f4 += 1, survival time = 0
0x004245c0 cStatsManager::Deaths
0x00424520 cStatsManager::SurvivalSeconds # +0x570c / 1000
0x00424540 cStatsManager::SetSurvivalSeconds # (s), ret 4
0x00424560 cStatsManager::CountSpawn # +0x5710 u16 (random spawns near the hero, 0x45ff50)
0x00424ce0 cStatsManager::Serialize # (stream, write): ends with magic 0xF00DF00D; survival time not included
0x00565c30 DiminishingCurve # (min, x, half, max): 0 below 1, else min + 2(max-min)(1 - 1/((x-1)/half + 1))
0x006046f0 ItemDataMgr::saveHero # 0x22c header + 0x40 block (+0 deaths, +8 survival s, +0xc spawns)
0x00605900 ItemDataMgr::loadHero
0x00672fd0 TextManager::GetTextByKey # (key string): id = hash 0x80e010
0x0080e010 TextKeyHash # h = (h*113 + toupper(c)) % 999999991, & 0x7fffffff