Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmake/MangosParams.cmake
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
set(MANGOS_EXP "CLASSIC")
set(MANGOS_PKG "Mangos Zero")
set(MANGOS_WORLD_VER 2026061702)
set(MANGOS_WORLD_VER 2026062000)
set(MANGOS_REALM_VER 2026060300)
set(MANGOS_AHBOT_VER 2021010100)
110 changes: 110 additions & 0 deletions src/game/ChatCommands/MovementCommands.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* GM/dev commands for the Movement subsystem: .movement status/config/set.
* Get/inspect movement state, and live-tune the movement config (smoothing +
* global player speed rate), mirroring the .timesync command pattern.
*/

#include "Chat.h"
#include "Player.h"
#include "World.h"
#include "ObjectAccessor.h"
#include "Log.h"

#include <cstring>
#include <cctype>
#include <cstdlib>

bool ChatHandler::HandleMovementStatusCommand(char* /*args*/)
{
Player* t = getSelectedPlayer();
if (!t)
t = m_session ? m_session->GetPlayer() : NULL;
if (!t)
{
SendSysMessage(".movement status FAILED: no player. Select/target a player or run in-game.");
SetSentErrorMessage(true);
return false;
}

PSendSysMessage("Movement %s: pos(%.1f, %.1f, %.1f) run=%.1f walk=%.1f swim=%.1f flags=0x%X",
t->GetName(), t->GetPositionX(), t->GetPositionY(), t->GetPositionZ(),
t->GetSpeed(MOVE_RUN), t->GetSpeed(MOVE_WALK), t->GetSpeed(MOVE_SWIM),
t->m_movementInfo.GetMovementFlags());
return true;
}

bool ChatHandler::HandleMovementConfigCommand(char* /*args*/)
{
PSendSysMessage("Movement config: smoothing=%u heartbeatMs=%u maxExtrapolateMs=%u",
(uint32)sWorld.getConfig(CONFIG_BOOL_MOVEMENT_SMOOTHING),
sWorld.getConfig(CONFIG_UINT32_MOVEMENT_HEARTBEAT_MS),
sWorld.getConfig(CONFIG_UINT32_MOVEMENT_MAX_EXTRAPOLATE_MS));
PSendSysMessage(" speedRate=%u%% (run=%u%% swim=%u%% walk=%u%%)",
sWorld.getConfig(CONFIG_UINT32_MOVEMENT_SPEED_RATE),
sWorld.getConfig(CONFIG_UINT32_MOVEMENT_RUN_RATE),
sWorld.getConfig(CONFIG_UINT32_MOVEMENT_SWIM_RATE),
sWorld.getConfig(CONFIG_UINT32_MOVEMENT_WALK_RATE));
SendSysMessage("Set at runtime with .movement set <field> <value> (reverts on restart/reload).");
return true;
}

bool ChatHandler::HandleMovementSetCommand(char* args)
{
char* f = strtok(args, " ");

Check notice on line 53 in src/game/ChatCommands/MovementCommands.cpp

View check run for this annotation

codefactor.io / CodeFactor

src/game/ChatCommands/MovementCommands.cpp#L53

Consider using strtok_r(...) instead of strtok(...) for improved thread safety. (runtime/threadsafe_fn)
char* v = strtok(NULL, " ");

Check notice on line 54 in src/game/ChatCommands/MovementCommands.cpp

View check run for this annotation

codefactor.io / CodeFactor

src/game/ChatCommands/MovementCommands.cpp#L54

Consider using strtok_r(...) instead of strtok(...) for improved thread safety. (runtime/threadsafe_fn)
if (!f || !v)
{
SendSysMessage(".movement set FAILED. Usage: .movement set <field> <value>. Fields:");
SendSysMessage(" smoothing (0/1), heartbeatms (100-2000), maxextrapolatems (100-3000),");
SendSysMessage(" speedrate / run / swim / walk (10-1000, percent of normal; apply live)");
SetSentErrorMessage(true);
return false;
}
std::string field = f;
for (size_t i = 0; i < field.size(); ++i) field[i] = (char)tolower(field[i]);
int32 val = atoi(v);
bool refreshSpeed = false;

if (field == "smoothing")
{
sWorld.setConfig(CONFIG_BOOL_MOVEMENT_SMOOTHING, val != 0);
}
else if (field == "heartbeatms")
{
if (val < 100) val = 100; if (val > 2000) val = 2000;
sWorld.setConfig(CONFIG_UINT32_MOVEMENT_HEARTBEAT_MS, uint32(val));
}
else if (field == "maxextrapolatems")
{
if (val < 100) val = 100; if (val > 3000) val = 3000;
sWorld.setConfig(CONFIG_UINT32_MOVEMENT_MAX_EXTRAPOLATE_MS, uint32(val));
}
else if (field == "speedrate" || field == "run" || field == "swim" || field == "walk")
{
if (val < 10) val = 10; if (val > 1000) val = 1000;
if (field == "speedrate") sWorld.setConfig(CONFIG_UINT32_MOVEMENT_SPEED_RATE, uint32(val));
else if (field == "run") sWorld.setConfig(CONFIG_UINT32_MOVEMENT_RUN_RATE, uint32(val));
else if (field == "swim") sWorld.setConfig(CONFIG_UINT32_MOVEMENT_SWIM_RATE, uint32(val));
else sWorld.setConfig(CONFIG_UINT32_MOVEMENT_WALK_RATE, uint32(val));
refreshSpeed = true;
}
else
{
PSendSysMessage(".movement set FAILED: unknown field '%s'.", field.c_str());
SetSentErrorMessage(true);
return false;
}

if (refreshSpeed)
{
// Apply live: refresh every online player's speeds (forced => clients told).
sObjectAccessor.DoForAllPlayers([](Player* p)
{
if (!p) return;
for (int mt = 0; mt < MAX_MOVE_TYPE; ++mt)
p->UpdateSpeed(UnitMoveType(mt), true);
});
}
PSendSysMessage("Movement: set %s = %d (runtime; reverts on restart/reload from file).", field.c_str(), val);
return true;
}
69 changes: 69 additions & 0 deletions src/game/Object/Player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,9 @@ Player::Player(WorldSession* session): Unit(), m_mover(this), m_camera(this), m_
m_playerbotMgr = 0;
#endif

m_lastMoveRelayMs = 0;
m_lastMoveHeartbeatMs = 0;

m_transport = 0;

m_speakTime = 0;
Expand Down Expand Up @@ -1560,6 +1563,72 @@ void Player::Update(uint32 update_diff, uint32 p_time)
return;
}

// Movement smoothing (Movement.Smoothing, OFF by default): if this player was
// moving on the ground but their client has gone quiet (lag/packet loss),
// observers would see them freeze then warp. Inject extrapolated MSG_MOVE_HEARTBEAT
// packets — dead-reckoned along their current heading, capped to a short window —
// so nearby clients interpolate smoothly until real packets resume. Broadcast only;
// the server's authoritative position is NOT changed (the next real packet corrects).
if (sWorld.getConfig(CONFIG_BOOL_MOVEMENT_SMOOTHING) && IsInWorld() && GetSession() &&
!IsBeingTeleported() && !GetTransport() && m_lastMoveRelayMs)
{
uint32 now = getMSTime();
uint32 stale = getMSTimeDiff(m_lastMoveRelayMs, now);
uint32 maxExt = sWorld.getConfig(CONFIG_UINT32_MOVEMENT_MAX_EXTRAPOLATE_MS);
uint32 hbMs = sWorld.getConfig(CONFIG_UINT32_MOVEMENT_HEARTBEAT_MS);

uint32 mflags = m_movementInfo.GetMovementFlags();
bool ground = !(mflags & (MOVEFLAG_FALLING | MOVEFLAG_FALLINGFAR | MOVEFLAG_SWIMMING |
MOVEFLAG_ONTRANSPORT | MOVEFLAG_FLYING | MOVEFLAG_CAN_FLY));
bool moving = (mflags & (MOVEFLAG_FORWARD | MOVEFLAG_BACKWARD |
MOVEFLAG_STRAFE_LEFT | MOVEFLAG_STRAFE_RIGHT)) != 0;

if (ground && moving && stale >= hbMs && stale <= maxExt &&
getMSTimeDiff(m_lastMoveHeartbeatMs, now) >= hbMs)
{
// Resolve the actual movement direction from the keypress flags in the
// body-local frame (x = forward, y = left), then rotate into world space.
float lx = 0.0f, ly = 0.0f;
if (mflags & MOVEFLAG_FORWARD) lx += 1.0f;
if (mflags & MOVEFLAG_BACKWARD) lx -= 1.0f;
if (mflags & MOVEFLAG_STRAFE_LEFT) ly += 1.0f;
if (mflags & MOVEFLAG_STRAFE_RIGHT) ly -= 1.0f;
if (lx != 0.0f || ly != 0.0f)
{
float o = GetOrientation();
float dir = o + atan2(ly, lx);
float speed = (mflags & MOVEFLAG_WALK_MODE) ? GetSpeed(MOVE_WALK)
: (lx < 0.0f ? GetSpeed(MOVE_RUN_BACK) : GetSpeed(MOVE_RUN));
float dt = float(stale) / 1000.0f;
float cx = GetPositionX(), cy = GetPositionY(), cz = GetPositionZ();
float nx = cx + cos(dir) * speed * dt;
float ny = cy + sin(dir) * speed * dt;

// Collision-aware: never extrapolate THROUGH geometry. If the
// predicted path crosses a wall, clamp the heartbeat to just before
// it (GetHitPosition pulls back 0.5yd) so the mover visually stops at
// the wall instead of clipping through and snapping back on reconcile.
float hx = nx, hy = ny, hz = cz + 1.5f;
if (GetMap()->GetHitPosition(cx, cy, cz + 1.5f, hx, hy, hz, -0.5f))
{
nx = hx; ny = hy;
}
float nz = GetMap()->GetHeight(nx, ny, cz + 2.0f);
if (nz < -50000.0f)
nz = cz;

MovementInfo hb = m_movementInfo;
hb.ChangePosition(nx, ny, nz, o);
hb.UpdateTime(now);
WorldPacket data(MSG_MOVE_HEARTBEAT, 32);
data << GetPackGUID();
data << hb;
SendMessageToSetExcept(&data, this);
m_lastMoveHeartbeatMs = now;
}
}
}

// Handle undelivered mail
if (m_nextMailDelivereTime && m_nextMailDelivereTime <= time(NULL))
{
Expand Down
9 changes: 9 additions & 0 deletions src/game/Object/Player.h
Original file line number Diff line number Diff line change
Expand Up @@ -4039,6 +4039,15 @@ class Player : public Unit
// Map reference for the player
MapReference m_mapRef;

public:
// Movement smoothing: server time of the last real movement packet relayed
// for this player (set by the movement opcode handler). Used to detect a
// "stale" mover and inject extrapolated heartbeats to nearby observers.
void SetLastMoveRelayMs(uint32 t) { m_lastMoveRelayMs = t; m_lastMoveHeartbeatMs = 0; }
private:
uint32 m_lastMoveRelayMs;
uint32 m_lastMoveHeartbeatMs;

#ifdef ENABLE_PLAYERBOTS
// Player bot AI
PlayerbotAI* m_playerbotAI;
Expand Down
23 changes: 23 additions & 0 deletions src/game/Object/Unit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9570,6 +9570,29 @@ void Unit::UpdateSpeed(UnitMoveType mtype, bool forced, float ratio)
{
speed *= sWorld.getConfig(((Player*)this)->InBattleGround() ? CONFIG_FLOAT_GHOST_RUN_SPEED_BG : CONFIG_FLOAT_GHOST_RUN_SPEED_WORLD);
}

// Movement subsystem: global player speed-rate knob (percent, default 100)
// plus an optional per-move-type multiplier on top.
uint32 mvRate = sWorld.getConfig(CONFIG_UINT32_MOVEMENT_SPEED_RATE);
if (mvRate != 100)
{
speed *= float(mvRate) / 100.0f;
}

uint32 typeRate = 100;
switch (mtype)
{
case MOVE_RUN:
case MOVE_RUN_BACK: typeRate = sWorld.getConfig(CONFIG_UINT32_MOVEMENT_RUN_RATE); break;
case MOVE_SWIM:
case MOVE_SWIM_BACK: typeRate = sWorld.getConfig(CONFIG_UINT32_MOVEMENT_SWIM_RATE); break;
case MOVE_WALK: typeRate = sWorld.getConfig(CONFIG_UINT32_MOVEMENT_WALK_RATE); break;
default: break;
}
if (typeRate != 100)
{
speed *= float(typeRate) / 100.0f;
}
}

// Apply strongest slow aura mod to speed
Expand Down
9 changes: 9 additions & 0 deletions src/game/WorldHandlers/Chat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ ChatCommand* ChatHandler::getCommandTable()
{ NULL, 0, true, NULL, "", NULL }
};

static ChatCommand movementCommandTable[] =
{
{ "status", SEC_GAMEMASTER, true, &ChatHandler::HandleMovementStatusCommand, "", NULL },
{ "config", SEC_GAMEMASTER, true, &ChatHandler::HandleMovementConfigCommand, "", NULL },
{ "set", SEC_ADMINISTRATOR, true, &ChatHandler::HandleMovementSetCommand, "", NULL },
{ NULL, 0, false, NULL, "", NULL }
};

static ChatCommand auctionCommandTable[] =
{
{ "alliance", SEC_ADMINISTRATOR, false, &ChatHandler::HandleAuctionAllianceCommand, "", NULL },
Expand Down Expand Up @@ -757,6 +765,7 @@ ChatCommand* ChatHandler::getCommandTable()
{ "account", SEC_PLAYER, true, NULL, "", accountCommandTable },
{ "auction", SEC_ADMINISTRATOR, false, NULL, "", auctionCommandTable },
{ "ahbot", SEC_ADMINISTRATOR, true, NULL, "", ahbotCommandTable },
{ "movement", SEC_GAMEMASTER, true, NULL, "", movementCommandTable },
{ "cast", SEC_ADMINISTRATOR, false, NULL, "", castCommandTable },
{ "character", SEC_GAMEMASTER, true, NULL, "", characterCommandTable},
{ "debug", SEC_MODERATOR, true, NULL, "", debugCommandTable },
Expand Down
5 changes: 5 additions & 0 deletions src/game/WorldHandlers/Chat.h
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,11 @@ class ChatHandler
bool HandleAHBotReloadCommand(char* args);
bool HandleAHBotStatusCommand(char* args);

// Movement subsystem commands
bool HandleMovementStatusCommand(char* args);
bool HandleMovementConfigCommand(char* args);
bool HandleMovementSetCommand(char* args);

bool HandleAuctionAllianceCommand(char* args);
bool HandleAuctionGoblinCommand(char* args);
bool HandleAuctionHordeCommand(char* args);
Expand Down
5 changes: 5 additions & 0 deletions src/game/WorldHandlers/MovementHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,11 @@ void WorldSession::HandleMovementOpcodes(WorldPacket& recv_data)
data << mover->GetPackGUID(); // write guid
movementInfo.Write(data); // write data
mover->SendMessageToSetExcept(&data, _player);

// Movement smoothing: mark when a real movement packet was last relayed for this
// mover, so Player::Update can detect a stale mover and inject heartbeats.
if (plMover)
plMover->SetLastMoveRelayMs(getMSTime());
// Fix for seeing movement by fellow transport passengers
if (plMover && plMover->GetTransport())
{
Expand Down
11 changes: 11 additions & 0 deletions src/game/WorldHandlers/World.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,17 @@ void World::LoadConfigSettings(bool reload)
setConfig(CONFIG_UINT32_MAX_WHOLIST_RETURNS, "MaxWhoListReturns", 49);
setConfig(CONFIG_UINT32_AUTOBROADCAST_INTERVAL, "AutoBroadcast", 600);

// Movement subsystem: other-player smoothing (heartbeat extrapolation for stale
// movers, A/B netcode so OFF by default) + a global player speed-rate knob and
// optional per-move-type multipliers.
setConfig(CONFIG_BOOL_MOVEMENT_SMOOTHING, "Movement.Smoothing", false);
setConfigMinMax(CONFIG_UINT32_MOVEMENT_HEARTBEAT_MS, "Movement.HeartbeatMs", 250, 100, 2000);
setConfigMinMax(CONFIG_UINT32_MOVEMENT_MAX_EXTRAPOLATE_MS,"Movement.MaxExtrapolateMs", 400, 100, 3000);
setConfigMinMax(CONFIG_UINT32_MOVEMENT_SPEED_RATE, "Movement.PlayerSpeedRate", 100, 10, 1000);
setConfigMinMax(CONFIG_UINT32_MOVEMENT_RUN_RATE, "Movement.RunSpeedRate", 100, 10, 1000);
setConfigMinMax(CONFIG_UINT32_MOVEMENT_SWIM_RATE, "Movement.SwimSpeedRate", 100, 10, 1000);
setConfigMinMax(CONFIG_UINT32_MOVEMENT_WALK_RATE, "Movement.WalkSpeedRate", 100, 10, 1000);

if (getConfig(CONFIG_UINT32_AUTOBROADCAST_INTERVAL) > 0)
{
m_broadcastEnable = true;
Expand Down
9 changes: 9 additions & 0 deletions src/game/WorldHandlers/World.h
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,13 @@ enum eConfigUInt32Values
CONFIG_UINT32_PLAYERBOT_MINBOTLEVEL,
#endif
CONFIG_UINT32_AUTOBROADCAST_INTERVAL,
// Movement subsystem
CONFIG_UINT32_MOVEMENT_HEARTBEAT_MS,
CONFIG_UINT32_MOVEMENT_MAX_EXTRAPOLATE_MS,
CONFIG_UINT32_MOVEMENT_SPEED_RATE,
CONFIG_UINT32_MOVEMENT_RUN_RATE,
CONFIG_UINT32_MOVEMENT_SWIM_RATE,
CONFIG_UINT32_MOVEMENT_WALK_RATE,
CONFIG_UINT32_VALUE_COUNT
};

Expand Down Expand Up @@ -383,6 +390,8 @@ enum eConfigBoolValues
// Recommended Or New Flag
CONFIG_BOOL_REALM_RECOMMENDED_OR_NEW_ENABLED,
CONFIG_BOOL_REALM_RECOMMENDED_OR_NEW,
// Movement subsystem
CONFIG_BOOL_MOVEMENT_SMOOTHING,
CONFIG_BOOL_VALUE_COUNT
};

Expand Down
39 changes: 39 additions & 0 deletions src/mangosd/mangosd.conf.dist.in
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,45 @@ PlayerCommands = 0
Motd = "Welcome to Mangos Zero"
AutoBroadcast = 0

###############################################################################
# MOVEMENT SUBSYSTEM
#
# Movement.Smoothing
# Inject extrapolated movement heartbeats for a "stale" mover (a player
# whose client packets stop mid-move during lag) so nearby players keep
# interpolating smoothly instead of seeing them freeze then warp.
# Conservative: ground movement only, capped extrapolation window,
# broadcast-only (the server's authoritative position is unchanged).
# Higher-risk netcode for A/B testing.
# Default: 0 (off)
#
# Movement.HeartbeatMs
# How often to inject a heartbeat while a mover is stale (100-2000).
# Default: 250
#
# Movement.MaxExtrapolateMs
# Stop extrapolating once a mover has been stale longer than this (100-3000).
# Default: 400
#
# Movement.PlayerSpeedRate
# Global player movement-speed multiplier, percent (100 = normal). Applies
# to all player move types. Live-tunable via .movement set speedrate.
# Default: 100
#
# Movement.RunSpeedRate / Movement.SwimSpeedRate / Movement.WalkSpeedRate
# Optional per-move-type multipliers (percent) applied on top of
# PlayerSpeedRate. Default: 100
#
###############################################################################

Movement.Smoothing = 0
Movement.HeartbeatMs = 250
Movement.MaxExtrapolateMs = 400
Movement.PlayerSpeedRate = 100
Movement.RunSpeedRate = 100
Movement.SwimSpeedRate = 100
Movement.WalkSpeedRate = 100

################################################################################
# PLAYER INTERACTION
#
Expand Down
Loading