PLAYER_STARTED_MOVING / PLAYER_STOPPED_MOVING

This commit is contained in:
Brues
2026-05-13 18:43:32 -05:00
parent 2391e39783
commit 1914eb7ddd
20 changed files with 239 additions and 32 deletions
+2
View File
@@ -61,6 +61,8 @@ functions, just behavior the stock 1.12 engine didn't have. See the
| `EQUIPMENT_SETS_CHANGED` | *(none)* |
| `EQUIPMENT_SWAP_PENDING` | `setID` |
| `EQUIPMENT_SWAP_FINISHED` | `success, setID` |
| `PLAYER_STARTED_MOVING` | *(none)* |
| `PLAYER_STOPPED_MOVING` | *(none)* |
| `FACTION_STANDING_CHANGED` | `factionID, newStanding, repGained` |
| `ITEM_DATA_LOAD_RESULT` | `itemID, success` |
| `MODIFIER_STATE_CHANGED` | `keyName, down` |
+7
View File
@@ -0,0 +1,7 @@
-xc++
-std=c++17
-Isrc
-Iexternal/minhook/include
-DCLASSICAPI_VERSION_VALUE=1
-D_CRT_SECURE_NO_WARNINGS
--target=i686-pc-windows-msvc
+35
View File
@@ -159,6 +159,7 @@ build instructions.
- [Events](#events)
- [`C_EventUtils.IsEventValid(eventName)`](#c_eventutilsiseventvalideventname)
- [`BAG_UPDATE_DELAYED` event](#bag_update_delayed-event)
- [`PLAYER_STARTED_MOVING` / `PLAYER_STOPPED_MOVING` events](#player_started_moving--player_stopped_moving-events)
- [Globals](#globals)
- [`CLASSIC_API_VERSION`](#classic_api_version)
- [`LE_EXPANSION_*`](#le_expansion_)
@@ -3782,6 +3783,40 @@ event handlers.
> currently won't trigger `BAG_UPDATE_DELAYED`. Player bag (0..4)
> and bank (5..10) updates work normally — the 95% case.
### `PLAYER_STARTED_MOVING` / `PLAYER_STOPPED_MOVING` events
Fires (with no payload) on the transition where the player begins or
ends translational movement, matching the modern events of the same
name. Vanilla 1.12 doesn't expose any movement-state event — addons
that need to know "is the player moving" otherwise poll
`GetUnitSpeed("player")` or `IsFalling` on every `OnUpdate`.
```lua
local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_STARTED_MOVING")
f:RegisterEvent("PLAYER_STOPPED_MOVING")
f:SetScript("OnEvent", function()
if event == "PLAYER_STARTED_MOVING" then ... end
end)
```
What counts as "moving" matches modern's behavior:
| Bit | Counts? | Note |
|-----|---------|------|
| `FORWARD` / `BACKWARD` / `STRAFE_LEFT` / `STRAFE_RIGHT` | yes | W/S/A/D press, mouse-click move |
| `JUMPING` / `FALLING` / `FALLING_FAR` | yes | spacebar, walking off a ledge |
| `TURN_LEFT` / `TURN_RIGHT` (turn-in-place) | no | matches modern (turning isn't moving) |
| `WALK_MODE` (toggle), `ONTRANSPORT`, `LEVITATING`, `SWIMMING` alone | no | protocol-level flags, not active motion |
Implementation: subscribes to the shared `Tick::WorldTick` per-frame
registry (single `MinHook` on `FUN_0066FD50` shared with
`BAG_UPDATE_DELAYED`) and edge-detects on the moving-bits subset of
the player's movement-flags word at `CGPlayer + 0x9E8`. State resets
between zones — if the player pointer is unresolvable (loading
screen, character select), the next valid tick won't fire a stale
`STOPPED_MOVING`.
## Globals
### `CLASSIC_API_VERSION`
+1 -1
View File
@@ -16,7 +16,7 @@ namespace Common {
#define HOOK_FUNCTION(offset, hook, original) \
{ \
auto *target = reinterpret_cast<LPVOID>(offset); \
if (MH_CreateHook(target, static_cast<LPVOID>(hook), \
if (MH_CreateHook(target, reinterpret_cast<LPVOID>(hook), \
reinterpret_cast<LPVOID *>(&original)) != MH_OK) \
return FALSE; \
if (MH_EnableHook(target) != MH_OK) \
+1 -1
View File
@@ -88,7 +88,7 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
return FALSE;
auto *target = reinterpret_cast<LPVOID>(Offsets::FUN_INVALID_FUNCTION_PTR_CHECK);
if (MH_CreateHook(target, static_cast<LPVOID>(InvalidFunctionPtrCheck_h), nullptr) != MH_OK)
if (MH_CreateHook(target, reinterpret_cast<LPVOID>(InvalidFunctionPtrCheck_h), nullptr) != MH_OK)
return FALSE;
if (MH_EnableHook(target) != MH_OK)
return FALSE;
-2
View File
@@ -15,8 +15,6 @@
#include "MinHook.h"
#include "Offsets.h"
#include <cstdio>
namespace Game {
namespace Lua {
+8
View File
@@ -1526,6 +1526,14 @@ enum Offsets {
// 0x1000000 FLYING (no flying mounts in vanilla, but the bit
// exists for druid Flight Form etc.)
OFF_PLAYER_MOVEMENT_FLAGS = 0x9E8,
MOVEFLAG_FORWARD = 0x1,
MOVEFLAG_BACKWARD = 0x2,
MOVEFLAG_STRAFE_LEFT = 0x4,
MOVEFLAG_STRAFE_RIGHT = 0x8,
// 0x10 / 0x20 are TURN_LEFT / TURN_RIGHT — turn-in-place, not
// translational movement. Excluded from "moving" semantics
// because modern `PLAYER_STARTED_MOVING` doesn't fire for them.
MOVEFLAG_JUMPING = 0x1000,
MOVEFLAG_FALLING = 0x2000,
MOVEFLAG_FALLING_FAR = 0x4000,
MOVEFLAG_SWIMMING = 0x200000,
-1
View File
@@ -25,7 +25,6 @@
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
#include <cstring>
namespace AddOns::Info {
+13 -15
View File
@@ -18,14 +18,16 @@
//
// See TODO #67 for the full investigation history.
//
// Architecture: three hooks total.
// Architecture: two MinHooks plus one tick subscription.
//
// 1. `FUN_004F91A0` — bag-slot diff loop. Hook just sets the
// `g_pending` flag.
// 2. `FUN_004F9370` — item→bag resolver (most common path). Hook
// just sets the `g_pending` flag.
// 3. `FUN_0066FD50` — engine's per-frame world-subsystem update.
// Hook drains `g_pending` and fires DELAYED if set.
// 3. `Tick::WorldTick::AutoSubscribe` — shared per-frame
// subscription (the engine's world-subsystem update at
// `FUN_0066FD50`, single-hooked in `src/tick/WorldTick.cpp`).
// Our callback drains `g_pending` and fires DELAYED if set.
//
// All three targets are quiet (≤2 callers each, in code regions
// other DLLs don't touch). The previous attempt at this feature
@@ -52,6 +54,7 @@
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "tick/WorldTick.h"
namespace Bag::UpdateDelayed {
@@ -86,14 +89,12 @@ void __stdcall BagItemToBag_h(int guidLo, int guidHi) {
g_pending = true;
}
// `FUN_0066FD50` — `__fastcall(int, int, int)` per-frame world tick.
// We hook the TAIL: run the original first (so all of this frame's
// world-tick work completes), then drain the pending flag and fire
// DELAYED if any BAG_UPDATE happened this frame.
using WorldTick_t = void(__fastcall *)(int arg1, int arg2, int arg3);
WorldTick_t WorldTick_o = nullptr;
void __fastcall WorldTick_h(int arg1, int arg2, int arg3) {
WorldTick_o(arg1, arg2, arg3);
// Per-frame drain. Runs at the tail of each world tick (subscribed
// through `Tick::WorldTick`, which owns the single hook on
// `FUN_0066FD50`). All this frame's BAG_UPDATEs have already fired
// by the time we run; we just consume the pending flag and emit one
// DELAYED if set.
void OnWorldTick() {
if (!g_pending)
return;
g_pending = false;
@@ -112,9 +113,6 @@ static const Game::HookAutoRegister _hookItemToBag{
reinterpret_cast<void *>(&BagItemToBag_h),
reinterpret_cast<void **>(&BagItemToBag_o)};
static const Game::HookAutoRegister _hookWorldTick{
Offsets::FUN_WORLD_TICK,
reinterpret_cast<void *>(&WorldTick_h),
reinterpret_cast<void **>(&WorldTick_o)};
static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
} // namespace Bag::UpdateDelayed
-1
View File
@@ -41,7 +41,6 @@
#include "Game.h"
#include "Offsets.h"
#include "debug/Log.h"
#include <cstdint>
#include <cstdio>
-2
View File
@@ -13,12 +13,10 @@
#include "Data.h"
#include "Game.h"
#include "Offsets.h"
#include "Storage.h"
#include "event/Custom.h"
#include <algorithm>
#include <cstdint>
#include <cstring>
-1
View File
@@ -19,7 +19,6 @@
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <sstream>
namespace EquipmentSet::Storage {
-2
View File
@@ -19,8 +19,6 @@
#include "item/Location.h"
#include <cstdint>
#include <cstdio>
#include <cstring>
namespace Item::Data {
+1 -1
View File
@@ -18,7 +18,7 @@
#include "item/Location.h"
#include <cstdint>
#include <cstring>
#include <string.h>
namespace Item::Equipment {
+87
View File
@@ -0,0 +1,87 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
// `PLAYER_STARTED_MOVING` / `PLAYER_STOPPED_MOVING` — modern events
// vanilla doesn't fire. Per-frame poll of the local player's
// movement-flags word (same word `IsFalling`/`IsSwimming` read) and
// edge-detect on the "translational motion" subset of bits. Fires
// STARTED on `false→true`, STOPPED on `true→false`.
//
// The mask intentionally excludes TURN_LEFT / TURN_RIGHT (turn-in-
// place doesn't count as moving on modern) and the protocol-level
// flags like WALK_MODE, ONTRANSPORT, LEVITATING that don't reflect
// active translation. Jumping and falling DO count — modern fires
// STARTED_MOVING when you jump in place, and the airborne portion of
// any jump/fall keeps the event "moving" until you touch down.
//
// We subscribe to the shared `Tick::WorldTick` registry rather than
// installing our own MinHook on `FUN_WORLD_TICK` — MinHook permits
// only one hook per target address, and `bag/UpdateDelayed.cpp`
// also wants per-frame ticks.
#include "Offsets.h"
#include "event/Custom.h"
#include "tick/WorldTick.h"
#include <cstdint>
namespace Player::Movement {
namespace {
constexpr const char *kStartedMovingEvent = "PLAYER_STARTED_MOVING";
constexpr const char *kStoppedMovingEvent = "PLAYER_STOPPED_MOVING";
constexpr uint32_t kMovingMask =
Offsets::MOVEFLAG_FORWARD
| Offsets::MOVEFLAG_BACKWARD
| Offsets::MOVEFLAG_STRAFE_LEFT
| Offsets::MOVEFLAG_STRAFE_RIGHT
| Offsets::MOVEFLAG_JUMPING
| Offsets::MOVEFLAG_FALLING
| Offsets::MOVEFLAG_FALLING_FAR;
bool g_wasMoving = false;
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
const uint8_t *Player() {
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
return static_cast<const uint8_t *>(fn("player"));
}
void OnWorldTick() {
auto *player = Player();
if (player == nullptr) {
// Pre-world / character select / loading screen. Reset
// state so the next valid tick doesn't fire a spurious
// STOPPED when the player pointer first comes back.
g_wasMoving = false;
return;
}
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
player + Offsets::OFF_PLAYER_MOVEMENT_FLAGS);
const bool moving = (flags & kMovingMask) != 0;
if (moving == g_wasMoving)
return;
g_wasMoving = moving;
const int slot = Event::Custom::Lookup(
moving ? kStartedMovingEvent : kStoppedMovingEvent);
if (slot >= 0)
Event::Custom::Fire_None(slot);
}
} // namespace
static const Event::Custom::AutoReserve _reserveStarted{kStartedMovingEvent};
static const Event::Custom::AutoReserve _reserveStopped{kStoppedMovingEvent};
static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
} // namespace Player::Movement
-1
View File
@@ -12,7 +12,6 @@
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "quest/Cache.h"
+42
View File
@@ -0,0 +1,42 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
#include "WorldTick.h"
#include "Game.h"
#include "Offsets.h"
namespace Tick::WorldTick {
namespace {
AutoSubscribe *g_head = nullptr;
using WorldTick_t = void(__fastcall *)(int, int, int);
WorldTick_t WorldTick_o = nullptr;
void __fastcall WorldTick_h(int a, int b, int c) {
WorldTick_o(a, b, c);
for (auto *node = g_head; node != nullptr; node = node->next)
node->cb();
}
} // namespace
AutoSubscribe::AutoSubscribe(Callback cb) : cb(cb), next(g_head) {
g_head = this;
}
static const Game::HookAutoRegister _hook{
Offsets::FUN_WORLD_TICK,
reinterpret_cast<void *>(&WorldTick_h),
reinterpret_cast<void **>(&WorldTick_o)};
} // namespace Tick::WorldTick
+42
View File
@@ -0,0 +1,42 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
#pragma once
// Shared per-frame tick subscription. The engine's world-subsystem
// update at `FUN_WORLD_TICK` (`0x0066FD50`) is the canonical
// once-per-frame hook target — single caller, quiet code region, no
// known collisions with other Octo DLLs. MinHook only permits ONE
// active hook per target address, so any feature that wants a
// per-frame tick must go through this module instead of registering
// its own `HookAutoRegister`.
//
// Subscribers receive a callback at the **tail** of each frame:
// the engine's original world-tick runs first, then this module
// walks the subscriber list. Order across subscribers is unspecified
// (LIFO of static-init order across TUs), so subscribers must not
// depend on each other.
//
// Module pattern, matching `HookAutoRegister` / `ModuleAutoRegister`:
// declare a file-scope `static const Tick::WorldTick::AutoSubscribe`
// in your module's TU. The constructor chains the callback onto the
// internal list at static-init time, before `DllMain` runs.
namespace Tick::WorldTick {
using Callback = void (*)();
struct AutoSubscribe {
explicit AutoSubscribe(Callback cb);
Callback cb;
AutoSubscribe *next;
};
} // namespace Tick::WorldTick
-1
View File
@@ -17,7 +17,6 @@
#include "Offsets.h"
#include <cstdint>
#include <cstring>
#include <ctime>
#include <windows.h>
-3
View File
@@ -14,9 +14,6 @@
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
#include <cstring>
namespace Unit::Tooltip {
// `GameTooltip:SetUnitAura(unit, index, [filter])` — modern unified-aura