feat(waypoint-panel): Add waypoint visualization, teleport, and highlighting

- Add split view layout for waypoint panel (list top, details bottom)
- Add Teleport button to teleport to selected waypoint with correct orientation
- Add waypoint marker highlighting (scales up selected marker)
- Add GM button sync on panel show
- Fix waypoint panel scrollbar and content visibility issues
- Add HighlightWaypointMarker and ClearWaypointMarkerAuras C++ functions
- Add TELEPORT_TO_WAYPOINT server handler
- Fix creature visual updates requiring client refresh (DestroyForNearbyPlayers)
- Add Smallfolk serialization for shared data in admin_handlers
This commit is contained in:
2025-11-30 16:09:39 -05:00
parent ad73d8d761
commit 090f5cbd5f
8 changed files with 898 additions and 49 deletions

View File

@@ -0,0 +1,188 @@
# Waypoint Detail Panel Implementation
**Date:** November 30, 2025
**Status:** Complete - Ready for Testing
**Requires:** Server rebuild (C++ changes)
## Overview
Implemented comprehensive waypoint visualization and inspection system allowing admins to:
- View all waypoints for a creature path in a clickable list
- See detailed information about each waypoint (position, orientation, delay, etc.)
- Click waypoints in the list to highlight them in the world
- Target waypoint markers in the world to auto-select them in the UI
- Keep NPC panel focused while inspecting waypoints
## Architecture
### Client-Server Communication
**New AMS Handlers (Server → Client):**
- `GET_WAYPOINT_DETAILS` - Request all nodes for a path
- `SELECT_WAYPOINT` - Highlight a specific waypoint
- `GET_WAYPOINT_FOR_GUID` - Lookup waypoint from creature GUID
**New AMS Responses (Client ← Server):**
- `WAYPOINT_DETAILS_RESPONSE` - Array of waypoint nodes with full data
- `WAYPOINT_SELECTED_RESPONSE` - Confirmation of waypoint selection
- `WAYPOINT_FOR_GUID_RESPONSE` - Path/node IDs for a visual waypoint GUID
### C++ Bindings
**New WaypointManager Methods:**
```cpp
bool GetPathAndNodeByVisualGUID(ObjectGuid guid, uint32& outPathId, uint32& outNodeId) const;
```
**New Global Lua Function:**
```lua
pathId, nodeId = GetWaypointNodeForVisualGUID(guidLow)
```
## UI Components
### Waypoints Tab Layout
```
┌─────────────────────────────────────┐
│ Waypoint List (Top 50%) │
├─────────────────────────────────────┤
│ Waypoint Details (Bottom 50%) │
│ [Deselect] │
└─────────────────────────────────────┘
```
**List Features:**
- Clickable node entries (Node 1, Node 2, etc.)
- Highlighted selected node (green background)
- Scrollable for paths with many nodes
- Auto-populated when "Show Waypoints" is clicked
**Detail Panel Features:**
- Node ID and detailed information
- Position (X, Y, Z coordinates)
- Orientation angle
- Delay in milliseconds
- Move type and action IDs
- Deselect button to close detail view
## Implementation Details
### Panel Locking
When waypoints are shown:
1. `panelLockedToNPC` flag is set to `true`
2. NPC panel data remains loaded even when targeting other creatures
3. Targeting waypoint markers doesn't unload NPC info
4. Flag is cleared when "Hide Waypoints" is clicked
### Waypoint Target Detection
When panel is locked and player targets a creature:
1. Extract GUID from target
2. Send `GET_WAYPOINT_FOR_GUID` to server
3. Server looks up path/node IDs using `GetPathAndNodeByVisualGUID`
4. Client receives response and auto-selects waypoint in list
5. Waypoints tab automatically switches to show detail
### Data Flow
```
User clicks "Show Waypoints"
SHOW_WAYPOINTS sent to server
Server visualizes path (spawns markers)
WAYPOINTS_RESPONSE received
GET_WAYPOINT_DETAILS sent to server
WAYPOINT_DETAILS_RESPONSE received with all nodes
Waypoints tab populated with clickable list
User clicks waypoint in list
Detail panel shows (split view)
User targets waypoint marker in world
GET_WAYPOINT_FOR_GUID sent to server
WAYPOINT_FOR_GUID_RESPONSE received
Waypoint auto-selected in list, detail shown
```
## Files Modified
### C++ (Requires Rebuild)
**`WaypointManager.h`**
- Added `GetPathAndNodeByVisualGUID()` declaration
**`WaypointManager.cpp`**
- Implemented `GetPathAndNodeByVisualGUID()` to lookup path/node from visual creature GUID
**`GlobalMethods.h`**
- Added `GetWaypointNodeForVisualGUID()` Lua binding
- Registered in method table
### Server Lua
**`admin_handlers.lua`**
- Added `GET_WAYPOINT_DETAILS` handler - returns array of waypoint nodes
- Added `SELECT_WAYPOINT` handler - highlights waypoint in world
- Added `GET_WAYPOINT_FOR_GUID` handler - looks up path/node from creature GUID
### Client Lua
**`NPCInfoPanel.lua`**
- Implemented waypoint list UI with scrollable frame
- Implemented waypoint detail panel with split view
- Added `UpdateWaypointList()` and `UpdateWaypointDetail()` functions
- Added `RequestWaypointDetails()` to fetch data from server
- Added `WAYPOINT_DETAILS_RESPONSE` handler
- Added `WAYPOINT_FOR_GUID_RESPONSE` handler
- Integrated panel locking when showing waypoints
- Enhanced `PLAYER_TARGET_CHANGED` to detect waypoint markers
- Added waypoint detail request when showing waypoints
## Testing Checklist
- [ ] Server compiles without errors
- [ ] Server loads admin_handlers.lua successfully
- [ ] Target NPC with waypoints
- [ ] Click "Show Waypoints" button
- [ ] Waypoints tab populates with node list
- [ ] Click a waypoint in the list
- [ ] Detail panel shows with waypoint information
- [ ] Target a waypoint marker in the world
- [ ] Waypoint auto-selects in the list
- [ ] NPC panel data remains loaded during waypoint inspection
- [ ] Click "Hide Waypoints" to deactivate
- [ ] Panel lock is cleared
## Known Limitations
- Visual spell effects for waypoint highlighting not yet implemented (placeholder)
- Waypoint modification/creation not yet implemented
- No persistence of waypoint data between sessions (by design)
## Future Enhancements
1. Add visual spell effects to highlight selected waypoints
2. Implement waypoint creation/editing UI
3. Add waypoint deletion functionality
4. Persist waypoint inspection state in SavedVariables
5. Add waypoint search/filter functionality
6. Implement waypoint path visualization (lines between nodes)
## Notes for Developer
- The `panelLockedToNPC` flag is crucial for keeping NPC data loaded
- GUID extraction from `UnitGUID()` uses pattern matching - ensure it matches creature GUID format
- Waypoint nodes are 1-indexed in the UI but use their actual IDs from the database
- The split view resizes dynamically - ensure scroll frames handle height changes
- Always call `UpdateWaypointList()` after `UpdateWaypointDetail()` to refresh highlighting

View File

@@ -91,6 +91,24 @@ local function UpdateGMButton()
end
end
-- Function to sync GM state from server
local function SyncGMState()
if not AMS then return end
-- Request current GM state from server
AMS.Send("GET_PLAYER_DATA", {})
end
-- Handler for player data response
if AMS then
AMS.RegisterHandler("PLAYER_DATA_RESPONSE", function(data)
if data and data.success and data.isGM ~= nil then
_G.AraxiaTrinityAdminGMMode = data.isGM
UpdateGMButton()
end
end)
end
gmButton:SetScript("OnClick", function()
-- Toggle GM mode
_G.AraxiaTrinityAdminGMMode = not _G.AraxiaTrinityAdminGMMode
@@ -117,6 +135,14 @@ end)
UpdateGMButton()
-- Sync GM state on panel show
npcPanel:HookScript("OnShow", function()
SyncGMState()
end)
-- Initial sync
SyncGMState()
-- ============================================================================
-- Tab Buttons
-- ============================================================================
@@ -385,17 +411,260 @@ local waypointContent = CreateFrame("Frame", nil, rightContentFrame)
waypointContent:SetAllPoints(rightContentFrame)
waypointContent:Hide()
local waypointTitle = waypointContent:CreateFontString(nil, "OVERLAY", "GameFontNormal")
waypointTitle:SetPoint("TOP", waypointContent, "TOP", 0, -8)
waypointTitle:SetText("Waypoint Editor")
-- Waypoint data storage
local currentWaypointPathId = nil
local currentWaypointNodes = {}
local selectedWaypointNodeId = nil
local panelLockedToNPC = false -- Lock panel to NPC when showing waypoints
-- Waypoint info text area
local waypointInfoText = waypointContent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
waypointInfoText:SetPoint("TOPLEFT", waypointContent, "TOPLEFT", 12, -35)
waypointInfoText:SetPoint("TOPRIGHT", waypointContent, "TOPRIGHT", -12, -35)
waypointInfoText:SetJustifyH("LEFT")
waypointInfoText:SetJustifyV("TOP")
waypointInfoText:SetText("|cFF888888No creature selected.|r\n\nSelect a creature to view waypoint options.")
-- Waypoint list (top half - 60% of height)
local waypointListContainer = CreateFrame("Frame", nil, waypointContent)
waypointListContainer:SetPoint("TOPLEFT", waypointContent, "TOPLEFT", 8, -8)
waypointListContainer:SetPoint("RIGHT", waypointContent, "RIGHT", -8, 0)
waypointListContainer:SetHeight(200)
local waypointListScroll = CreateFrame("ScrollFrame", nil, waypointListContainer, "UIPanelScrollFrameTemplate")
waypointListScroll:SetPoint("TOPLEFT", waypointListContainer, "TOPLEFT", 0, 0)
waypointListScroll:SetPoint("BOTTOMRIGHT", waypointListContainer, "BOTTOMRIGHT", -22, 0)
local waypointListChild = CreateFrame("Frame", nil, waypointListScroll)
waypointListChild:SetWidth(280)
waypointListChild:SetHeight(100)
waypointListScroll:SetScrollChild(waypointListChild)
-- Divider line between list and detail
local waypointDivider = waypointContent:CreateTexture(nil, "ARTWORK")
waypointDivider:SetHeight(1)
waypointDivider:SetPoint("TOPLEFT", waypointListContainer, "BOTTOMLEFT", 0, -4)
waypointDivider:SetPoint("RIGHT", waypointContent, "RIGHT", -8, 0)
waypointDivider:SetColorTexture(0.4, 0.4, 0.4, 0.8)
-- Detail panel (bottom half - always visible when waypoint selected)
local waypointDetailPanel = CreateFrame("Frame", nil, waypointContent, "BackdropTemplate")
waypointDetailPanel:SetPoint("TOPLEFT", waypointDivider, "BOTTOMLEFT", 0, -4)
waypointDetailPanel:SetPoint("BOTTOMRIGHT", waypointContent, "BOTTOMRIGHT", -8, 8)
waypointDetailPanel:SetBackdrop({
bgFile = "Interface/Tooltips/UI-Tooltip-Background",
edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 12,
insets = { left = 3, right = 3, top = 3, bottom = 3 }
})
waypointDetailPanel:SetBackdropColor(0.1, 0.1, 0.1, 0.8)
waypointDetailPanel:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
-- Detail panel content
local detailTitle = waypointDetailPanel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
detailTitle:SetPoint("TOPLEFT", waypointDetailPanel, "TOPLEFT", 10, -8)
detailTitle:SetText("Select a waypoint above")
-- Detail info text
local detailInfoText = waypointDetailPanel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
detailInfoText:SetPoint("TOPLEFT", waypointDetailPanel, "TOPLEFT", 10, -26)
detailInfoText:SetPoint("RIGHT", waypointDetailPanel, "RIGHT", -80, 0)
detailInfoText:SetJustifyH("LEFT")
detailInfoText:SetJustifyV("TOP")
detailInfoText:SetText("")
-- Action buttons container (right side of detail panel)
local detailActionsContainer = CreateFrame("Frame", nil, waypointDetailPanel)
detailActionsContainer:SetPoint("TOPRIGHT", waypointDetailPanel, "TOPRIGHT", -8, -8)
detailActionsContainer:SetSize(65, 100)
-- Teleport button
local teleportBtn = CreateFrame("Button", nil, detailActionsContainer, "UIPanelButtonTemplate")
teleportBtn:SetSize(60, 22)
teleportBtn:SetPoint("TOP", detailActionsContainer, "TOP", 0, 0)
teleportBtn:SetText("Teleport")
teleportBtn:Disable()
teleportBtn:SetScript("OnClick", function()
if not selectedWaypointNodeId or not currentWaypointPathId then return end
-- Find the selected node data
local node = nil
for _, n in ipairs(currentWaypointNodes) do
if n.id == selectedWaypointNodeId then
node = n
break
end
end
if node and AMS then
print("|cFF00FF00[ATA]|r Teleporting to waypoint " .. node.id)
AMS.Send("TELEPORT_TO_WAYPOINT", {
x = node.x,
y = node.y,
z = node.z,
orientation = node.orientation
})
end
end)
teleportBtn:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_LEFT")
GameTooltip:AddLine("Teleport to Waypoint", 1, 1, 1)
GameTooltip:AddLine("Teleports you to this waypoint location", 0.7, 0.7, 0.7)
GameTooltip:Show()
end)
teleportBtn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
-- Buttons to hold waypoint node entries
local waypointNodeButtons = {}
-- Forward declare functions
local UpdateWaypointList
local UpdateWaypointDetail
-- Function to update waypoint detail display
UpdateWaypointDetail = function()
if not selectedWaypointNodeId then
detailTitle:SetText("Select a waypoint above")
detailInfoText:SetText("")
teleportBtn:Disable()
return
end
local node = nil
for _, n in ipairs(currentWaypointNodes) do
if n.id == selectedWaypointNodeId then
node = n
break
end
end
if not node then
teleportBtn:Disable()
return
end
detailTitle:SetText("Node " .. node.id)
local detailStr = string.format(
"|cFFFFD700Position:|r %.1f, %.1f, %.1f\n" ..
"|cFFFFD700Orientation:|r %.2f\n" ..
"|cFFFFD700Delay:|r %dms\n" ..
"|cFFFFD700Move Type:|r %d",
node.x, node.y, node.z,
node.orientation,
node.delay,
node.moveType
)
detailInfoText:SetText(detailStr)
teleportBtn:Enable()
end
-- Function to update waypoint list display
UpdateWaypointList = function()
print("|cFF00FF00[ATA]|r UpdateWaypointList called: pathId=" .. (currentWaypointPathId or 0) .. ", nodes=" .. #currentWaypointNodes)
-- Hide all existing buttons
for _, btn in ipairs(waypointNodeButtons) do
btn:Hide()
end
if not currentWaypointPathId or #currentWaypointNodes == 0 then
-- Show empty message
print("|cFFFF0000[ATA]|r No waypoint data to display")
waypointListChild:SetHeight(40)
return
end
-- Create/update buttons for each node
for i, node in ipairs(currentWaypointNodes) do
local btn = waypointNodeButtons[i]
if not btn then
btn = CreateFrame("Button", nil, waypointListChild)
btn:SetHeight(24)
btn:EnableMouse(true)
btn:RegisterForClicks("AnyUp")
btn:SetHighlightTexture("Interface/QuestFrame/UI-QuestTitleHighlight", "ADD")
btn.bg = btn:CreateTexture(nil, "BACKGROUND")
btn.bg:SetAllPoints()
btn.bg:SetColorTexture(0.2, 0.2, 0.2, 0.5)
btn.text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
btn.text:SetPoint("LEFT", btn, "LEFT", 8, 0)
btn.text:SetJustifyH("LEFT")
btn:SetScript("OnClick", function(self)
local clickedNode = self.nodeData
if not clickedNode then return end
selectedWaypointNodeId = clickedNode.id
UpdateWaypointDetail()
UpdateWaypointList() -- Refresh to show selection highlight
-- Send SELECT_WAYPOINT to server to highlight in world
if AMS and currentWaypointPathId then
print("|cFF00FF00[ATA]|r Sending SELECT_WAYPOINT: path=" .. currentWaypointPathId .. " node=" .. clickedNode.id)
AMS.Send("SELECT_WAYPOINT", { pathId = currentWaypointPathId, nodeId = clickedNode.id })
end
end)
waypointNodeButtons[i] = btn
end
btn.nodeData = node
btn:ClearAllPoints()
btn:SetPoint("TOPLEFT", waypointListChild, "TOPLEFT", 0, -((i-1) * 26))
btn:SetPoint("RIGHT", waypointListChild, "RIGHT", 0, 0)
-- Highlight selected node
if node.id == selectedWaypointNodeId then
btn.bg:SetColorTexture(0.3, 0.5, 0.3, 0.7)
btn.text:SetTextColor(1, 1, 1, 1)
else
btn.bg:SetColorTexture(0.2, 0.2, 0.2, 0.5)
btn.text:SetTextColor(1, 0.82, 0, 1)
end
btn.text:SetText("Node " .. node.id)
btn:Show()
end
-- Update scroll child size to fit all buttons
local totalHeight = #currentWaypointNodes * 26
waypointListChild:SetHeight(totalHeight)
print("|cFF00FF00[ATA]|r Set scroll child height to " .. totalHeight)
end
-- Deselect button no longer needed in split view layout
-- (kept for code compatibility but hidden)
-- Request waypoint details from server
local function RequestWaypointDetails(pathId)
if not AMS then return end
currentWaypointPathId = pathId
currentWaypointNodes = {}
selectedWaypointNodeId = nil
AMS.Send("GET_WAYPOINT_DETAILS", { pathId = pathId })
end
-- Handler for waypoint details response
if AMS then
AMS.RegisterHandler("WAYPOINT_DETAILS_RESPONSE", function(data)
if not data or not data.success then
print("|cFFFF0000[ATA]|r Failed to get waypoint details: " .. (data.error or "Unknown error"))
return
end
print("|cFF00FF00[ATA]|r Received waypoint details: pathId=" .. (data.pathId or 0) .. ", nodes=" .. #(data.nodes or {}))
currentWaypointPathId = data.pathId
currentWaypointNodes = data.nodes or {}
selectedWaypointNodeId = nil
print("|cFF00FF00[ATA]|r Updating waypoint list with " .. #currentWaypointNodes .. " nodes")
UpdateWaypointList()
end)
end
-- Waypoint action buttons container
local waypointButtonContainer = CreateFrame("Frame", nil, waypointContent)
@@ -1053,6 +1322,12 @@ function npcPanel:Update(requestServerData, forceRefresh)
-- Enable waypoint button if creature has a waypoint path
if data and data.movement and data.movement.waypointPath then
waypointButton:Enable()
-- Auto-load waypoint details for the Waypoints tab
local pathId = data.movement.waypointPath.pathId
if pathId and pathId > 0 then
RequestWaypointDetails(pathId)
end
else
waypointButton:Disable()
end
@@ -1072,40 +1347,8 @@ function npcPanel:Update(requestServerData, forceRefresh)
npcModel:SetModel("interface/buttons/talktomequestionmark.m2")
end
-- Update Waypoint tab content
if npcData then
local waypointLines = {}
table.insert(waypointLines, "|cFFFFD700" .. (npcData.name or "Unknown") .. "|r")
table.insert(waypointLines, "Entry: " .. (npcData.npcID or "?"))
table.insert(waypointLines, "")
if serverData and serverData.movement and serverData.movement.waypointPath then
local wp = serverData.movement.waypointPath
local pathId = wp.pathId or 0
if pathId > 0 then
table.insert(waypointLines, "|cFF00FF00Has Waypoint Path|r")
table.insert(waypointLines, "Path ID: " .. pathId)
table.insert(waypointLines, "Nodes: " .. (wp.nodeCount or 0))
table.insert(waypointLines, "")
table.insert(waypointLines, "|cFF888888Use the 'Show Waypoints' button|r")
table.insert(waypointLines, "|cFF888888to visualize the path in 3D.|r")
else
table.insert(waypointLines, "|cFFFF8800No Waypoint Path|r")
table.insert(waypointLines, "")
table.insert(waypointLines, "|cFF888888This creature does not have|r")
table.insert(waypointLines, "|cFF888888a defined waypoint path.|r")
end
elseif isLoadingServerData then
table.insert(waypointLines, "|cFF888888Loading waypoint data...|r")
else
table.insert(waypointLines, "|cFF888888Server data not loaded.|r")
table.insert(waypointLines, "|cFF888888Click Refresh to load.|r")
end
waypointInfoText:SetText(table.concat(waypointLines, "\n"))
else
waypointInfoText:SetText("|cFF888888No creature selected.|r\n\nSelect a creature to view waypoint options.")
end
-- Waypoint tab is now handled by the new list/detail UI
-- No additional update needed here
end
-- ============================================================================
@@ -1163,12 +1406,14 @@ waypointButton:SetScript("OnClick", function()
print("|cFF00FF00[ATA]|r Hiding waypoint markers...")
waypointButton:SetText("Working...")
waypointButton:Disable()
panelLockedToNPC = false
AMS.Send("HIDE_WAYPOINTS", { guid = npcData.guid })
else
-- Show waypoints
-- Show waypoints and lock panel to this NPC
print("|cFF00FF00[ATA]|r Spawning waypoint markers in 3D space...")
waypointButton:SetText("Working...")
waypointButton:Disable()
panelLockedToNPC = true -- Lock panel to prevent target changes from unloading NPC data
AMS.Send("SHOW_WAYPOINTS", { guid = npcData.guid })
end
end)
@@ -1198,6 +1443,11 @@ local function InitWaypointHandler()
nodeCount = pendingWaypointRequest.nodeCount
}
print("|cFF00FF00[ATA]|r Tracking path for: " .. pendingWaypointRequest.name)
-- Request waypoint details for the Waypoints tab
if data.pathId then
RequestWaypointDetails(data.pathId)
end
elseif pendingWaypointRequest.action == "hide" then
-- Remove from tracked paths
if trackedPaths[pendingWaypointRequest.guid] then
@@ -1223,6 +1473,19 @@ local updateFrame = CreateFrame("Frame")
updateFrame:RegisterEvent("PLAYER_TARGET_CHANGED")
updateFrame:SetScript("OnEvent", function()
if npcPanel:IsShown() then
-- Check if targeting a waypoint marker (visual waypoint creature)
local targetGUID = UnitGUID("target")
if targetGUID and panelLockedToNPC then
-- Panel is locked to an NPC, check if we targeted a waypoint marker
local guidLow = tonumber(string.match(targetGUID, "Creature%-0%-0%-0%-(%d+)")) or 0
if guidLow > 0 and AMS then
-- Request waypoint info for this GUID
AMS.Send("GET_WAYPOINT_FOR_GUID", { guid = guidLow })
return -- Don't update NPC panel
end
end
-- Normal NPC targeting behavior
serverData = nil
isLoadingServerData = false
@@ -1240,10 +1503,40 @@ updateFrame:SetScript("OnEvent", function()
currentTargetGUID = npcData and npcData.guid or nil
end
waypointButton:Disable() -- Will be enabled by Update() if creature has waypoints
npcPanel:Update(true)
npcPanel:Update(true, true) -- Request server data and force refresh from cache
end
end)
-- Handler for waypoint selection response (with visual highlighting)
if AMS then
AMS.RegisterHandler("WAYPOINT_SELECTED_RESPONSE", function(data)
if data and data.success then
print("|cFF00FF00[ATA]|r Waypoint selected: node " .. data.nodeId .. " at " .. data.x .. ", " .. data.y .. ", " .. data.z)
-- The waypoint is already visualized as a creature marker in the world
-- The highlighting happens through the UI selection (green background on the button)
-- Future: Could add a spell effect or other visual indicator here
end
end)
AMS.RegisterHandler("WAYPOINT_FOR_GUID_RESPONSE", function(data)
if data and data.success then
-- Select this waypoint in the Waypoints tab
selectedWaypointNodeId = data.nodeId
currentWaypointPathId = data.pathId
-- Switch to Waypoints tab to show the detail
ShowRightTab("Waypoints")
-- Update the display
UpdateWaypointList()
UpdateWaypointDetail()
print("|cFF00FF00[ATA]|r Selected waypoint node " .. data.nodeId)
end
end)
end
-- ============================================================================
-- Initialization
-- ============================================================================

View File

@@ -13,6 +13,9 @@ if not AMS then
return
end
-- Load Smallfolk for serialization (used by shared data)
local Smallfolk = require("smallfolk")
-- ============================================================================
-- Helper Functions
-- ============================================================================
@@ -371,6 +374,159 @@ AMS.RegisterHandler("CLEAR_ALL_WAYPOINT_MARKERS", function(player, data)
AMS.Send(player, "CLEAR_WAYPOINTS_RESPONSE", { success = true })
end)
-- Get detailed waypoint data for a path
AMS.RegisterHandler("GET_WAYPOINT_DETAILS", function(player, data)
if not data or not data.pathId then
print("[Admin Handlers] GET_WAYPOINT_DETAILS: No pathId provided")
AMS.Send(player, "WAYPOINT_DETAILS_RESPONSE", { success = false, error = "No pathId" })
return
end
local pathId = data.pathId
print("[Admin Handlers] GET_WAYPOINT_DETAILS: Fetching details for path " .. pathId)
-- Get the waypoint path using the C++ binding
local path = GetWaypointPath(pathId)
if not path or not path.nodes then
print("[Admin Handlers] GET_WAYPOINT_DETAILS: Path not found")
AMS.Send(player, "WAYPOINT_DETAILS_RESPONSE", { success = false, error = "Path not found" })
return
end
print("[Admin Handlers] GET_WAYPOINT_DETAILS: Sending " .. #path.nodes .. " nodes")
AMS.Send(player, "WAYPOINT_DETAILS_RESPONSE", {
success = true,
pathId = pathId,
nodes = path.nodes
})
end)
-- Highlight a specific waypoint in the world
AMS.RegisterHandler("SELECT_WAYPOINT", function(player, data)
if not data or not data.pathId or not data.nodeId then
print("[Admin Handlers] SELECT_WAYPOINT: Missing pathId or nodeId")
return
end
local pathId = data.pathId
local nodeId = data.nodeId
print("[Admin Handlers] SELECT_WAYPOINT: Highlighting path " .. pathId .. " node " .. nodeId)
-- Get the waypoint path
local path = GetWaypointPath(pathId)
if not path or not path.nodes then
print("[Admin Handlers] SELECT_WAYPOINT: Path not found")
return
end
-- Find the node
local node = nil
for _, n in ipairs(path.nodes) do
if n.id == nodeId then
node = n
break
end
end
if not node then
print("[Admin Handlers] SELECT_WAYPOINT: Node not found")
return
end
-- Notify player of selection
print("[Admin Handlers] SELECT_WAYPOINT: Node " .. nodeId .. " at " .. node.x .. ", " .. node.y .. ", " .. node.z)
-- Clear previous highlight if any (stored in shared data per player)
local playerKey = "waypoint_highlight_" .. tostring(player:GetGUIDLow())
local prevData = GetSharedData(playerKey)
if prevData then
local prev = Smallfolk.loads(prevData)
if prev and prev.pathId and prev.nodeId then
ClearWaypointMarkerAuras(player, prev.pathId, prev.nodeId)
end
end
-- Highlight the new waypoint marker by changing its display
-- Display IDs to try: 17519 (red orb), 17188 (blue orb), 26754 (green flame), 11686 (purple crystal)
local highlightDisplayId = 17519 -- Red glowing orb - very visible
local success = HighlightWaypointMarker(player, pathId, nodeId, highlightDisplayId)
if success then
print("[Admin Handlers] SELECT_WAYPOINT: Highlighted marker with displayId " .. highlightDisplayId)
-- Store current selection for later cleanup
SetSharedData(playerKey, Smallfolk.dumps({ pathId = pathId, nodeId = nodeId }))
else
print("[Admin Handlers] SELECT_WAYPOINT: Could not find marker to highlight")
end
player:SendBroadcastMessage("Waypoint " .. nodeId .. " selected")
-- Send response with node position
AMS.Send(player, "WAYPOINT_SELECTED_RESPONSE", {
success = true,
pathId = pathId,
nodeId = nodeId,
x = node.x,
y = node.y,
z = node.z
})
end)
-- Get waypoint node info from a visual waypoint GUID (when targeting a waypoint marker)
AMS.RegisterHandler("GET_WAYPOINT_FOR_GUID", function(player, data)
if not data or not data.guid then
print("[Admin Handlers] GET_WAYPOINT_FOR_GUID: No GUID provided")
return
end
local pathId, nodeId = GetWaypointNodeForVisualGUID(data.guid)
if pathId == 0 or nodeId == 0 then
print("[Admin Handlers] GET_WAYPOINT_FOR_GUID: GUID not found in waypoint system")
AMS.Send(player, "WAYPOINT_FOR_GUID_RESPONSE", { success = false })
return
end
print("[Admin Handlers] GET_WAYPOINT_FOR_GUID: Found path " .. pathId .. " node " .. nodeId)
AMS.Send(player, "WAYPOINT_FOR_GUID_RESPONSE", {
success = true,
pathId = pathId,
nodeId = nodeId
})
end)
-- Get player data (GM state, etc.)
AMS.RegisterHandler("GET_PLAYER_DATA", function(player, data)
if not player then
return
end
-- Send player data including GM state (IsGM is the correct Eluna method)
AMS.Send(player, "PLAYER_DATA_RESPONSE", {
success = true,
isGM = player:IsGM()
})
end)
-- Teleport player to a waypoint location
AMS.RegisterHandler("TELEPORT_TO_WAYPOINT", function(player, data)
if not data or not data.x or not data.y or not data.z then
print("[Admin Handlers] TELEPORT_TO_WAYPOINT: Missing coordinates")
return
end
local x = data.x
local y = data.y
local z = data.z
local orientation = data.orientation or 0
print("[Admin Handlers] TELEPORT_TO_WAYPOINT: Teleporting to " .. x .. ", " .. y .. ", " .. z .. " facing " .. orientation)
-- Teleport player to the waypoint location with correct orientation
player:Teleport(player:GetMapId(), x, y, z, orientation)
player:SendBroadcastMessage("Teleported to waypoint")
end)
-- ============================================================================
-- Initialization
-- ============================================================================
@@ -382,3 +538,8 @@ print(" - SHOW_WAYPOINTS")
print(" - HIDE_WAYPOINTS")
print(" - HIDE_WAYPOINTS_BY_GUID")
print(" - CLEAR_ALL_WAYPOINT_MARKERS")
print(" - GET_WAYPOINT_DETAILS")
print(" - SELECT_WAYPOINT")
print(" - GET_WAYPOINT_FOR_GUID")
print(" - GET_PLAYER_DATA")
print(" - TELEPORT_TO_WAYPOINT")

View File

@@ -1806,7 +1806,7 @@ namespace LuaCreature
// Spells
lua_pushstring(L, "spells");
lua_newtable(L);
for (int i = 0; i < MAX_CREATURE_SPELLS; ++i)
for (uint32 i = 0; i < MAX_CREATURE_SPELLS; ++i)
{
if (cTemplate->spells[i] != 0)
{

View File

@@ -3346,6 +3346,193 @@ namespace LuaGlobalFunctions
return 0;
}
/**
* Gets waypoint path data by path ID.
* Returns a table with all nodes and their properties.
*
* @param uint32 pathId : the waypoint path ID
* @return table path : table with nodes array, or nil if not found
*/
int GetWaypointPath(Eluna* E)
{
uint32 pathId = E->CHECKVAL<uint32>(1);
WaypointPath const* path = sWaypointMgr->GetPath(pathId);
if (!path)
{
E->Push(); // Push nil
return 1;
}
lua_State* L = E->L;
lua_newtable(L); // Create path table
// Create nodes array
lua_newtable(L);
int nodeIndex = 1;
for (WaypointNode const& node : path->Nodes)
{
lua_newtable(L); // Create node table
lua_pushinteger(L, node.Id);
lua_setfield(L, -2, "id");
lua_pushnumber(L, node.X);
lua_setfield(L, -2, "x");
lua_pushnumber(L, node.Y);
lua_setfield(L, -2, "y");
lua_pushnumber(L, node.Z);
lua_setfield(L, -2, "z");
// Orientation is Optional<float>
if (node.Orientation)
{
lua_pushnumber(L, *node.Orientation);
lua_setfield(L, -2, "orientation");
}
else
{
lua_pushnumber(L, 0.0);
lua_setfield(L, -2, "orientation");
}
// Delay is Optional<Milliseconds>
if (node.Delay)
{
lua_pushinteger(L, node.Delay->count());
lua_setfield(L, -2, "delay");
}
else
{
lua_pushinteger(L, 0);
lua_setfield(L, -2, "delay");
}
// MoveType is WaypointMoveType enum
lua_pushinteger(L, static_cast<int>(node.MoveType));
lua_setfield(L, -2, "moveType");
lua_rawseti(L, -2, nodeIndex++);
}
lua_setfield(L, -2, "nodes");
lua_pushinteger(L, path->Id);
lua_setfield(L, -2, "id");
return 1;
}
/**
* Gets the path ID and node ID for a visual waypoint creature GUID.
* Used to identify which waypoint a creature marker represents.
*
* @param uint64 counter : the counter/low part of the creature GUID
* @return uint32 pathId : the waypoint path ID (0 if not found)
* @return uint32 nodeId : the node ID within the path (0 if not found)
*/
int GetWaypointNodeForVisualGUID(Eluna* E)
{
uint64 counter = E->CHECKVAL<uint64>(1);
// Create a creature GUID using the factory method
// We use mapId=0, entry=1 (VISUAL_WAYPOINT), and the counter from the client
ObjectGuid guid = ObjectGuid::Create<HighGuid::Creature>(0, 1, counter);
uint32 pathId = 0, nodeId = 0;
sWaypointMgr->GetPathAndNodeByVisualGUID(guid, pathId, nodeId);
E->Push(pathId);
E->Push(nodeId);
return 2;
}
/**
* Highlights a visual waypoint marker by changing its display ID.
* Used to visually indicate the selected waypoint in the world.
*
* @param Player player : the player (used to find the creature in their map)
* @param uint32 pathId : the waypoint path ID
* @param uint32 nodeId : the node ID within the path
* @param uint32 displayId : the display ID to use for highlighting
* @return bool success : true if the marker was changed
*/
int HighlightWaypointMarker(Eluna* E)
{
Player* player = E->CHECKOBJ<Player>(1);
uint32 pathId = E->CHECKVAL<uint32>(2);
uint32 nodeId = E->CHECKVAL<uint32>(3);
uint32 displayId = E->CHECKVAL<uint32>(4);
ObjectGuid const& markerGuid = sWaypointMgr->GetVisualGUIDByNode(pathId, nodeId);
if (markerGuid.IsEmpty())
{
E->Push(false);
return 1;
}
Creature* marker = ObjectAccessor::GetCreature(*player, markerGuid);
if (!marker)
{
E->Push(false);
return 1;
}
// Change display to highlight model and scale up
marker->SetDisplayId(displayId);
marker->SetObjectScale(3.0f); // Scale up significantly
marker->DestroyForNearbyPlayers(); // Force client update
marker->UpdateObjectVisibilityOnCreate(); // Recreate for players
E->Push(true);
return 1;
}
/**
* Resets a visual waypoint marker to its original display.
* Used to remove highlighting from a previously selected waypoint.
*
* @param Player player : the player (used to find the creature in their map)
* @param uint32 pathId : the waypoint path ID
* @param uint32 nodeId : the node ID within the path
* @param uint32 originalDisplayId : the original display ID to restore
* @return bool success : true if marker was reset
*/
int ClearWaypointMarkerAuras(Eluna* E)
{
Player* player = E->CHECKOBJ<Player>(1);
uint32 pathId = E->CHECKVAL<uint32>(2);
uint32 nodeId = E->CHECKVAL<uint32>(3);
uint32 originalDisplayId = E->CHECKVAL<uint32>(4, 0);
ObjectGuid const& markerGuid = sWaypointMgr->GetVisualGUIDByNode(pathId, nodeId);
if (markerGuid.IsEmpty())
{
E->Push(false);
return 1;
}
Creature* marker = ObjectAccessor::GetCreature(*player, markerGuid);
if (!marker)
{
E->Push(false);
return 1;
}
// Reset to original display and scale
if (originalDisplayId > 0)
marker->SetDisplayId(originalDisplayId);
else
marker->SetDisplayId(marker->GetNativeDisplayId());
marker->SetObjectScale(1.0f);
marker->DestroyForNearbyPlayers(); // Force client update
marker->UpdateObjectVisibilityOnCreate(); // Recreate for players
E->Push(true);
return 1;
}
/**
* Gets all shared data keys.
* Useful for debugging cross-state data.
@@ -3499,7 +3686,13 @@ namespace LuaGlobalFunctions
{ "ClearSharedData", &LuaGlobalFunctions::ClearSharedData },
{ "HasSharedData", &LuaGlobalFunctions::HasSharedData },
{ "ClearAllSharedData", &LuaGlobalFunctions::ClearAllSharedData },
{ "GetSharedDataKeys", &LuaGlobalFunctions::GetSharedDataKeys }
{ "GetSharedDataKeys", &LuaGlobalFunctions::GetSharedDataKeys },
// Waypoint utilities
{ "GetWaypointPath", &LuaGlobalFunctions::GetWaypointPath },
{ "GetWaypointNodeForVisualGUID", &LuaGlobalFunctions::GetWaypointNodeForVisualGUID },
{ "HighlightWaypointMarker", &LuaGlobalFunctions::HighlightWaypointMarker },
{ "ClearWaypointMarkerAuras", &LuaGlobalFunctions::ClearWaypointMarkerAuras }
};
}
#endif

View File

@@ -4265,7 +4265,7 @@ namespace LuaPlayer
* This removes stale tracking state and despawns any remaining marker creatures.
* Useful for cleaning up after CLEAR_ALL_WAYPOINT_MARKERS or Eluna reload.
*/
int ClearAllWaypointVisualizations(Eluna* E, Player* player)
int ClearAllWaypointVisualizations(Eluna* /*E*/, Player* player)
{
sWaypointMgr->ClearAllVisualizations(player);
return 0;

View File

@@ -398,3 +398,14 @@ void WaypointPath::BuildSegments()
ContinuousSegments.emplace_back(i, 1);
}
}
bool WaypointMgr::GetPathAndNodeByVisualGUID(ObjectGuid guid, uint32& outPathId, uint32& outNodeId) const
{
auto itr = _visualWaypointGUIDToNodeMap.find(guid);
if (itr == _visualWaypointGUIDToNodeMap.end())
return false;
outPathId = itr->second.first->Id;
outNodeId = itr->second.second->Id;
return true;
}

View File

@@ -65,6 +65,9 @@ class TC_GAME_API WaypointMgr
WaypointPath const* GetPathByVisualGUID(ObjectGuid guid) const;
WaypointNode const* GetNodeByVisualGUID(ObjectGuid guid) const;
ObjectGuid const& GetVisualGUIDByNode(uint32 pathId, uint32 nodeId) const;
// Get path and node IDs from a visual waypoint GUID
bool GetPathAndNodeByVisualGUID(ObjectGuid guid, uint32& outPathId, uint32& outNodeId) const;
private:
WaypointMgr();