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>
8.8 KiB
WASD movement (src/mods/wasd.cpp)
How the game moves the hero
Movement is a creature order: cEvent_creature (vtable 0x88e95c, 0x44
bytes), command 4 = walk, sent with cEngine::SendCreatureOrder (0x6170d0,
(creature, event)), which snaps a ground target to a walkable point and
passes the order to creature->vf18 (cCreature::ReceiveOrder, 0x52e400).
| Offset | Field |
|---|---|
| +4 | command (4 walk, 0x105 ...) |
| +0x10 | target object id (attacks) |
| +0x14 | 2: x, y are a ground point; 0: no target |
| +0x18 / +0x1c | ground point (world units, same as creature +0x1c / +0x20) |
| +0x20 | 1: re-aim while a mouse button is held; 2: skip target snapping |
The controlled creature's object id is at cEngine+0x13c; objects come from
cObjectManager::GetObject (0x5fe060, manager *0xad3bc0), cast with
__RTDynamicCast to cCreature. Hero state: +0xfc action mode (2 while
following a held mouse button), +0x150 movement (1 walking, 2 arriving,
0 standing).
Mouse, measured with an order log:
- click (
cEngine::HandleMouseButton0x617360): one walk order to the ground point, then engine flag+0x54 |= 0x10(button held); - while held: every mouse event re-sends a plain walk order (+0x20 = 0) to the
point under the cursor, dozens per tick; the hero stays in mode 2 and walks
smoothly.
cEngine::Tick(0x60f1d0, 10 Hz) additionally sends a re-aim order (+0x20 = 1) per tick after 0.4 s of walking; - release after > 0.5 s: a walk order without target (+0x14 = 0), which stops the hero in place.
What the mod does
Direction movement (2026-10-18)
Request: "lets go all in with directional vector movement". The keys steer the hero directly instead of sending walk orders 10 times per second, which 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 fromcCreature::Update(0x594110, thiscall (u8), ret 4) at0x59653a, about 40 times per second on the engine thread. Updatesteps 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 at0x5953daand0x5955f7). It then passes throughcCreature::CanStepTo(0x593c50, thiscall (x, y, position*), ret 0xc), which refuses a step into another cell thatcCreature::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) andcCreature::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
Keys reach the game as cEvent_key (category 4; key at +6/+8, modifiers at
+0xa) through the kernel queue to cEngine::ReceiveEvent (0x618130), which
offers them to cUI_Manager::ReceiveEvent (0x757120) first. Both compare
toupper(key) (0x84aa86). Presses come from WM_CHAR (the character), releases
from WM_KEYUP (the virtual key), so keys without a character (F-keys) never
reach the hotkeys. The key bindings (src/mods/input.cpp) handle the raw
WM_KEYDOWN/WM_KEYUP instead: W/A/S/D and the digit row are kept from the
game, bound keys post the original hotkey's key event (Kernel_Instance
0x808490, cKernel::PostEvent 0x808930); the temporary bindings for the
old W/A/S actions are listed in docs/todo.md.
Hotkeys found: engine A (pick up nearby), P (pause), Q/W/E/R/B/Space
(cEngine::UseActionSlot), ^ (console), Esc, Ctrl+B/F/X/Z (Z quicksave); UI
manager C F H I L M N O S T (S save game window). Arrow keys in
cEngine::ScrollInput (0x610650) walk object #2 or scroll the camera; not
reachable in normal play.
Text fields (2026-10-18)
Request: in character creation and the save window, bound keys were ignored or typed something else (K -> s: K posts the game's S hotkey).
Measured (key-press log): the UI manager's dialog slot (+0x144) held a hidden
cUI_BusyDlg in both cases, so the old "dialog shown + focused edit control"
test never found the field. The focused control (+0x148) is the right
cUI_EditControl, but neither the focus nor the control's flags (0x8005)
change when its window closes, and the control has no parent at +0x50. The
save window draws two edit controls per frame.
Now (src/mods/input.cpp, FocusedTextField): keys pass to the game while the
focused control is a writable edit control that cUI_EditControl::Render
(0x737ae0, thiscall (renderer), ret 4, returns at once while hidden) drew in
the last 250 ms. The render hook only notes the focused control.
Weapon set switch while walking (2026-10-18)
Request: the hero stops for a moment when switching weapon sets (F-keys) while walking with WASD.
Measured (state per tick + orders): cUI_Taskbar2::SelectWeaponSet
(0x6e1440, ret 8) sends order 0x114 (set, items) and 0x107; between melee
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
animation state (0x54e540(1, 0)). The next WASD tick restarts the walk.
Measured again with a watchpoint on the action word: on a weapon class
change cCreature::ResetToIdle (0x54e540) plays the idle animation and
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.