diff --git a/docs/wasd.md b/docs/wasd.md index ba74dbd..a221ce8 100644 --- a/docs/wasd.md +++ b/docs/wasd.md @@ -34,15 +34,87 @@ Mouse, measured with an order log: ## What the mod does -Every `cEngine::Tick`, while W/A/S/D are held (and no text field has the -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. +### Direction movement (2026-10-18) -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 it arrive and stop. +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 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 @@ -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 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. +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. diff --git a/src/game/engine/orders.h b/src/game/engine/orders.h index 84a7888..4fe2b42 100644 --- a/src/game/engine/orders.h +++ b/src/game/engine/orders.h @@ -67,8 +67,55 @@ void OnCreatureOrder(CreatureOrderFn callback); constexpr size_t kCreatureId = 0xc; constexpr size_t kCreatureX = 0x1c; 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(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(0x004f45b0)(creature, nullptr); +} + // +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), +// 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 // after walking up (2026-10-15, presumably a combat move), 5 a shot with a // projectile weapon (bow, 2026-10-18). @@ -76,6 +123,7 @@ constexpr size_t kCreatureAction = 0x150; enum CreatureAction : int32_t { kActionIdle = 0, kActionWalk = 1, + kActionStop = 2, kActionAttack = 3, kActionCastSpell = 4, kActionShoot = 5, @@ -83,6 +131,8 @@ enum CreatureAction : int32_t { kActionCombatMove = 8, }; 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 == // 9), health (+0x4d8) left. @@ -113,6 +163,40 @@ inline int32_t CurrentAction(const void* creature) { 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(creature); + MapPosition cell; + cell.area = *reinterpret_cast(c + kObjectPosition); + cell.x = static_cast(x / kMapCellSize); + cell.y = static_cast(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(0x004f7b20)(creature, nullptr, &cell) != 0; +} + // 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 // without; monsters 70..480). A melee attacker swings from about there diff --git a/src/mods/skillbar.cpp b/src/mods/skillbar.cpp index 244625d..740ae6f 100644 --- a/src/mods/skillbar.cpp +++ b/src/mods/skillbar.cpp @@ -36,7 +36,6 @@ #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" @@ -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. void __fastcall UiTaskbar_RenderArtSlots(void* self, void* edx, void* renderer); RE_HOOK(0x006e31e0, UiTaskbar_RenderArtSlots); diff --git a/src/mods/wasd.cpp b/src/mods/wasd.cpp index bc1ccb0..f84872d 100644 --- a/src/mods/wasd.cpp +++ b/src/mods/wasd.cpp @@ -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 -// (cEvent_creature 4 to a ground point) every engine tick, aimed a cursor's -// distance from it in the pressed screen direction. That is what the mouse -// does while its button is held: every mouse event re-sends a walk to the point -// under the cursor (cEngine::HandleMouseButton), which keeps the creature -// walking smoothly. (The "held" re-aim orders cEngine::Tick sends as well stop -// it when sent on their own.) Releasing the keys sends the target-less walk -// order a released mouse button sends, which stops it in place. +// 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. // // 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 @@ -34,9 +50,25 @@ using game::ControlledCreature; using game::CreatureOrder; using game::kEngineFlags; -// Walk orders aim this far (world units) from the creature, about where the -// cursor would be. -constexpr double kTargetDistance = 300.0; +// 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; bool KeyDown(int key) { return (GetAsyncKeyState(key) & 0x8000) != 0; @@ -59,10 +91,125 @@ void SendStop(uint8_t* engine, void* creature) { game::SendCreatureOrder(engine, creature, order); } -struct MoveState { - bool moving = false; +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; }; -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(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(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 @@ -70,78 +217,72 @@ bool IsMovementKey(int vk) { return (vk == 'W' || vk == 'A' || vk == 'S' || vk == 'D'); } -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(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; } +// 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; } -} - -void Walk(uint8_t* engine, void* creature, int dx, int dy) { - const double length = std::sqrt(static_cast(dx * dx + dy * dy)); - const int x = game::CreatureField(creature, game::kCreatureX) + static_cast(dx / length * kTargetDistance); - const int y = game::CreatureField(creature, game::kCreatureY) + static_cast(dy / length * kTargetDistance); - SendWalk(engine, creature, x, y); -} - -std::atomic 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(engine + kEngineFlags) & 0x10) != 0; - if (dx == 0 && dy == 0) { - if (g_move.moving && !mouseMoving) { - if (void* creature = ControlledCreature(engine)) { + 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; } - g_move.moving = false; return; } - void* creature = ControlledCreature(engine); - if (!creature) { + 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; } - 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); + 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 { -struct Registration { - Registration() { - game::OnEngineTick(&Update); - } -} g_registration; -} // namespace - } // 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 diff --git a/src/mods/wasd.h b/src/mods/wasd.h index 6950aee..d663921 100644 --- a/src/mods/wasd.h +++ b/src/mods/wasd.h @@ -5,8 +5,4 @@ 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 diff --git a/tools/names.txt b/tools/names.txt index dcc66ae..39a7143 100644 --- a/tools/names.txt +++ b/tools/names.txt @@ -258,3 +258,37 @@ 0x005819a0 cCreatureArts::FindCombo # (id) -> index, 4 if none 0x00581410 cCreatureArts::ComboTooltip # (index, text, mode) 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