// WASD movement for the player character: the keys steer the hero directly // (request of 2026-10-18: direction movement instead of walk orders). // // The game walks a creature along a path of waypoints in cCreature::Update // (0x594110, about 40 times per second; path layout in game/engine/orders.h). // While W, A, S or D is held, each update of the controlled creature first // replaces its path with one straight segment from where it stands, far into // the pressed screen direction: the game's own step then moves it at its own // speed, with its walk animation, and its step test refuses blocked cells. No // pathfinding, no arrival, and a new direction counts from the next update. // // 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. // // The movement keys are key bindings (default W/A/S/D, src/mods/input.cpp, // which keeps them away from the game; the game's W/A/S actions moved to // other keys). Typing into a text field (chat, console, save names) is left // alone. See docs/wasd.md. #include #include #include #include #include "game/engine/events.h" #include "game/engine/objects.h" #include "game/engine/orders.h" #include "mods/input.h" #include "re/hook.h" namespace mods::wasd { namespace { using game::ControlledCreature; using game::CreatureOrder; using game::kEngineFlags; // The walk order that starts walking aims this far (world units). constexpr double kStartDistance = 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; using mods::input::Move; using mods::input::MovementKey; bool KeyDown(int key) { return key != 0 && (GetAsyncKeyState(key) & 0x8000) != 0; } void SendWalk(uint8_t* engine, void* creature, int x, int y) { CreatureOrder order; order.command = game::kOrderWalk; order.groundTarget = 2; order.a = x; order.b = y; game::SendCreatureOrder(engine, creature, order); } // A walk order without a target stops the creature where it is (what // releasing a held mouse button sends, cEngine::HandleMouseButton). void SendStop(uint8_t* engine, void* creature) { CreatureOrder order; order.command = game::kOrderWalk; game::SendCreatureOrder(engine, creature, order); } std::atomic g_engine{nullptr}; 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; }; // 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(engine + kEngineFlags); if ((flags & (kEngineMouseHeld | kEngineInputBlocked)) == 0 && !mods::input::TypingIntoTextField()) { if (KeyDown(MovementKey(Move::Up))) { x -= 1; y -= 1; } if (KeyDown(MovementKey(Move::Down))) { x += 1; y += 1; } if (KeyDown(MovementKey(Move::Left))) { x -= 1; y += 1; } if (KeyDown(MovementKey(Move::Right))) { x += 1; y -= 1; } } if (x == 0 && y == 0) { return false; } const double length = std::sqrt(static_cast(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(dir.x * d), y + static_cast(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(creature + game::kCreaturePath); path[0] = x; path[1] = y; path[2] = x + static_cast(dir.x * kSegmentLength); path[3] = y + static_cast(dir.y * kSegmentLength); *reinterpret_cast(creature + game::kCreaturePathIndex) = 0; *reinterpret_cast(creature + game::kCreaturePathCount) = 2; auto* facing = reinterpret_cast(creature + game::kCreatureFacing); facing[0] = static_cast(dir.x); facing[1] = static_cast(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(dir.x * kStartDistance), game::CreatureField(creature, game::kCreatureY) + static_cast(dir.y * kStartDistance)); if (*reinterpret_cast(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 // Before each update of a creature: steer the controlled one. void BeforeCreatureUpdate(uint8_t* creature) { uint8_t* engine = g_engine.load(); if (!engine || creature != ControlledCreature(engine)) { return; } 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) { if ((*reinterpret_cast(engine + kEngineFlags) & kEngineMouseHeld) == 0) { SendStop(engine, creature); } g_moving = false; } return; } g_moving = true; 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(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; } const DWORD now = GetTickCount(); if (open && now - g_startOrderAt >= kStartOrderMs) { g_startOrderAt = now; if (StartWalk(engine, creature, dir)) { g_startOrderAt = 0; Steer(creature, dir); } } } } // 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(self)); } Creature_Update_original(self, edx, arg); } } // namespace game