Skip to content

Commit 2208ddd

Browse files
authored
Merge bb4bf09 into 38bd6be
2 parents 38bd6be + bb4bf09 commit 2208ddd

11 files changed

Lines changed: 290 additions & 1 deletion

File tree

cmake/MangosParams.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
set(MANGOS_EXP "CLASSIC")
22
set(MANGOS_PKG "Mangos Zero")
3-
set(MANGOS_WORLD_VER 2026061702)
3+
set(MANGOS_WORLD_VER 2026062000)
44
set(MANGOS_REALM_VER 2026060300)
55
set(MANGOS_AHBOT_VER 2021010100)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* GM/dev commands for the Movement subsystem: .movement status/config/set.
3+
* Get/inspect movement state, and live-tune the movement config (smoothing +
4+
* global player speed rate), mirroring the .timesync command pattern.
5+
*/
6+
7+
#include "Chat.h"
8+
#include "Player.h"
9+
#include "World.h"
10+
#include "ObjectAccessor.h"
11+
#include "Log.h"
12+
13+
#include <cstring>
14+
#include <cctype>
15+
#include <cstdlib>
16+
17+
bool ChatHandler::HandleMovementStatusCommand(char* /*args*/)
18+
{
19+
Player* t = getSelectedPlayer();
20+
if (!t)
21+
t = m_session ? m_session->GetPlayer() : NULL;
22+
if (!t)
23+
{
24+
SendSysMessage(".movement status FAILED: no player. Select/target a player or run in-game.");
25+
SetSentErrorMessage(true);
26+
return false;
27+
}
28+
29+
PSendSysMessage("Movement %s: pos(%.1f, %.1f, %.1f) run=%.1f walk=%.1f swim=%.1f flags=0x%X",
30+
t->GetName(), t->GetPositionX(), t->GetPositionY(), t->GetPositionZ(),
31+
t->GetSpeed(MOVE_RUN), t->GetSpeed(MOVE_WALK), t->GetSpeed(MOVE_SWIM),
32+
t->m_movementInfo.GetMovementFlags());
33+
return true;
34+
}
35+
36+
bool ChatHandler::HandleMovementConfigCommand(char* /*args*/)
37+
{
38+
PSendSysMessage("Movement config: smoothing=%u heartbeatMs=%u maxExtrapolateMs=%u",
39+
(uint32)sWorld.getConfig(CONFIG_BOOL_MOVEMENT_SMOOTHING),
40+
sWorld.getConfig(CONFIG_UINT32_MOVEMENT_HEARTBEAT_MS),
41+
sWorld.getConfig(CONFIG_UINT32_MOVEMENT_MAX_EXTRAPOLATE_MS));
42+
PSendSysMessage(" speedRate=%u%% (run=%u%% swim=%u%% walk=%u%%)",
43+
sWorld.getConfig(CONFIG_UINT32_MOVEMENT_SPEED_RATE),
44+
sWorld.getConfig(CONFIG_UINT32_MOVEMENT_RUN_RATE),
45+
sWorld.getConfig(CONFIG_UINT32_MOVEMENT_SWIM_RATE),
46+
sWorld.getConfig(CONFIG_UINT32_MOVEMENT_WALK_RATE));
47+
SendSysMessage("Set at runtime with .movement set <field> <value> (reverts on restart/reload).");
48+
return true;
49+
}
50+
51+
bool ChatHandler::HandleMovementSetCommand(char* args)
52+
{
53+
char* f = strtok(args, " ");
54+
char* v = strtok(NULL, " ");
55+
if (!f || !v)
56+
{
57+
SendSysMessage(".movement set FAILED. Usage: .movement set <field> <value>. Fields:");
58+
SendSysMessage(" smoothing (0/1), heartbeatms (100-2000), maxextrapolatems (100-3000),");
59+
SendSysMessage(" speedrate / run / swim / walk (10-1000, percent of normal; apply live)");
60+
SetSentErrorMessage(true);
61+
return false;
62+
}
63+
std::string field = f;
64+
for (size_t i = 0; i < field.size(); ++i) field[i] = (char)tolower(field[i]);
65+
int32 val = atoi(v);
66+
bool refreshSpeed = false;
67+
68+
if (field == "smoothing")
69+
{
70+
sWorld.setConfig(CONFIG_BOOL_MOVEMENT_SMOOTHING, val != 0);
71+
}
72+
else if (field == "heartbeatms")
73+
{
74+
if (val < 100) val = 100; if (val > 2000) val = 2000;
75+
sWorld.setConfig(CONFIG_UINT32_MOVEMENT_HEARTBEAT_MS, uint32(val));
76+
}
77+
else if (field == "maxextrapolatems")
78+
{
79+
if (val < 100) val = 100; if (val > 3000) val = 3000;
80+
sWorld.setConfig(CONFIG_UINT32_MOVEMENT_MAX_EXTRAPOLATE_MS, uint32(val));
81+
}
82+
else if (field == "speedrate" || field == "run" || field == "swim" || field == "walk")
83+
{
84+
if (val < 10) val = 10; if (val > 1000) val = 1000;
85+
if (field == "speedrate") sWorld.setConfig(CONFIG_UINT32_MOVEMENT_SPEED_RATE, uint32(val));
86+
else if (field == "run") sWorld.setConfig(CONFIG_UINT32_MOVEMENT_RUN_RATE, uint32(val));
87+
else if (field == "swim") sWorld.setConfig(CONFIG_UINT32_MOVEMENT_SWIM_RATE, uint32(val));
88+
else sWorld.setConfig(CONFIG_UINT32_MOVEMENT_WALK_RATE, uint32(val));
89+
refreshSpeed = true;
90+
}
91+
else
92+
{
93+
PSendSysMessage(".movement set FAILED: unknown field '%s'.", field.c_str());
94+
SetSentErrorMessage(true);
95+
return false;
96+
}
97+
98+
if (refreshSpeed)
99+
{
100+
// Apply live: refresh every online player's speeds (forced => clients told).
101+
sObjectAccessor.DoForAllPlayers([](Player* p)
102+
{
103+
if (!p) return;
104+
for (int mt = 0; mt < MAX_MOVE_TYPE; ++mt)
105+
p->UpdateSpeed(UnitMoveType(mt), true);
106+
});
107+
}
108+
PSendSysMessage("Movement: set %s = %d (runtime; reverts on restart/reload from file).", field.c_str(), val);
109+
return true;
110+
}

src/game/Object/Player.cpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,9 @@ Player::Player(WorldSession* session): Unit(), m_mover(this), m_camera(this), m_
533533
m_playerbotMgr = 0;
534534
#endif
535535

536+
m_lastMoveRelayMs = 0;
537+
m_lastMoveHeartbeatMs = 0;
538+
536539
m_transport = 0;
537540

538541
m_speakTime = 0;
@@ -1560,6 +1563,72 @@ void Player::Update(uint32 update_diff, uint32 p_time)
15601563
return;
15611564
}
15621565

1566+
// Movement smoothing (Movement.Smoothing, OFF by default): if this player was
1567+
// moving on the ground but their client has gone quiet (lag/packet loss),
1568+
// observers would see them freeze then warp. Inject extrapolated MSG_MOVE_HEARTBEAT
1569+
// packets — dead-reckoned along their current heading, capped to a short window —
1570+
// so nearby clients interpolate smoothly until real packets resume. Broadcast only;
1571+
// the server's authoritative position is NOT changed (the next real packet corrects).
1572+
if (sWorld.getConfig(CONFIG_BOOL_MOVEMENT_SMOOTHING) && IsInWorld() && GetSession() &&
1573+
!IsBeingTeleported() && !GetTransport() && m_lastMoveRelayMs)
1574+
{
1575+
uint32 now = getMSTime();
1576+
uint32 stale = getMSTimeDiff(m_lastMoveRelayMs, now);
1577+
uint32 maxExt = sWorld.getConfig(CONFIG_UINT32_MOVEMENT_MAX_EXTRAPOLATE_MS);
1578+
uint32 hbMs = sWorld.getConfig(CONFIG_UINT32_MOVEMENT_HEARTBEAT_MS);
1579+
1580+
uint32 mflags = m_movementInfo.GetMovementFlags();
1581+
bool ground = !(mflags & (MOVEFLAG_FALLING | MOVEFLAG_FALLINGFAR | MOVEFLAG_SWIMMING |
1582+
MOVEFLAG_ONTRANSPORT | MOVEFLAG_FLYING | MOVEFLAG_CAN_FLY));
1583+
bool moving = (mflags & (MOVEFLAG_FORWARD | MOVEFLAG_BACKWARD |
1584+
MOVEFLAG_STRAFE_LEFT | MOVEFLAG_STRAFE_RIGHT)) != 0;
1585+
1586+
if (ground && moving && stale >= hbMs && stale <= maxExt &&
1587+
getMSTimeDiff(m_lastMoveHeartbeatMs, now) >= hbMs)
1588+
{
1589+
// Resolve the actual movement direction from the keypress flags in the
1590+
// body-local frame (x = forward, y = left), then rotate into world space.
1591+
float lx = 0.0f, ly = 0.0f;
1592+
if (mflags & MOVEFLAG_FORWARD) lx += 1.0f;
1593+
if (mflags & MOVEFLAG_BACKWARD) lx -= 1.0f;
1594+
if (mflags & MOVEFLAG_STRAFE_LEFT) ly += 1.0f;
1595+
if (mflags & MOVEFLAG_STRAFE_RIGHT) ly -= 1.0f;
1596+
if (lx != 0.0f || ly != 0.0f)
1597+
{
1598+
float o = GetOrientation();
1599+
float dir = o + atan2(ly, lx);
1600+
float speed = (mflags & MOVEFLAG_WALK_MODE) ? GetSpeed(MOVE_WALK)
1601+
: (lx < 0.0f ? GetSpeed(MOVE_RUN_BACK) : GetSpeed(MOVE_RUN));
1602+
float dt = float(stale) / 1000.0f;
1603+
float cx = GetPositionX(), cy = GetPositionY(), cz = GetPositionZ();
1604+
float nx = cx + cos(dir) * speed * dt;
1605+
float ny = cy + sin(dir) * speed * dt;
1606+
1607+
// Collision-aware: never extrapolate THROUGH geometry. If the
1608+
// predicted path crosses a wall, clamp the heartbeat to just before
1609+
// it (GetHitPosition pulls back 0.5yd) so the mover visually stops at
1610+
// the wall instead of clipping through and snapping back on reconcile.
1611+
float hx = nx, hy = ny, hz = cz + 1.5f;
1612+
if (GetMap()->GetHitPosition(cx, cy, cz + 1.5f, hx, hy, hz, -0.5f))
1613+
{
1614+
nx = hx; ny = hy;
1615+
}
1616+
float nz = GetMap()->GetHeight(nx, ny, cz + 2.0f);
1617+
if (nz < -50000.0f)
1618+
nz = cz;
1619+
1620+
MovementInfo hb = m_movementInfo;
1621+
hb.ChangePosition(nx, ny, nz, o);
1622+
hb.UpdateTime(now);
1623+
WorldPacket data(MSG_MOVE_HEARTBEAT, 32);
1624+
data << GetPackGUID();
1625+
data << hb;
1626+
SendMessageToSetExcept(&data, this);
1627+
m_lastMoveHeartbeatMs = now;
1628+
}
1629+
}
1630+
}
1631+
15631632
// Handle undelivered mail
15641633
if (m_nextMailDelivereTime && m_nextMailDelivereTime <= time(NULL))
15651634
{

src/game/Object/Player.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4039,6 +4039,15 @@ class Player : public Unit
40394039
// Map reference for the player
40404040
MapReference m_mapRef;
40414041

4042+
public:
4043+
// Movement smoothing: server time of the last real movement packet relayed
4044+
// for this player (set by the movement opcode handler). Used to detect a
4045+
// "stale" mover and inject extrapolated heartbeats to nearby observers.
4046+
void SetLastMoveRelayMs(uint32 t) { m_lastMoveRelayMs = t; m_lastMoveHeartbeatMs = 0; }
4047+
private:
4048+
uint32 m_lastMoveRelayMs;
4049+
uint32 m_lastMoveHeartbeatMs;
4050+
40424051
#ifdef ENABLE_PLAYERBOTS
40434052
// Player bot AI
40444053
PlayerbotAI* m_playerbotAI;

src/game/Object/Unit.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9570,6 +9570,29 @@ void Unit::UpdateSpeed(UnitMoveType mtype, bool forced, float ratio)
95709570
{
95719571
speed *= sWorld.getConfig(((Player*)this)->InBattleGround() ? CONFIG_FLOAT_GHOST_RUN_SPEED_BG : CONFIG_FLOAT_GHOST_RUN_SPEED_WORLD);
95729572
}
9573+
9574+
// Movement subsystem: global player speed-rate knob (percent, default 100)
9575+
// plus an optional per-move-type multiplier on top.
9576+
uint32 mvRate = sWorld.getConfig(CONFIG_UINT32_MOVEMENT_SPEED_RATE);
9577+
if (mvRate != 100)
9578+
{
9579+
speed *= float(mvRate) / 100.0f;
9580+
}
9581+
9582+
uint32 typeRate = 100;
9583+
switch (mtype)
9584+
{
9585+
case MOVE_RUN:
9586+
case MOVE_RUN_BACK: typeRate = sWorld.getConfig(CONFIG_UINT32_MOVEMENT_RUN_RATE); break;
9587+
case MOVE_SWIM:
9588+
case MOVE_SWIM_BACK: typeRate = sWorld.getConfig(CONFIG_UINT32_MOVEMENT_SWIM_RATE); break;
9589+
case MOVE_WALK: typeRate = sWorld.getConfig(CONFIG_UINT32_MOVEMENT_WALK_RATE); break;
9590+
default: break;
9591+
}
9592+
if (typeRate != 100)
9593+
{
9594+
speed *= float(typeRate) / 100.0f;
9595+
}
95739596
}
95749597

95759598
// Apply strongest slow aura mod to speed

src/game/WorldHandlers/Chat.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ ChatCommand* ChatHandler::getCommandTable()
155155
{ NULL, 0, true, NULL, "", NULL }
156156
};
157157

158+
static ChatCommand movementCommandTable[] =
159+
{
160+
{ "status", SEC_GAMEMASTER, true, &ChatHandler::HandleMovementStatusCommand, "", NULL },
161+
{ "config", SEC_GAMEMASTER, true, &ChatHandler::HandleMovementConfigCommand, "", NULL },
162+
{ "set", SEC_ADMINISTRATOR, true, &ChatHandler::HandleMovementSetCommand, "", NULL },
163+
{ NULL, 0, false, NULL, "", NULL }
164+
};
165+
158166
static ChatCommand auctionCommandTable[] =
159167
{
160168
{ "alliance", SEC_ADMINISTRATOR, false, &ChatHandler::HandleAuctionAllianceCommand, "", NULL },
@@ -757,6 +765,7 @@ ChatCommand* ChatHandler::getCommandTable()
757765
{ "account", SEC_PLAYER, true, NULL, "", accountCommandTable },
758766
{ "auction", SEC_ADMINISTRATOR, false, NULL, "", auctionCommandTable },
759767
{ "ahbot", SEC_ADMINISTRATOR, true, NULL, "", ahbotCommandTable },
768+
{ "movement", SEC_GAMEMASTER, true, NULL, "", movementCommandTable },
760769
{ "cast", SEC_ADMINISTRATOR, false, NULL, "", castCommandTable },
761770
{ "character", SEC_GAMEMASTER, true, NULL, "", characterCommandTable},
762771
{ "debug", SEC_MODERATOR, true, NULL, "", debugCommandTable },

src/game/WorldHandlers/Chat.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,11 @@ class ChatHandler
253253
bool HandleAHBotReloadCommand(char* args);
254254
bool HandleAHBotStatusCommand(char* args);
255255

256+
// Movement subsystem commands
257+
bool HandleMovementStatusCommand(char* args);
258+
bool HandleMovementConfigCommand(char* args);
259+
bool HandleMovementSetCommand(char* args);
260+
256261
bool HandleAuctionAllianceCommand(char* args);
257262
bool HandleAuctionGoblinCommand(char* args);
258263
bool HandleAuctionHordeCommand(char* args);

src/game/WorldHandlers/MovementHandler.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,11 @@ void WorldSession::HandleMovementOpcodes(WorldPacket& recv_data)
362362
data << mover->GetPackGUID(); // write guid
363363
movementInfo.Write(data); // write data
364364
mover->SendMessageToSetExcept(&data, _player);
365+
366+
// Movement smoothing: mark when a real movement packet was last relayed for this
367+
// mover, so Player::Update can detect a stale mover and inject heartbeats.
368+
if (plMover)
369+
plMover->SetLastMoveRelayMs(getMSTime());
365370
// Fix for seeing movement by fellow transport passengers
366371
if (plMover && plMover->GetTransport())
367372
{

src/game/WorldHandlers/World.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,17 @@ void World::LoadConfigSettings(bool reload)
615615
setConfig(CONFIG_UINT32_MAX_WHOLIST_RETURNS, "MaxWhoListReturns", 49);
616616
setConfig(CONFIG_UINT32_AUTOBROADCAST_INTERVAL, "AutoBroadcast", 600);
617617

618+
// Movement subsystem: other-player smoothing (heartbeat extrapolation for stale
619+
// movers, A/B netcode so OFF by default) + a global player speed-rate knob and
620+
// optional per-move-type multipliers.
621+
setConfig(CONFIG_BOOL_MOVEMENT_SMOOTHING, "Movement.Smoothing", false);
622+
setConfigMinMax(CONFIG_UINT32_MOVEMENT_HEARTBEAT_MS, "Movement.HeartbeatMs", 250, 100, 2000);
623+
setConfigMinMax(CONFIG_UINT32_MOVEMENT_MAX_EXTRAPOLATE_MS,"Movement.MaxExtrapolateMs", 400, 100, 3000);
624+
setConfigMinMax(CONFIG_UINT32_MOVEMENT_SPEED_RATE, "Movement.PlayerSpeedRate", 100, 10, 1000);
625+
setConfigMinMax(CONFIG_UINT32_MOVEMENT_RUN_RATE, "Movement.RunSpeedRate", 100, 10, 1000);
626+
setConfigMinMax(CONFIG_UINT32_MOVEMENT_SWIM_RATE, "Movement.SwimSpeedRate", 100, 10, 1000);
627+
setConfigMinMax(CONFIG_UINT32_MOVEMENT_WALK_RATE, "Movement.WalkSpeedRate", 100, 10, 1000);
628+
618629
if (getConfig(CONFIG_UINT32_AUTOBROADCAST_INTERVAL) > 0)
619630
{
620631
m_broadcastEnable = true;

src/game/WorldHandlers/World.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,13 @@ enum eConfigUInt32Values
215215
CONFIG_UINT32_PLAYERBOT_MINBOTLEVEL,
216216
#endif
217217
CONFIG_UINT32_AUTOBROADCAST_INTERVAL,
218+
// Movement subsystem
219+
CONFIG_UINT32_MOVEMENT_HEARTBEAT_MS,
220+
CONFIG_UINT32_MOVEMENT_MAX_EXTRAPOLATE_MS,
221+
CONFIG_UINT32_MOVEMENT_SPEED_RATE,
222+
CONFIG_UINT32_MOVEMENT_RUN_RATE,
223+
CONFIG_UINT32_MOVEMENT_SWIM_RATE,
224+
CONFIG_UINT32_MOVEMENT_WALK_RATE,
218225
CONFIG_UINT32_VALUE_COUNT
219226
};
220227

@@ -383,6 +390,8 @@ enum eConfigBoolValues
383390
// Recommended Or New Flag
384391
CONFIG_BOOL_REALM_RECOMMENDED_OR_NEW_ENABLED,
385392
CONFIG_BOOL_REALM_RECOMMENDED_OR_NEW,
393+
// Movement subsystem
394+
CONFIG_BOOL_MOVEMENT_SMOOTHING,
386395
CONFIG_BOOL_VALUE_COUNT
387396
};
388397

0 commit comments

Comments
 (0)