WASD: resume the walk right after a weapon set switch (untested in-game)

Switching between weapon classes resets the hero's animation state inside
cUI_Taskbar2::SelectWeaponSet, which dropped the walk for a tick. The hook
sends the WASD walk again at once. Checkpoint before the movement rework.

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 01:41:13 +02:00
1 parent 184c7058fd
commit d296e2850b
4 files changed
+79 -12

No files matched your search

+15
View File
@@ -79,3 +79,18 @@ 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.
Now: `SelectWeaponSet` is hooked (`skillbar.cpp`) and calls
`mods::wasd::ResumeWalk`, which sends the walk for the held keys at once.
Not yet confirmed in-game.
+13
View File
@@ -36,6 +36,7 @@
#include "game/engine/objects.h"
#include "game/ui/text.h"
#include "mods/skillslots.h"
#include "mods/wasd.h"
#include "re/hook.h"
#include "re/log.h"
#include "re/patch.h"
@@ -307,6 +308,18 @@ 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.
void __fastcall UiTaskbar_RenderArtSlots(void* self, void* edx, void* renderer);
RE_HOOK(0x006e31e0, UiTaskbar_RenderArtSlots);
+47 -12
View File
@@ -15,6 +15,7 @@
#include "mods/wasd.h"
#include <atomic>
#include <cmath>
#include <cstdint>
@@ -69,19 +70,39 @@ bool IsMovementKey(int vk) {
return (vk == 'W' || vk == 'A' || vk == 'S' || vk == 'D');
}
// Called once per engine tick (10 Hz), after the game's own input handling.
void Update(uint8_t* engine) {
// World axes on screen: (-1,-1) up, (1,1) down, (-1,1) left, (1,-1) right.
int dx = 0;
int dy = 0;
namespace {
// The held movement keys as a screen direction in world axes: (-1,-1) up,
// (1,1) down, (-1,1) left, (1,-1) right; 0,0 when none (or the keys belong to
// the mouse, blocked input or a text field).
void HeldDirection(uint8_t* engine, int& dx, int& dy) {
dx = 0;
dy = 0;
const uint32_t flags = *reinterpret_cast<uint32_t*>(engine + kEngineFlags);
const bool mouseMoving = (flags & 0x10) != 0;
if (!mouseMoving && (flags & 0x4000) == 0 && !mods::input::TypingIntoTextField()) {
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; }
}
}
void Walk(uint8_t* engine, void* creature, int dx, int dy) {
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)) {
@@ -95,16 +116,30 @@ void Update(uint8_t* engine) {
if (!creature) {
return;
}
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);
Walk(engine, creature, dx, dy);
g_move.moving = true;
}
} // namespace
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); }
Registration() {
game::OnEngineTick(&Update);
}
} g_registration;
} // namespace
+4
View File
@@ -5,4 +5,8 @@ namespace mods::wasd {
// Whether `vk` is one of the movement keys W/A/S/D.
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