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` focused control is a writable edit control that `cUI_EditControl::Render`
(0x737ae0, thiscall (renderer), ret 4, returns at once while hidden) drew in (0x737ae0, thiscall (renderer), ret 4, returns at once while hidden) drew in
the last 250 ms. The render hook only notes the focused control. 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/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"
@@ -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. // 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);
+47 -12
View File
@@ -15,6 +15,7 @@
#include "mods/wasd.h" #include "mods/wasd.h"
#include <atomic>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
@@ -69,19 +70,39 @@ bool IsMovementKey(int vk) {
return (vk == 'W' || vk == 'A' || vk == 'S' || vk == 'D'); return (vk == 'W' || vk == 'A' || vk == 'S' || vk == 'D');
} }
// Called once per engine tick (10 Hz), after the game's own input handling. namespace {
void Update(uint8_t* engine) {
// World axes on screen: (-1,-1) up, (1,1) down, (-1,1) left, (1,-1) right. // The held movement keys as a screen direction in world axes: (-1,-1) up,
int dx = 0; // (1,1) down, (-1,1) left, (1,-1) right; 0,0 when none (or the keys belong to
int dy = 0; // 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 uint32_t flags = *reinterpret_cast<uint32_t*>(engine + kEngineFlags);
const bool mouseMoving = (flags & 0x10) != 0; if ((flags & 0x10) == 0 && (flags & 0x4000) == 0 && !mods::input::TypingIntoTextField()) {
if (!mouseMoving && (flags & 0x4000) == 0 && !mods::input::TypingIntoTextField()) {
if (KeyDown('W')) { dx -= 1; dy -= 1; } if (KeyDown('W')) { dx -= 1; dy -= 1; }
if (KeyDown('S')) { dx += 1; dy += 1; } if (KeyDown('S')) { dx += 1; dy += 1; }
if (KeyDown('A')) { dx -= 1; dy += 1; } if (KeyDown('A')) { dx -= 1; dy += 1; }
if (KeyDown('D')) { 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 (dx == 0 && dy == 0) {
if (g_move.moving && !mouseMoving) { if (g_move.moving && !mouseMoving) {
if (void* creature = ControlledCreature(engine)) { if (void* creature = ControlledCreature(engine)) {
@@ -95,16 +116,30 @@ void Update(uint8_t* engine) {
if (!creature) { if (!creature) {
return; return;
} }
const double length = std::sqrt(static_cast<double>(dx * dx + dy * dy)); Walk(engine, creature, dx, 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);
g_move.moving = true; 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 { namespace {
struct Registration { struct Registration {
Registration() { game::OnEngineTick(&Update); } Registration() {
game::OnEngineTick(&Update);
}
} g_registration; } g_registration;
} // namespace } // namespace
+4
View File
@@ -5,4 +5,8 @@ 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