From 1818347bc51571d9dade4fe28eebe319ce32e6a8 Mon Sep 17 00:00:00 2001 From: Krill Date: Fri, 19 Jun 2026 17:19:15 -0500 Subject: [PATCH 1/3] Movement enhancements: other-player heartbeat smoothing + tunable player speed rates Other-player movement smoothing (Movement.Smoothing, OFF by default): when a moving player's client goes quiet mid-stride (lag/packet loss), the server injects extrapolated MSG_MOVE_HEARTBEAT packets dead-reckoned along the mover's actual heading (forward/back/strafe/diagonal, backpedal-aware), capped to a short window, so nearby clients interpolate smoothly instead of seeing them freeze then warp. Broadcast-only - the server's authoritative position is never changed; the next real client packet corrects it. Ground movement only; A/B netcode. Tunable player speed (Unit::UpdateSpeed): Movement.PlayerSpeedRate global percent multiplier plus optional per-move-type Movement.Run/Swim/WalkSpeedRate, applied to players only. .movement status|config|set command group (console/SOAP capable) to inspect a player's movement state and live-tune the config; speed changes refresh online players immediately. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/game/ChatCommands/MovementCommands.cpp | 110 +++++++++++++++++++++ src/game/Object/Player.cpp | 58 +++++++++++ src/game/Object/Player.h | 9 ++ src/game/Object/Unit.cpp | 23 +++++ src/game/WorldHandlers/Chat.cpp | 9 ++ src/game/WorldHandlers/Chat.h | 5 + src/game/WorldHandlers/MovementHandler.cpp | 5 + src/game/WorldHandlers/World.cpp | 11 +++ src/game/WorldHandlers/World.h | 9 ++ src/mangosd/mangosd.conf.dist.in | 39 ++++++++ 10 files changed, 278 insertions(+) create mode 100644 src/game/ChatCommands/MovementCommands.cpp diff --git a/src/game/ChatCommands/MovementCommands.cpp b/src/game/ChatCommands/MovementCommands.cpp new file mode 100644 index 000000000..e01eedeb6 --- /dev/null +++ b/src/game/ChatCommands/MovementCommands.cpp @@ -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 +#include +#include + +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 (reverts on restart/reload)."); + return true; +} + +bool ChatHandler::HandleMovementSetCommand(char* args) +{ + char* f = strtok(args, " "); + char* v = strtok(NULL, " "); + if (!f || !v) + { + SendSysMessage(".movement set FAILED. Usage: .movement set . 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; +} diff --git a/src/game/Object/Player.cpp b/src/game/Object/Player.cpp index 3182323c8..be90b9999 100644 --- a/src/game/Object/Player.cpp +++ b/src/game/Object/Player.cpp @@ -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; @@ -1560,6 +1563,61 @@ 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 nx = GetPositionX() + cos(dir) * speed * dt; + float ny = GetPositionY() + sin(dir) * speed * dt; + float nz = GetMap()->GetHeight(nx, ny, GetPositionZ() + 2.0f); + if (nz < -50000.0f) + nz = GetPositionZ(); + + 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)) { diff --git a/src/game/Object/Player.h b/src/game/Object/Player.h index f4f84bd25..2540d742e 100644 --- a/src/game/Object/Player.h +++ b/src/game/Object/Player.h @@ -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; diff --git a/src/game/Object/Unit.cpp b/src/game/Object/Unit.cpp index 94b7dc520..b1aaeae63 100644 --- a/src/game/Object/Unit.cpp +++ b/src/game/Object/Unit.cpp @@ -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 diff --git a/src/game/WorldHandlers/Chat.cpp b/src/game/WorldHandlers/Chat.cpp index c2b7b71a1..a9f29e021 100644 --- a/src/game/WorldHandlers/Chat.cpp +++ b/src/game/WorldHandlers/Chat.cpp @@ -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 }, @@ -756,6 +764,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 }, diff --git a/src/game/WorldHandlers/Chat.h b/src/game/WorldHandlers/Chat.h index 12ae4b2a8..7d6c34db4 100644 --- a/src/game/WorldHandlers/Chat.h +++ b/src/game/WorldHandlers/Chat.h @@ -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); diff --git a/src/game/WorldHandlers/MovementHandler.cpp b/src/game/WorldHandlers/MovementHandler.cpp index 68b5d4d94..dac4e9c34 100644 --- a/src/game/WorldHandlers/MovementHandler.cpp +++ b/src/game/WorldHandlers/MovementHandler.cpp @@ -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()) { diff --git a/src/game/WorldHandlers/World.cpp b/src/game/WorldHandlers/World.cpp index 153f13d9c..79716f518 100644 --- a/src/game/WorldHandlers/World.cpp +++ b/src/game/WorldHandlers/World.cpp @@ -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; diff --git a/src/game/WorldHandlers/World.h b/src/game/WorldHandlers/World.h index 3ee1cfe08..03bdd3d09 100644 --- a/src/game/WorldHandlers/World.h +++ b/src/game/WorldHandlers/World.h @@ -214,6 +214,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 }; @@ -381,6 +388,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 }; diff --git a/src/mangosd/mangosd.conf.dist.in b/src/mangosd/mangosd.conf.dist.in index c20263309..23f2aeb89 100644 --- a/src/mangosd/mangosd.conf.dist.in +++ b/src/mangosd/mangosd.conf.dist.in @@ -885,6 +885,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 # From 1835b9799ad1d75b30841f1c17dd1a8978423d02 Mon Sep 17 00:00:00 2001 From: Krill Date: Sat, 20 Jun 2026 09:05:14 -0500 Subject: [PATCH 2/3] Movement smoothing: clamp heartbeat extrapolation at walls (no clip-through) If a predicted heartbeat position crossed world geometry, observers saw the mover clip through the wall then snap back on reconcile. The extrapolation is now collision-aware: raycast the predicted path via VMap GetHitPosition and clamp the heartbeat to just before any obstacle, so the mover visually stops at the wall. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/game/Object/Player.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/game/Object/Player.cpp b/src/game/Object/Player.cpp index be90b9999..be4ab9492 100644 --- a/src/game/Object/Player.cpp +++ b/src/game/Object/Player.cpp @@ -1600,11 +1600,22 @@ void Player::Update(uint32 update_diff, uint32 p_time) 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 nx = GetPositionX() + cos(dir) * speed * dt; - float ny = GetPositionY() + sin(dir) * speed * dt; - float nz = GetMap()->GetHeight(nx, ny, GetPositionZ() + 2.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 = GetPositionZ(); + nz = cz; MovementInfo hb = m_movementInfo; hb.ChangePosition(nx, ny, nz, o); From bb4bf09a0978edafac2c68d0b08996d21ef42890 Mon Sep 17 00:00:00 2001 From: Krill Date: Sat, 20 Jun 2026 09:13:50 -0500 Subject: [PATCH 3/3] Bump world/config version to 2026062000 for the new Movement.* config options ConfVersion and MANGOSD_CONFIG_VERSION both derive from MANGOS_WORLD_VER; bumping it so servers are prompted to refresh their config after the Movement.* additions. Co-Authored-By: Claude Opus 4.8 (1M context) --- cmake/MangosParams.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/MangosParams.cmake b/cmake/MangosParams.cmake index 52d74bce7..0bf261bcf 100644 --- a/cmake/MangosParams.cmake +++ b/cmake/MangosParams.cmake @@ -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)