diff --git a/cmake/MangosParams.cmake b/cmake/MangosParams.cmake index 52d74bce7..5fa32406a 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 2026062001) set(MANGOS_REALM_VER 2026060300) set(MANGOS_AHBOT_VER 2021010100) diff --git a/src/game/CMakeLists.txt b/src/game/CMakeLists.txt index f4784b20d..957ea5bed 100644 --- a/src/game/CMakeLists.txt +++ b/src/game/CMakeLists.txt @@ -69,6 +69,10 @@ source_group("Tool" FILES ${SRC_GRP_TOOL}) file(GLOB SRC_GRP_VMAPS vmap/*.cpp vmap/*.h) source_group("vmaps" FILES ${SRC_GRP_VMAPS}) +#DebugVis group +file(GLOB SRC_GRP_DEBUGVIS DebugVis/*.cpp DebugVis/*.h) +source_group("DebugVis" FILES ${SRC_GRP_DEBUGVIS}) + #Warden group file(GLOB SRC_GRP_WARDEN Warden/*.cpp Warden/*.h) source_group("Warden" FILES ${SRC_GRP_WARDEN}) @@ -235,6 +239,7 @@ add_library(game STATIC ${SRC_GRP_TIME} ${SRC_GRP_TOOL} ${SRC_GRP_VMAPS} + ${SRC_GRP_DEBUGVIS} ${SRC_GRP_WARDEN} ${SRC_GRP_WARDEN_MODULES} ${SRC_GRP_WORLD_HANDLERS} @@ -257,6 +262,7 @@ target_include_directories(game Time Tools vmap + DebugVis Warden Warden/Modules WorldHandlers diff --git a/src/game/ChatCommands/DebugVisCommands.cpp b/src/game/ChatCommands/DebugVisCommands.cpp new file mode 100644 index 000000000..c1af8daf4 --- /dev/null +++ b/src/game/ChatCommands/DebugVisCommands.cpp @@ -0,0 +1,273 @@ +/* + * GM chat commands for the server-side debug visualization toolkit: + * .debug vis cells|los|path|collision|height + * Each draws a one-shot, auto-despawning set of markers (engine-style debug draw). + */ + +#include "Chat.h" +#include "DebugVis.h" +#include "PerformanceMonitor.h" +#include "Player.h" +#include "Map.h" +#include "World.h" +#include "GridDefines.h" +#include "PathFinder.h" + +#include +#include +#include + +bool ChatHandler::HandleDebugVisCellsCommand(char* args) +{ + Player* player = m_session ? m_session->GetPlayer() : NULL; + if (!player) + return false; + + int radius = 2; + if (args && *args) + radius = atoi(args); + if (radius < 1) radius = 1; + if (radius > 5) radius = 5; // (2r+1)^2 markers cap + + Map* map = player->GetMap(); + const float cell = SIZE_OF_GRID_CELL; + // Snap to the cell lattice so markers line up with cell spacing. + float baseX = floor(player->GetPositionX() / cell) * cell; + float baseY = floor(player->GetPositionY() / cell) * cell; + + uint32 placed = 0; + for (int i = -radius; i <= radius; ++i) + { + for (int j = -radius; j <= radius; ++j) + { + float wx = baseX + i * cell; + float wy = baseY + j * cell; + // Search ground from well above the player's elevation so distant cells + // snap to real terrain; skip cells with no ground beneath (was placing + // markers at the player's Z, which left them floating in mid-air). + float wz = map->GetHeight(wx, wy, player->GetPositionZ() + 50.0f); + if (wz < -50000.0f) + continue; + char lbl[160]; + snprintf(lbl, sizeof(lbl), "DebugVis CELL [%+d,%+d]\n(%.1f, %.1f, %.1f) ground Z=%.1f", + i, j, wx, wy, wz, wz); + if (DebugVis::Marker(player, DebugVis::DV_CELL, wx, wy, wz, lbl)) + ++placed; + } + } + PSendSysMessage("DebugVis: drew %u cell-grid markers (cell=%.1f yd, radius=%d). Despawn in %us.", + placed, cell, radius, DebugVis::DespawnSeconds()); + return true; +} + +bool ChatHandler::HandleDebugVisLosCommand(char* /*args*/) +{ + Player* player = m_session ? m_session->GetPlayer() : NULL; + if (!player) + return false; + + Unit* target = getSelectedUnit(); + if (!target) + { + SendSysMessage(".debug vis los FAILED: no target selected. It draws the line of sight " + "from you to a unit (green = clear, red = blocked, with the hit point marked). " + "Select/target a creature or player, then run .debug vis los again."); + SetSentErrorMessage(true); + return false; + } + + Map* map = player->GetMap(); + float x1 = player->GetPositionX(), y1 = player->GetPositionY(), z1 = player->GetPositionZ() + 2.0f; + float x2 = target->GetPositionX(), y2 = target->GetPositionY(), z2 = target->GetPositionZ() + 2.0f; + + float total = sqrtf((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1) + (z2 - z1) * (z2 - z1)); + if (map->IsInLineOfSight(x1, y1, z1, x2, y2, z2)) + { + char lbl[160]; + snprintf(lbl, sizeof(lbl), "DebugVis LoS: CLEAR\nto %s, %.1f yd", target->GetName(), total); + DebugVis::Line(player, DebugVis::DV_LOS_OK, x1, y1, z1, x2, y2, z2, 2.0f, lbl); + SendSysMessage("DebugVis: line of sight is CLEAR (green)."); + } + else + { + float hx = x2, hy = y2, hz = z2; + map->GetHitPosition(x1, y1, z1, hx, hy, hz, -0.5f); + float hitDist = sqrtf((hx - x1) * (hx - x1) + (hy - y1) * (hy - y1) + (hz - z1) * (hz - z1)); + DebugVis::Line(player, DebugVis::DV_LOS_BLOCK, x1, y1, z1, hx, hy, hz, 2.0f, "DebugVis LoS: BLOCKED"); + char lbl[192]; + snprintf(lbl, sizeof(lbl), + "DebugVis LoS BLOCKED\nhit (%.1f, %.1f, %.1f)\n%.1f yd from you (target %.1f yd)", + hx, hy, hz, hitDist, total); + DebugVis::Marker(player, DebugVis::DV_HITPOINT, hx, hy, hz, lbl); + PSendSysMessage("DebugVis: line of sight BLOCKED (red); hit at (%.1f, %.1f, %.1f).", hx, hy, hz); + } + return true; +} + +bool ChatHandler::HandleDebugVisPathCommand(char* /*args*/) +{ + Player* player = m_session ? m_session->GetPlayer() : NULL; + if (!player) + return false; + + Unit* target = getSelectedUnit(); + if (!target) + { + SendSysMessage(".debug vis path FAILED: no target selected. It draws the navmesh path " + "from you to a unit (green = valid path, red = incomplete/none). " + "Select/target a creature or player, then run .debug vis path again."); + SetSentErrorMessage(true); + return false; + } + + PathFinder pf(player); + pf.calculate(target->GetPositionX(), target->GetPositionY(), target->GetPositionZ()); + PathType type = pf.getPathType(); + PointsArray& pts = pf.getPath(); + + DebugVis::Category cat = (type & (PATHFIND_NOPATH | PATHFIND_INCOMPLETE | PATHFIND_NOT_USING_PATH)) + ? DebugVis::DV_PATH_BAD : DebugVis::DV_PATH; + + uint32 total = uint32(pts.size()); + uint32 placed = 0; + uint32 idx = 0; + for (PointsArray::const_iterator it = pts.begin(); it != pts.end(); ++it, ++idx) + { + // Skip the path point on/next to the player — markers are solid and would + // trap the caster inside the object. + float pdx = it->x - player->GetPositionX(); + float pdy = it->y - player->GetPositionY(); + if (pdx * pdx + pdy * pdy < 9.0f) // within ~3 yd + continue; + char lbl[176]; + snprintf(lbl, sizeof(lbl), + "DebugVis PATH pt %u/%u\n(%.1f, %.1f, %.1f)\ntype=0x%X (%s)", + idx + 1, total, it->x, it->y, it->z, uint32(type), + cat == DebugVis::DV_PATH ? "ok" : "incomplete/none"); + if (DebugVis::Marker(player, cat, it->x, it->y, it->z, lbl)) + ++placed; + } + + PSendSysMessage("DebugVis: path type=0x%X, %u points (%s).", + uint32(type), placed, cat == DebugVis::DV_PATH ? "green=ok" : "red=incomplete/none"); + return true; +} + +bool ChatHandler::HandleDebugVisCollisionCommand(char* args) +{ + Player* player = m_session ? m_session->GetPlayer() : NULL; + if (!player) + return false; + + float dist = 40.0f; + if (args && *args) + { + float v = (float)atof(args); + if (v > 1.0f && v < 300.0f) + dist = v; + } + + Map* map = player->GetMap(); + float o = player->GetOrientation(); + float x1 = player->GetPositionX(), y1 = player->GetPositionY(), z1 = player->GetPositionZ() + 2.0f; + float x2 = x1 + cos(o) * dist, y2 = y1 + sin(o) * dist, z2 = z1; + + float hx = x2, hy = y2, hz = z2; + if (map->GetHitPosition(x1, y1, z1, hx, hy, hz, -0.5f)) + { + float hitDist = sqrtf((hx - x1) * (hx - x1) + (hy - y1) * (hy - y1)); + DebugVis::Line(player, DebugVis::DV_COLLISION, x1, y1, z1, hx, hy, hz, 2.0f, "DebugVis: collision ray"); + char lbl[176]; + snprintf(lbl, sizeof(lbl), "DebugVis COLLISION\nhit (%.1f, %.1f, %.1f)\n%.1f yd ahead", + hx, hy, hz, hitDist); + DebugVis::Marker(player, DebugVis::DV_HITPOINT, hx, hy, hz, lbl); + PSendSysMessage("DebugVis: collision at (%.1f, %.1f, %.1f), %.1f yd ahead.", hx, hy, hz, hitDist); + } + else + { + char lbl[96]; + snprintf(lbl, sizeof(lbl), "DebugVis: no collision\nwithin %.0f yd ahead", dist); + DebugVis::Line(player, DebugVis::DV_COLLISION, x1, y1, z1, x2, y2, z2, 2.0f, lbl); + PSendSysMessage("DebugVis: no collision within %.0f yd ahead.", dist); + } + return true; +} + +bool ChatHandler::HandleDebugVisualCommand(char* args) +{ + Player* player = m_session ? m_session->GetPlayer() : NULL; + if (!player) + return false; + if (!args || !*args) + { + // NOTE: SMSG_PLAY_SPELL_VISUAL takes a SpellVisualKit.dbc id (not SpellVisual). + // 179 is confirmed working (trainer learn-spell sparkle). Valid kit ids in the + // 1.12.1 client run 1..6757; good ones to try below. + SendSysMessage("Usage: .debug visual (1-6757). Confirmed working: 179. " + "Try also 224, 300, 451, 686, 1027, 5670."); + SetSentErrorMessage(true); + return false; + } + uint32 id = uint32(atoi(args)); + if (!id) + { + SendSysMessage("DebugVis: invalid visual id."); + SetSentErrorMessage(true); + return false; + } + player->PlaySpellVisual(id); + PSendSysMessage("DebugVis: played spell visual kit %u on you (visible to nearby players).", id); + return true; +} + +bool ChatHandler::HandleDebugPerfCommand(char* args) +{ + if (args && *args && (*args == 'r' || *args == 'R')) + { + PerformanceMonitor::Reset(); + SendSysMessage("PerformanceMonitor: stats reset."); + return true; + } + + uint32 ticks, avgMs, maxMs, lastMs; + PerformanceMonitor::GetStats(ticks, avgMs, maxMs, lastMs); + PSendSysMessage("Server perf: world ticks=%u avg=%ums max=%ums last=%ums (.debug perf r to reset)", + ticks, avgMs, maxMs, lastMs); + return true; +} + +bool ChatHandler::HandleDebugVisHeightCommand(char* /*args*/) +{ + Player* player = m_session ? m_session->GetPlayer() : NULL; + if (!player) + return false; + + Map* map = player->GetMap(); + float px = player->GetPositionX(), py = player->GetPositionY(), pz = player->GetPositionZ(); + float groundZ = map->GetHeight(px, py, pz); + + if (groundZ < -50000.0f) + { + SendSysMessage("DebugVis: no terrain height data at this position."); + return true; + } + + char lbl[160]; + snprintf(lbl, sizeof(lbl), "DebugVis HEIGHT\nground Z=%.2f\nyou Z=%.2f (delta %.2f)", + groundZ, pz, pz - groundZ); + DebugVis::Marker(player, DebugVis::DV_HEIGHT, px, py, groundZ, lbl); + PSendSysMessage("DebugVis: ground Z=%.2f, you Z=%.2f (delta %.2f).", groundZ, pz, pz - groundZ); + return true; +} + +bool ChatHandler::HandleDebugVisClearCommand(char* /*args*/) +{ + Player* player = m_session ? m_session->GetPlayer() : NULL; + if (!player) + return false; + + uint32 removed = DebugVis::Clear(player); + PSendSysMessage("DebugVis: cleared %u of your markers (the rest had already auto-despawned). " + "Markers also auto-clear after %u seconds.", removed, DebugVis::DespawnSeconds()); + return true; +} diff --git a/src/game/DebugVis/DebugVis.cpp b/src/game/DebugVis/DebugVis.cpp new file mode 100644 index 000000000..a22e9b798 --- /dev/null +++ b/src/game/DebugVis/DebugVis.cpp @@ -0,0 +1,247 @@ +/* + * Server-side debug visualization toolkit — implementation. + */ + +#include "DebugVis.h" +#include "Player.h" +#include "GameObject.h" +#include "Map.h" +#include "ObjectGuid.h" +#include "World.h" +#include "Config/Config.h" + +#include +#include +#include + +namespace +{ + // Per-category marker MODEL/COLOUR (a GameObjectDisplayInfo.dbc display id), + // applied per-instance via GAMEOBJECT_DISPLAYID. + // + // IMPORTANT: the client only lets you mouse-over/hover a gameobject whose + // *model* is a solid clickable mesh. Particle/effect doodads (light columns, + // auras, ground reticles) render but are NOT hoverable, so their per-instance + // tooltip never shows. Both presets below therefore use solid crystal models. + // DebugVis.Style 0 = colour-coded Power Crystals [default], 1 = assorted + // floating crystals. Any single category is overridable via DebugVis.Disp.. + uint32 ColorDisplayId(DebugVis::Category cat) + { + const bool alt = sConfig.GetIntDefault("DebugVis.Style", 0) != 0; + + // {primary (power crystals, clean palette), alternate (assorted crystals)}. + uint32 prim, alt2; const char* key; + switch (cat) + { + case DebugVis::DV_CELL: key = "DebugVis.Disp.Cell"; prim = 5912; alt2 = 5811; break; // WSG (silverwing) flag - small, clean grid marker + case DebugVis::DV_LOS_OK: key = "DebugVis.Disp.LosOk"; prim = 2972; alt2 = 6431; break; // green power crystal / glyphed crystal + case DebugVis::DV_LOS_BLOCK: key = "DebugVis.Disp.LosBlock"; prim = 2973; alt2 = 6573; break; // red power crystal / red crystal + case DebugVis::DV_PATH: key = "DebugVis.Disp.Path"; prim = 2972; alt2 = 6431; break; // green power crystal / glyphed crystal + case DebugVis::DV_PATH_BAD: key = "DebugVis.Disp.PathBad"; prim = 2973; alt2 = 6573; break; // red power crystal / red crystal + case DebugVis::DV_COLLISION: key = "DebugVis.Disp.Collision"; prim = 1667; alt2 = 1667; break; // purple floating crystal + case DebugVis::DV_HEIGHT: key = "DebugVis.Disp.Height"; prim = 2974; alt2 = 6570; break; // yellow power crystal / silithus crystal + case DebugVis::DV_HITPOINT: key = "DebugVis.Disp.HitPoint"; prim = 5746; alt2 = 6571; break; // crimson shard / broken red crystal + case DebugVis::DV_GENERIC: + default: key = "DebugVis.Disp.Generic"; prim = 5811; alt2 = 5746; break; // dark crystal / crimson shard + } + return uint32(sConfig.GetIntDefault(key, int32(alt ? alt2 : prim))); + } + + // Optional GLOW companion model spawned alongside each clickable crystal: a + // colour-matched particle effect (light column / glow circle) for the + // "engine debug-draw" look. Effect models aren't clickable, which is fine — + // the co-located crystal is the hover target. 0 disables a category's glow. + uint32 GlowDisplayId(DebugVis::Category cat) + { + uint32 def; const char* key; + switch (cat) + { + case DebugVis::DV_CELL: key = "DebugVis.GlowDisp.Cell"; def = 0; break; // none: the WSG flag is the cell marker (no tall column) + case DebugVis::DV_LOS_OK: key = "DebugVis.GlowDisp.LosOk"; def = 3993; break; // green column + case DebugVis::DV_LOS_BLOCK: key = "DebugVis.GlowDisp.LosBlock"; def = 327; break; // red + case DebugVis::DV_PATH: key = "DebugVis.GlowDisp.Path"; def = 3993; break; // green column + case DebugVis::DV_PATH_BAD: key = "DebugVis.GlowDisp.PathBad"; def = 327; break; // red + case DebugVis::DV_COLLISION: key = "DebugVis.GlowDisp.Collision"; def = 363; break; // purple column + case DebugVis::DV_HEIGHT: key = "DebugVis.GlowDisp.Height"; def = 266; break; // yellow column + case DebugVis::DV_HITPOINT: key = "DebugVis.GlowDisp.HitPoint"; def = 6430; break; // cannon target reticle + case DebugVis::DV_GENERIC: + default: key = "DebugVis.GlowDisp.Generic"; def = 6679; break; // glow circle + } + return uint32(sConfig.GetIntDefault(key, int32(def))); + } + + // Per-category marker scale. Some models (the collision/hit crystals) render + // much larger than the rest; shrink those so the line/impact markers aren't + // oversized. Overridable via config. + float MarkerScale(DebugVis::Category cat) + { + switch (cat) + { + case DebugVis::DV_COLLISION: return sConfig.GetFloatDefault("DebugVis.Scale.Collision", 0.5f); + case DebugVis::DV_HITPOINT: return sConfig.GetFloatDefault("DebugVis.Scale.HitPoint", 0.6f); + default: return sConfig.GetFloatDefault("DebugVis.Scale", 1.0f); + } + } + + // entry -> per-instance tooltip text for spawned pool markers. Touched only + // from the world/session thread (command handlers + GO-query handler), so no + // locking needed. Bounded to the pool size (ring reuse overwrites). + std::map& LabelMap() + { + static std::map s_labels; + return s_labels; + } + + // Next ring slot. Each labeled marker consumes one pool entry. + uint32 NextLabeledEntry(const std::string& label) + { + static uint32 s_cursor = 0; + uint32 entry = uint32(DebugVis::DEBUGVIS_ENTRY_BASE) + (s_cursor % DebugVis::DEBUGVIS_ENTRY_COUNT); + s_cursor = (s_cursor + 1) % DebugVis::DEBUGVIS_ENTRY_COUNT; + LabelMap()[entry] = label; + return entry; + } + + // Shared, never-relabeled pool entry for unlabeled fill markers (ray dots). + // Uses the very top of the pool so the ring above doesn't clobber it often. + uint32 SharedFillEntry() + { + return uint32(DebugVis::DEBUGVIS_ENTRY_BASE) + uint32(DebugVis::DEBUGVIS_ENTRY_COUNT) - 1; + } + + // Per-placer record of spawned marker GO guids, so `.debug vis clear` can + // despawn them on demand (before their auto-despawn fires). Same single + // world thread as Marker()/Clear(), so no locking. + std::map >& MarkerOwners() + { + static std::map > s_owners; + return s_owners; + } +} + +namespace DebugVis +{ + uint32 DespawnSeconds() + { + return sWorld.getConfig(CONFIG_UINT32_DEBUGVIS_DESPAWN); + } + + bool IsDebugEntry(uint32 entry) + { + return entry >= uint32(DEBUGVIS_ENTRY_BASE) && + entry < uint32(DEBUGVIS_ENTRY_BASE) + uint32(DEBUGVIS_ENTRY_COUNT); + } + + bool GetEntryLabel(uint32 entry, std::string& out) + { + std::map& m = LabelMap(); + std::map::const_iterator it = m.find(entry); + if (it == m.end() || it->second.empty()) + return false; + out = it->second; + return true; + } + + bool Marker(Player* viewer, Category cat, float x, float y, float z, const std::string& label) + { + if (!viewer || !viewer->IsInWorld()) + return false; + + uint32 despawnMs = DespawnSeconds() * IN_MILLISECONDS; + float ori = viewer->GetOrientation(); + float scale = MarkerScale(cat); + ObjectGuid owner = viewer->GetObjectGuid(); + bool placed = false; + + // Optional colour-matched glow effect co-located with the crystal, for the + // "debug-draw" look. Effect model => not clickable (that's the crystal's + // job), so it shares the fill entry and carries no tooltip. + if (sConfig.GetIntDefault("DebugVis.Glow", 1)) + { + uint32 glow = GlowDisplayId(cat); + if (glow) + if (GameObject* g = viewer->SummonGameObject(SharedFillEntry(), x, y, z, ori, despawnMs, glow, scale)) + { + g->SetSpawnedByDefault(false); // honour the despawn timer + MarkerOwners()[owner].push_back(g->GetObjectGuid()); + placed = true; + } + } + + // Clickable crystal — the hover target. Labeled markers each take a distinct + // pool entry (per-instance tooltip); unlabeled ones share a single entry. + uint32 entry = label.empty() ? SharedFillEntry() : NextLabeledEntry(label); + if (GameObject* go = viewer->SummonGameObject(entry, x, y, z, ori, despawnMs, ColorDisplayId(cat), scale)) + { + go->SetSpawnedByDefault(false); // honour the despawn timer + MarkerOwners()[owner].push_back(go->GetObjectGuid()); + placed = true; + } + + return placed; + } + + uint32 Clear(Player* viewer) + { + if (!viewer) + return 0; + + std::map >& owners = MarkerOwners(); + std::map >::iterator it = owners.find(viewer->GetObjectGuid()); + if (it == owners.end()) + return 0; + + Map* map = viewer->GetMap(); + uint32 removed = 0; + for (std::vector::const_iterator g = it->second.begin(); g != it->second.end(); ++g) + { + // Skip any that already auto-despawned (GetGameObject returns NULL). + if (map) + if (GameObject* go = map->GetGameObject(*g)) + { + go->Delete(); + ++removed; + } + } + owners.erase(it); + return removed; + } + + uint32 Line(Player* viewer, Category cat, + float x1, float y1, float z1, float x2, float y2, float z2, + float spacing, const std::string& label) + { + if (!viewer || !viewer->IsInWorld()) + return 0; + if (spacing < 0.5f) + spacing = 0.5f; + + float dx = x2 - x1, dy = y2 - y1, dz = z2 - z1; + float len = sqrtf(dx * dx + dy * dy + dz * dz); + uint32 steps = uint32(len / spacing); + if (steps > 200) // safety cap so a long ray can't flood the world + steps = 200; + + // Don't drop a marker on (or right next to) the caster — markers are solid + // and would trap the player inside the object. Skip the first few yards. + const float MIN_FROM_START = 3.0f; + + uint32 placed = 0; + bool labelUsed = false; + for (uint32 i = 0; i <= steps; ++i) + { + float t = steps ? float(i) / float(steps) : 0.0f; + if (len * t < MIN_FROM_START) + continue; // skip dots on/next to the caster + // The first PLACED dot (out along the line) carries the verbose tooltip; + // the rest are unlabeled fill so we don't burn the whole ring on one ray. + std::string dotLabel = labelUsed ? std::string() : label; + if (Marker(viewer, cat, x1 + dx * t, y1 + dy * t, z1 + dz * t, dotLabel)) + { + ++placed; + labelUsed = true; + } + } + return placed; + } +} diff --git a/src/game/DebugVis/DebugVis.h b/src/game/DebugVis/DebugVis.h new file mode 100644 index 000000000..92cad2a17 --- /dev/null +++ b/src/game/DebugVis/DebugVis.h @@ -0,0 +1,76 @@ +/* + * Server-side debug visualization toolkit. + * + * Engine-style "debug draw", but rendered server-side by spawning temporary + * gameobjects the client can see. General purpose: anti-cheat movement traces, + * map cells/grid, line-of-sight, pathfinding, collision raycasts, terrain + * height/liquid — anything server-authoritative we can place a marker for. + * + * Diagnostic only (markers are non-interactive generic gameobjects; no gameplay + * effect) and GM-driven via the `.debug` commands. Markers auto-despawn. + */ + +#ifndef MANGOS_DEBUGVIS_H +#define MANGOS_DEBUGVIS_H + +#include "Common.h" + +#include + +class Player; + +namespace DebugVis +{ + // Visual categories -> colour/model (per-category display id, config-driven). + enum Category + { + DV_GENERIC = 0, + DV_CELL, // map grid / cell boundaries + DV_LOS_OK, // line of sight: clear segment + DV_LOS_BLOCK, // line of sight: blocked segment + DV_PATH, // pathfinding: a navmesh path point + DV_PATH_BAD, // pathfinding: incomplete / no path + DV_COLLISION, // collision raycast ray + DV_HEIGHT, // terrain / liquid height sample + DV_HITPOINT // impact point (LoS block / collision) — reticle + }; + + // Reserved gameobject_template entry pool (see debugvis_marker_pool.sql). + // Each *labeled* marker takes a distinct entry from this ring so the client + // (which caches GO name per entry) can show that marker's own tooltip. + enum + { + DEBUGVIS_ENTRY_BASE = 305000, + DEBUGVIS_ENTRY_COUNT = 512 + }; + + // Marker lifetime (seconds) from config. + uint32 DespawnSeconds(); + + // Spawn a single marker at (x,y,z), summoned into the viewer's map so the + // viewer (and nearby players) can see it. If `label` is non-empty it becomes + // the marker's mouse-over tooltip (verbose per-instance debug text). + // Returns false if it couldn't spawn. + bool Marker(Player* viewer, Category cat, float x, float y, float z, + const std::string& label = std::string()); + + // Spawn markers along the 3D segment p1->p2 every `spacing` yards (raw 3D, not + // ground-snapped — used for LoS/collision rays). Returns markers placed. + // The ray fill dots share a generic tooltip (`label`, optional). + uint32 Line(Player* viewer, Category cat, + float x1, float y1, float z1, float x2, float y2, float z2, + float spacing, const std::string& label = std::string()); + + // Immediately despawn all markers the given viewer has placed (that haven't + // already auto-despawned). Returns how many were removed. + uint32 Clear(Player* viewer); + + // True if `entry` belongs to the reserved debug-marker pool. + bool IsDebugEntry(uint32 entry); + + // Look up the per-instance tooltip stored for a pool entry. Returns false if + // none recorded. Consumed by the gameobject-query handler. + bool GetEntryLabel(uint32 entry, std::string& out); +} + +#endif // MANGOS_DEBUGVIS_H diff --git a/src/game/DebugVis/PerformanceMonitor.cpp b/src/game/DebugVis/PerformanceMonitor.cpp new file mode 100644 index 000000000..3427ecc9d --- /dev/null +++ b/src/game/DebugVis/PerformanceMonitor.cpp @@ -0,0 +1,38 @@ +/* + * Lightweight server performance monitor — implementation. + */ + +#include "PerformanceMonitor.h" + +namespace +{ + uint32 s_ticks = 0; + uint64 s_sumMs = 0; + uint32 s_maxMs = 0; + uint32 s_lastMs = 0; +} + +namespace PerformanceMonitor +{ + void TrackUpdate(uint32 diffMs) + { + ++s_ticks; + s_sumMs += diffMs; + s_lastMs = diffMs; + if (diffMs > s_maxMs) + s_maxMs = diffMs; + } + + void GetStats(uint32& ticks, uint32& avgMs, uint32& maxMs, uint32& lastMs) + { + ticks = s_ticks; + avgMs = s_ticks ? uint32(s_sumMs / s_ticks) : 0; + maxMs = s_maxMs; + lastMs = s_lastMs; + } + + void Reset() + { + s_ticks = 0; s_sumMs = 0; s_maxMs = 0; s_lastMs = 0; + } +} diff --git a/src/game/DebugVis/PerformanceMonitor.h b/src/game/DebugVis/PerformanceMonitor.h new file mode 100644 index 000000000..d6d5a107e --- /dev/null +++ b/src/game/DebugVis/PerformanceMonitor.h @@ -0,0 +1,26 @@ +/* + * Lightweight server performance monitor. + * + * Tracks World::Update tick timing (the diff passed each world tick) so server + * health is observable via `.debug perf`. Single-threaded: World::Update runs on + * the world thread only, so no locking is needed. + */ + +#ifndef MANGOS_PERFORMANCEMONITOR_H +#define MANGOS_PERFORMANCEMONITOR_H + +#include "Common.h" + +namespace PerformanceMonitor +{ + // Record one world tick (its elapsed diff in ms). + void TrackUpdate(uint32 diffMs); + + // Read cumulative stats since the last Reset(). + void GetStats(uint32& ticks, uint32& avgMs, uint32& maxMs, uint32& lastMs); + + // Clear accumulated stats. + void Reset(); +} + +#endif // MANGOS_PERFORMANCEMONITOR_H diff --git a/src/game/DebugVis/sql/debugvis_marker_pool.sql b/src/game/DebugVis/sql/debugvis_marker_pool.sql new file mode 100644 index 000000000..443ff720b --- /dev/null +++ b/src/game/DebugVis/sql/debugvis_marker_pool.sql @@ -0,0 +1,27 @@ +-- DebugVis labeled-marker pool. +-- +-- Reserves a block of generic gameobject_template entries the debug visualizer +-- ring-allocates at runtime. Each labeled marker gets its own entry so the +-- client (which caches GO name/type per entry) shows that marker's verbose +-- tooltip. The model/colour is overridden per-instance via GAMEOBJECT_DISPLAYID, +-- so the template displayId here is only a harmless default. +-- +-- Range 305000..305511 sits just above the current MAX(entry) (~300153), so +-- sGOStorage's index array grows only marginally. +-- +-- type=10 = GAMEOBJECT_TYPE_GOOBER. This matters: the vanilla client only shows +-- a name tooltip / mouse-over highlight for *interactive* gameobjects. type=5 +-- GENERIC (decorative doodads: auras, columns) render but never show a tooltip, +-- so the per-instance hover label would be invisible. GOOBER shows the name on +-- hover with a harmless click. All goober data fields are 0 (no lock, no quest, +-- no spell), so clicking does nothing meaningful. Safe to re-run. + +DELETE FROM `gameobject_template` WHERE `entry` BETWEEN 305000 AND 305511; + +INSERT INTO `gameobject_template` (`entry`, `type`, `displayId`, `name`, `faction`, `flags`, `size`) +WITH RECURSIVE seq(n) AS ( + SELECT 305000 + UNION ALL + SELECT n + 1 FROM seq WHERE n < 305511 +) +SELECT n, 10, 263, 'DebugVis Marker', 0, 0, 1 FROM seq; diff --git a/src/game/Object/Object.cpp b/src/game/Object/Object.cpp index 963180e0b..50bc6a110 100644 --- a/src/game/Object/Object.cpp +++ b/src/game/Object/Object.cpp @@ -2614,7 +2614,7 @@ Creature* WorldObject::SummonCreature(uint32 id, float x, float y, float z, floa * @param despwtime The despawn time in milliseconds. * @return The summoned game object, or null on failure. */ -GameObject* WorldObject::SummonGameObject(uint32 id, float x, float y, float z, float angle, uint32 despwtime) +GameObject* WorldObject::SummonGameObject(uint32 id, float x, float y, float z, float angle, uint32 despwtime, uint32 displayId, float scale) { GameObject* pGameObj = new GameObject; @@ -2633,6 +2633,14 @@ GameObject* WorldObject::SummonGameObject(uint32 id, float x, float y, float z, pGameObj->SetRespawnTime(despwtime/IN_MILLISECONDS); + // Optional per-instance model/colour override (used by the debug visualizer + // so one pooled template can render any category colour). Set before Add so + // the create packet carries the right display id. + if (displayId) + pGameObj->SetDisplayId(displayId); + if (scale > 0.0f) + pGameObj->SetObjectScale(scale); + map->Add(pGameObj); pGameObj->AIM_Initialize(); @@ -2996,6 +3004,21 @@ void WorldObject::PlayDirectSound(uint32 sound_id, Player const* target /*= NULL } } +void WorldObject::PlaySpellVisual(uint32 kitId, Player const* target /*= NULL*/) const +{ + WorldPacket data(SMSG_PLAY_SPELL_VISUAL, 8 + 4); + data << GetObjectGuid(); + data << uint32(kitId); // index from SpellVisualKit.dbc + if (target) + { + target->SendDirectMessage(&data); + } + else + { + SendMessageToSet(&data, true); + } +} + /** * @brief Plays music for one player or nearby players. * diff --git a/src/game/Object/Object.h b/src/game/Object/Object.h index 307a9ddad..ae17e7ff0 100644 --- a/src/game/Object/Object.h +++ b/src/game/Object/Object.h @@ -904,6 +904,10 @@ class WorldObject : public Object void PlayDistanceSound(uint32 sound_id, Player const* target = NULL) const; void PlayDirectSound(uint32 sound_id, Player const* target = NULL) const; void PlayMusic(uint32 sound_id, Player const* target = NULL) const; + // Play a SpellVisualKit on this object (SMSG_PLAY_SPELL_VISUAL). Broadcast + // to nearby players, or to a single target if given. Used by the debug + // visualizer for dynamic, on-unit visual cues. + void PlaySpellVisual(uint32 kitId, Player const* target = NULL) const; void SendObjectDeSpawnAnim(ObjectGuid guid); @@ -933,7 +937,7 @@ class WorldObject : public Object void BuildUpdateData(UpdateDataMapType&) override; Creature* SummonCreature(uint32 id, float x, float y, float z, float ang, TempSpawnType spwtype, uint32 despwtime, bool asActiveObject = false, bool setRun = false); - GameObject* SummonGameObject(uint32 id, float x, float y, float z, float angle, uint32 despwtime); + GameObject* SummonGameObject(uint32 id, float x, float y, float z, float angle, uint32 despwtime, uint32 displayId = 0, float scale = 0.0f); bool IsActiveObject() const { return m_isActiveObject || m_viewPoint.hasViewers(); } bool isActiveObject() const { return IsActiveObject(); } // This is for Eluna to build. Should be removed in the future! diff --git a/src/game/WorldHandlers/Chat.cpp b/src/game/WorldHandlers/Chat.cpp index 374cb18a7..2c822535b 100644 --- a/src/game/WorldHandlers/Chat.cpp +++ b/src/game/WorldHandlers/Chat.cpp @@ -241,9 +241,23 @@ ChatCommand* ChatHandler::getCommandTable() { NULL, 0, false, NULL, "", NULL } }; + static ChatCommand debugVisCommandTable[] = + { + { "cells", SEC_GAMEMASTER, false, &ChatHandler::HandleDebugVisCellsCommand, "", NULL }, + { "los", SEC_GAMEMASTER, false, &ChatHandler::HandleDebugVisLosCommand, "", NULL }, + { "path", SEC_GAMEMASTER, false, &ChatHandler::HandleDebugVisPathCommand, "", NULL }, + { "collision", SEC_GAMEMASTER, false, &ChatHandler::HandleDebugVisCollisionCommand, "", NULL }, + { "height", SEC_GAMEMASTER, false, &ChatHandler::HandleDebugVisHeightCommand, "", NULL }, + { "clear", SEC_GAMEMASTER, false, &ChatHandler::HandleDebugVisClearCommand, "", NULL }, + { NULL, 0, false, NULL, "", NULL } + }; + static ChatCommand debugCommandTable[] = { { "anim", SEC_GAMEMASTER, false, &ChatHandler::HandleDebugAnimCommand, "", NULL }, + { "vis", SEC_GAMEMASTER, false, NULL, "", debugVisCommandTable }, + { "perf", SEC_GAMEMASTER, true, &ChatHandler::HandleDebugPerfCommand, "", NULL }, + { "visual", SEC_GAMEMASTER, false, &ChatHandler::HandleDebugVisualCommand, "", NULL }, { "bg", SEC_ADMINISTRATOR, false, &ChatHandler::HandleDebugBattlegroundCommand, "", NULL }, { "getitemstate", SEC_ADMINISTRATOR, false, &ChatHandler::HandleDebugGetItemStateCommand, "", NULL }, { "lootrecipient", SEC_GAMEMASTER, false, &ChatHandler::HandleDebugGetLootRecipientCommand, "", NULL }, diff --git a/src/game/WorldHandlers/Chat.h b/src/game/WorldHandlers/Chat.h index 3dd0a7a9b..afb4683e1 100644 --- a/src/game/WorldHandlers/Chat.h +++ b/src/game/WorldHandlers/Chat.h @@ -286,6 +286,14 @@ class ChatHandler bool HandleDebugAnimCommand(char* args); bool HandleDebugBattlegroundCommand(char* args); + bool HandleDebugVisCellsCommand(char* args); + bool HandleDebugVisLosCommand(char* args); + bool HandleDebugVisPathCommand(char* args); + bool HandleDebugVisCollisionCommand(char* args); + bool HandleDebugVisHeightCommand(char* args); + bool HandleDebugVisClearCommand(char* args); + bool HandleDebugPerfCommand(char* args); + bool HandleDebugVisualCommand(char* args); bool HandleDebugGetItemStateCommand(char* args); bool HandleDebugGetItemValueCommand(char* args); bool HandleDebugGetLootRecipientCommand(char* args); diff --git a/src/game/WorldHandlers/QueryHandler.cpp b/src/game/WorldHandlers/QueryHandler.cpp index f7f4f434a..2eecd400f 100644 --- a/src/game/WorldHandlers/QueryHandler.cpp +++ b/src/game/WorldHandlers/QueryHandler.cpp @@ -52,6 +52,7 @@ #include "Player.h" #include "NPCHandler.h" #include "SQLStorages.h" +#include "DebugVis.h" /** * @brief Sends an in-memory name query response for a player. @@ -262,6 +263,14 @@ void WorldSession::HandleGameObjectQueryOpcode(WorldPacket& recv_data) } } } + // Debug visualizer: pooled marker entries carry a per-instance tooltip so + // mousing over a marker shows that marker's own captured debug values. + if (DebugVis::IsDebugEntry(entryID)) + { + std::string dbgLabel; + if (DebugVis::GetEntryLabel(entryID, dbgLabel)) + Name = dbgLabel; + } DETAIL_LOG("WORLD: CMSG_GAMEOBJECT_QUERY '%s' - Entry: %u. ", info->name, entryID); WorldPacket data(SMSG_GAMEOBJECT_QUERY_RESPONSE, 150); data << uint32(entryID); diff --git a/src/game/WorldHandlers/World.cpp b/src/game/WorldHandlers/World.cpp index f09c406de..818fba5ed 100644 --- a/src/game/WorldHandlers/World.cpp +++ b/src/game/WorldHandlers/World.cpp @@ -64,6 +64,7 @@ #include "LootMgr.h" #include "ItemEnchantmentMgr.h" #include "MapManager.h" +#include "PerformanceMonitor.h" #include "ScriptMgr.h" #include "CreatureAIRegistry.h" #include "ProgressBar.h" @@ -614,6 +615,8 @@ void World::LoadConfigSettings(bool reload) setConfig(CONFIG_BOOL_GRID_UNLOAD, "GridUnload", true); setConfig(CONFIG_UINT32_MAX_WHOLIST_RETURNS, "MaxWhoListReturns", 49); setConfig(CONFIG_UINT32_AUTOBROADCAST_INTERVAL, "AutoBroadcast", 600); + // Server-side debug-draw toolkit (.debug vis): marker auto-despawn seconds. + setConfigMinMax(CONFIG_UINT32_DEBUGVIS_DESPAWN, "DebugVis.DespawnSeconds", 30, 5, 600); if (getConfig(CONFIG_UINT32_AUTOBROADCAST_INTERVAL) > 0) { @@ -1790,6 +1793,9 @@ void World::DetectDBCLang() /// Update the World ! void World::Update(uint32 diff) { + ///- Record world-tick timing for the performance monitor (.debug perf) + PerformanceMonitor::TrackUpdate(diff); + ///- Update the different timers for (int i = 0; i < WUPDATE_COUNT; ++i) { diff --git a/src/game/WorldHandlers/World.h b/src/game/WorldHandlers/World.h index 1032e4309..3b37bb82c 100644 --- a/src/game/WorldHandlers/World.h +++ b/src/game/WorldHandlers/World.h @@ -215,6 +215,8 @@ enum eConfigUInt32Values CONFIG_UINT32_PLAYERBOT_MINBOTLEVEL, #endif CONFIG_UINT32_AUTOBROADCAST_INTERVAL, + // Server-side debug visualization toolkit (.debug vis): marker lifetime. + CONFIG_UINT32_DEBUGVIS_DESPAWN, CONFIG_UINT32_VALUE_COUNT }; diff --git a/src/mangosd/mangosd.conf.dist.in b/src/mangosd/mangosd.conf.dist.in index 9d4705aa8..acfd0fe19 100644 --- a/src/mangosd/mangosd.conf.dist.in +++ b/src/mangosd/mangosd.conf.dist.in @@ -1944,3 +1944,53 @@ Eluna.CompatibilityMode = false Eluna.OnlyOnMaps = "" Eluna.TraceBack = false Eluna.ScriptPath = "lua_scripts" + +################################################################################################### +# SERVER-SIDE DEBUG-DRAW TOOLKIT (.debug vis) +# +# Engine-style debug markers spawned server-side and visible to the GM, for +# map cells, line-of-sight, pathfinding, collision and terrain height. Markers +# are temporary gameobjects that auto-despawn; hovering a marker shows that +# marker's captured values. Requires the reserved gameobject_template pool from +# src/game/DebugVis/sql/debugvis_marker_pool.sql to be applied once. +# +# DebugVis.DespawnSeconds +# Marker lifetime in seconds (5-600). Markers also clear with ".debug vis clear". +# DebugVis.Style +# 0 = colour-coded crystals (default), 1 = assorted-crystal alternate set. +# DebugVis.Disp. +# Override the GameObjectDisplayInfo.dbc display id per category +# (Cell/LosOk/LosBlock/Path/PathBad/Collision/Height/HitPoint/Generic). +# NOTE: only SOLID models are hover-able; particle/effect models render but +# cannot be moused over. +# DebugVis.Glow / DebugVis.GlowDisp. +# Optional colour-matched glow effect co-located with each marker (1 = on). +# DebugVis.Scale[.Collision|.HitPoint] +# Per-marker scale; the collision/impact crystals render large, so they are +# shrunk by default. +################################################################################################### + +DebugVis.DespawnSeconds = 30 +DebugVis.Style = 0 +#DebugVis.Disp.Cell = 5912 +#DebugVis.Disp.LosOk = 2972 +#DebugVis.Disp.LosBlock = 2973 +#DebugVis.Disp.Path = 2972 +#DebugVis.Disp.PathBad = 2973 +#DebugVis.Disp.Collision = 1667 +#DebugVis.Disp.Height = 2974 +#DebugVis.Disp.HitPoint = 5746 +#DebugVis.Disp.Generic = 5811 +DebugVis.Glow = 1 +#DebugVis.GlowDisp.Cell = 0 +#DebugVis.GlowDisp.LosOk = 3993 +#DebugVis.GlowDisp.LosBlock = 327 +#DebugVis.GlowDisp.Path = 3993 +#DebugVis.GlowDisp.PathBad = 327 +#DebugVis.GlowDisp.Collision = 363 +#DebugVis.GlowDisp.Height = 266 +#DebugVis.GlowDisp.HitPoint = 6430 +#DebugVis.GlowDisp.Generic = 6679 +#DebugVis.Scale = 1.0 +#DebugVis.Scale.Collision = 0.5 +#DebugVis.Scale.HitPoint = 0.6