From b18b7540e067fa8d69cece3a139fb39387c67d8d Mon Sep 17 00:00:00 2001 From: James Huston Date: Sun, 30 Nov 2025 18:16:10 -0500 Subject: [PATCH] feat(admin): Wander radius visualization and spawn marker tools - Add SHOW_WANDER_RADIUS / HIDE_WANDER_RADIUS handlers - Add SHOW_SPAWN_MARKER / HIDE_SPAWN_MARKER handlers - Add CLEAR_WANDER_MARKERS and CLEAR_NEARBY_MARKERS (orphan cleanup) - Add UI buttons: Show Spawn Point, Show Radius, Clear All, Clear Orphaned - Button state locking during save operations - Auto-respawn creature after wander distance change - Fix: Use tostring() for uint64 spawnId in Lua table keys - Fix: Update ObjectMgr cache when saving wander_distance/movementType Known issue: Database UPDATE may not be persisting - needs investigation with direct DB access to verify prepared statement execution. --- .../UI/Panels/NPCInfoPanel.lua | 388 +++++++++++++++ araxiaonline/lua_scripts/admin_handlers.lua | 461 ++++++++++++++++++ src/araxiaonline/CMakeLists.txt | 25 + .../Creature/AraxiaCreatureWriter.cpp | 159 ++++++ .../Entities/Creature/AraxiaCreatureWriter.h | 99 ++++ src/server/game/CMakeLists.txt | 3 + .../methods/TrinityCore/CreatureMethods.h | 182 ++++++- 7 files changed, 1316 insertions(+), 1 deletion(-) create mode 100644 src/araxiaonline/CMakeLists.txt create mode 100644 src/araxiaonline/game/Entities/Creature/AraxiaCreatureWriter.cpp create mode 100644 src/araxiaonline/game/Entities/Creature/AraxiaCreatureWriter.h diff --git a/araxiaonline/client_addons/AraxiaTrinityAdmin/UI/Panels/NPCInfoPanel.lua b/araxiaonline/client_addons/AraxiaTrinityAdmin/UI/Panels/NPCInfoPanel.lua index 2e3f6fe579..1bb1901c5d 100644 --- a/araxiaonline/client_addons/AraxiaTrinityAdmin/UI/Panels/NPCInfoPanel.lua +++ b/araxiaonline/client_addons/AraxiaTrinityAdmin/UI/Panels/NPCInfoPanel.lua @@ -636,6 +636,382 @@ end -- Deselect button no longer needed in split view layout -- (kept for code compatibility but hidden) +-- ============================================================================ +-- Wander Settings Content (shown for Random movement type instead of waypoints) +-- ============================================================================ + +local wanderSettingsContainer = CreateFrame("Frame", nil, waypointContent, "BackdropTemplate") +wanderSettingsContainer:SetAllPoints(waypointContent) +wanderSettingsContainer:SetBackdrop({ + bgFile = "Interface/Tooltips/UI-Tooltip-Background", + tile = true, tileSize = 16, + insets = { left = 4, right = 4, top = 4, bottom = 4 } +}) +wanderSettingsContainer:SetBackdropColor(0.05, 0.05, 0.05, 0.5) +wanderSettingsContainer:Hide() + +local wanderTitle = wanderSettingsContainer:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") +wanderTitle:SetPoint("TOP", wanderSettingsContainer, "TOP", 0, -15) +wanderTitle:SetText("|cFFFFD700Random Movement Settings|r") + +local wanderDesc = wanderSettingsContainer:CreateFontString(nil, "OVERLAY", "GameFontHighlight") +wanderDesc:SetPoint("TOP", wanderTitle, "BOTTOM", 0, -10) +wanderDesc:SetWidth(280) +wanderDesc:SetJustifyH("CENTER") +wanderDesc:SetText("This creature uses Random movement.\nAdjust the wander distance below:") + +-- Current wander distance display +local wanderCurrentLabel = wanderSettingsContainer:CreateFontString(nil, "OVERLAY", "GameFontNormal") +wanderCurrentLabel:SetPoint("TOPLEFT", wanderSettingsContainer, "TOPLEFT", 20, -90) +wanderCurrentLabel:SetText("|cFF00FF00Current Wander Distance:|r") + +local wanderCurrentValue = wanderSettingsContainer:CreateFontString(nil, "OVERLAY", "GameFontHighlight") +wanderCurrentValue:SetPoint("LEFT", wanderCurrentLabel, "RIGHT", 8, 0) +wanderCurrentValue:SetText("0 yards") + +-- Edit section +local wanderEditLabel = wanderSettingsContainer:CreateFontString(nil, "OVERLAY", "GameFontNormal") +wanderEditLabel:SetPoint("TOPLEFT", wanderCurrentLabel, "BOTTOMLEFT", 0, -20) +wanderEditLabel:SetText("|cFF00FF00New Distance (yards):|r") + +local wanderEditBox = CreateFrame("EditBox", nil, wanderSettingsContainer, "InputBoxTemplate") +wanderEditBox:SetSize(60, 24) +wanderEditBox:SetPoint("LEFT", wanderEditLabel, "RIGHT", 10, 0) +wanderEditBox:SetAutoFocus(false) +wanderEditBox:SetNumeric(false) -- Allow decimal input +wanderEditBox:SetMaxLetters(6) +wanderEditBox:SetText("5") + +-- Save button +local wanderSaveBtn = CreateFrame("Button", nil, wanderSettingsContainer, "UIPanelButtonTemplate") +wanderSaveBtn:SetSize(100, 26) +wanderSaveBtn:SetPoint("TOPLEFT", wanderEditLabel, "BOTTOMLEFT", 0, -20) +wanderSaveBtn:SetText("Save") +-- Helper function to disable all wander setting buttons while waiting for server +local function DisableWanderButtons() + wanderSaveBtn:Disable() + -- Other buttons will be defined below, we'll call this after they exist +end + +-- Helper function to re-enable all wander setting buttons +local function EnableWanderButtons() + wanderSaveBtn:Enable() + if spawnMarkerBtn then spawnMarkerBtn:Enable() end + if wanderRadiusBtn then wanderRadiusBtn:Enable() end + if clearAllBtn then clearAllBtn:Enable() end + if clearNearbyBtn then clearNearbyBtn:Enable() end +end + +-- Forward declare buttons so DisableWanderButtons can reference them +local spawnMarkerBtn, wanderRadiusBtn, clearAllBtn, clearNearbyBtn + +-- Update DisableWanderButtons to use the forward declarations +DisableWanderButtons = function() + wanderSaveBtn:Disable() + if spawnMarkerBtn then spawnMarkerBtn:Disable() end + if wanderRadiusBtn then wanderRadiusBtn:Disable() end + if clearAllBtn then clearAllBtn:Disable() end + if clearNearbyBtn then clearNearbyBtn:Disable() end +end + +-- Update EnableWanderButtons to use the forward declarations +EnableWanderButtons = function() + wanderSaveBtn:Enable() + if spawnMarkerBtn then spawnMarkerBtn:Enable() end + if wanderRadiusBtn then wanderRadiusBtn:Enable() end + if clearAllBtn then clearAllBtn:Enable() end + if clearNearbyBtn then clearNearbyBtn:Enable() end +end + +wanderSaveBtn:SetScript("OnClick", function() + local newDistance = tonumber(wanderEditBox:GetText()) + if not newDistance then + print("|cFFFF0000[ATA]|r Invalid distance value") + return + end + + if newDistance < 0 or newDistance > 100 then + print("|cFFFF0000[ATA]|r Distance must be between 0 and 100 yards") + return + end + + if AMS then + DisableWanderButtons() + AMS.Send("SET_WANDER_DISTANCE", { distance = newDistance }) + print("|cFF00FF00[ATA]|r Saving wander distance: " .. newDistance) + end +end) + +-- Spawn marker toggle button +local spawnMarkerVisible = false +spawnMarkerBtn = CreateFrame("Button", nil, wanderSettingsContainer, "UIPanelButtonTemplate") +spawnMarkerBtn:SetSize(140, 26) +spawnMarkerBtn:SetPoint("LEFT", wanderSaveBtn, "RIGHT", 10, 0) +spawnMarkerBtn:SetText("Show Spawn Point") +spawnMarkerBtn:SetScript("OnClick", function() + if AMS then + if spawnMarkerVisible then + AMS.Send("HIDE_SPAWN_MARKER", {}) + spawnMarkerVisible = false + spawnMarkerBtn:SetText("Show Spawn Point") + else + AMS.Send("SHOW_SPAWN_MARKER", {}) + spawnMarkerVisible = true + spawnMarkerBtn:SetText("Hide Spawn Point") + end + end +end) +spawnMarkerBtn:SetScript("OnEnter", function(self) + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + GameTooltip:AddLine("Spawn Point Marker", 1, 1, 1) + GameTooltip:AddLine("Shows a marker at this creature's", 0.7, 0.7, 0.7) + GameTooltip:AddLine("spawn/home position.", 0.7, 0.7, 0.7) + GameTooltip:Show() +end) +spawnMarkerBtn:SetScript("OnLeave", function() + GameTooltip:Hide() +end) + +-- Wander radius visualization button +local wanderRadiusVisible = false +wanderRadiusBtn = CreateFrame("Button", nil, wanderSettingsContainer, "UIPanelButtonTemplate") +wanderRadiusBtn:SetSize(140, 26) +wanderRadiusBtn:SetPoint("TOPLEFT", spawnMarkerBtn, "BOTTOMLEFT", 0, -5) +wanderRadiusBtn:SetText("Show Radius") +wanderRadiusBtn:SetScript("OnClick", function() + if AMS then + if wanderRadiusVisible then + AMS.Send("HIDE_WANDER_RADIUS", {}) + wanderRadiusVisible = false + wanderRadiusBtn:SetText("Show Radius") + else + AMS.Send("SHOW_WANDER_RADIUS", { segments = 16 }) + wanderRadiusVisible = true + wanderRadiusBtn:SetText("Hide Radius") + end + end +end) +wanderRadiusBtn:SetScript("OnEnter", function(self) + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + GameTooltip:AddLine("Wander Radius Visualization", 1, 1, 1) + GameTooltip:AddLine("Shows markers in a circle around", 0.7, 0.7, 0.7) + GameTooltip:AddLine("the spawn point at the wander", 0.7, 0.7, 0.7) + GameTooltip:AddLine("distance to visualize the area.", 0.7, 0.7, 0.7) + GameTooltip:Show() +end) +wanderRadiusBtn:SetScript("OnLeave", function() + GameTooltip:Hide() +end) + +-- Clear All button - removes spawn marker and wander radius markers +clearAllBtn = CreateFrame("Button", nil, wanderSettingsContainer, "UIPanelButtonTemplate") +clearAllBtn:SetSize(140, 26) +clearAllBtn:SetPoint("TOPLEFT", wanderRadiusBtn, "BOTTOMLEFT", 0, -5) +clearAllBtn:SetText("Clear All Markers") +clearAllBtn:SetScript("OnClick", function() + if AMS then + AMS.Send("CLEAR_WANDER_MARKERS", {}) + -- Reset button states + spawnMarkerVisible = false + spawnMarkerBtn:SetText("Show Spawn Point") + wanderRadiusVisible = false + wanderRadiusBtn:SetText("Show Radius") + end +end) +clearAllBtn:SetScript("OnEnter", function(self) + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + GameTooltip:AddLine("Clear All Markers", 1, 1, 1) + GameTooltip:AddLine("Removes both spawn point marker", 0.7, 0.7, 0.7) + GameTooltip:AddLine("and wander radius markers.", 0.7, 0.7, 0.7) + GameTooltip:Show() +end) +clearAllBtn:SetScript("OnLeave", function() + GameTooltip:Hide() +end) + +-- Clear Nearby (orphaned) markers button +clearNearbyBtn = CreateFrame("Button", nil, wanderSettingsContainer, "UIPanelButtonTemplate") +clearNearbyBtn:SetSize(140, 26) +clearNearbyBtn:SetPoint("TOPLEFT", clearAllBtn, "BOTTOMLEFT", 0, -5) +clearNearbyBtn:SetText("Clear Orphaned") +clearNearbyBtn:SetScript("OnClick", function() + if AMS then + AMS.Send("CLEAR_NEARBY_MARKERS", { range = 100 }) + end +end) +clearNearbyBtn:SetScript("OnEnter", function(self) + GameTooltip:SetOwner(self, "ANCHOR_RIGHT") + GameTooltip:AddLine("Clear Orphaned Markers", 1, 0.5, 0) + GameTooltip:AddLine("Removes ALL waypoint markers within", 0.7, 0.7, 0.7) + GameTooltip:AddLine("100 yards of your character.", 0.7, 0.7, 0.7) + GameTooltip:AddLine(" ", 0.7, 0.7, 0.7) + GameTooltip:AddLine("Use this to clean up markers that", 1, 1, 0) + GameTooltip:AddLine("were lost after a script reload.", 1, 1, 0) + GameTooltip:Show() +end) +clearNearbyBtn:SetScript("OnLeave", function() + GameTooltip:Hide() +end) + +-- Help text +local wanderHelp = wanderSettingsContainer:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") +wanderHelp:SetPoint("BOTTOMLEFT", wanderSettingsContainer, "BOTTOMLEFT", 20, 20) +wanderHelp:SetPoint("RIGHT", wanderSettingsContainer, "RIGHT", -20, 0) +wanderHelp:SetJustifyH("LEFT") +wanderHelp:SetText("|cFF888888Wander distance is the radius (in yards) the creature will\nrandomly move within from its spawn point.\nSet to 0 for no movement (idle).|r") + +-- Handler for spawn marker response +if AMS then + AMS.RegisterHandler("SPAWN_MARKER_RESPONSE", function(data) + if data and data.success then + if data.message and data.message:find("shown") then + spawnMarkerVisible = true + spawnMarkerBtn:SetText("Hide Spawn Point") + print("|cFF00FF00[ATA]|r Spawn marker shown at " .. string.format("%.1f, %.1f, %.1f", data.x or 0, data.y or 0, data.z or 0)) + else + spawnMarkerVisible = false + spawnMarkerBtn:SetText("Show Spawn Point") + print("|cFF00FF00[ATA]|r Spawn marker hidden") + end + else + print("|cFFFF0000[ATA]|r Spawn marker error: " .. (data.error or "Unknown error")) + end + end) +end + +-- Handler for wander distance save response +if AMS then + AMS.RegisterHandler("SET_WANDER_DISTANCE_RESPONSE", function(data) + EnableWanderButtons() + if data and data.success then + print("|cFF00FF00[ATA]|r Wander distance updated to " .. (data.newDistance or "?") .. " yards") + wanderCurrentValue:SetText(string.format("%.1f yards", data.newDistance or 0)) + + -- Remember what markers were visible BEFORE we cleared them + local wasSpawnVisible = spawnMarkerVisible + local wasRadiusVisible = wanderRadiusVisible + + -- Reset button states since markers were hidden by the save + spawnMarkerVisible = false + spawnMarkerBtn:SetText("Show Spawn Point") + wanderRadiusVisible = false + wanderRadiusBtn:SetText("Show Radius") + + -- Schedule re-showing markers after creature respawns (2 seconds) + -- Only if they were visible before AND we still have same target + if wasSpawnVisible or wasRadiusVisible then + C_Timer.After(2, function() + if AMS and UnitExists("target") then + if wasSpawnVisible then + AMS.Send("SHOW_SPAWN_MARKER", {}) + spawnMarkerVisible = true + spawnMarkerBtn:SetText("Hide Spawn Point") + end + if wasRadiusVisible then + AMS.Send("SHOW_WANDER_RADIUS", { segments = 16 }) + wanderRadiusVisible = true + wanderRadiusBtn:SetText("Hide Radius") + end + print("|cFF00FF00[ATA]|r Markers refreshed at new location") + else + print("|cFFFFFF00[ATA]|r Target lost - markers not refreshed") + end + end) + end + else + print("|cFFFF0000[ATA]|r Failed to update wander distance: " .. (data.message or "Unknown error")) + end + end) +end + +-- Handler for wander radius visualization response +if AMS then + AMS.RegisterHandler("WANDER_RADIUS_RESPONSE", function(data) + if data and data.success then + if data.message and data.message:find("shown") then + wanderRadiusVisible = true + wanderRadiusBtn:SetText("Hide Radius") + print("|cFF00FF00[ATA]|r " .. data.message .. " (" .. (data.markerCount or "?") .. " markers)") + else + wanderRadiusVisible = false + wanderRadiusBtn:SetText("Show Radius") + print("|cFF00FF00[ATA]|r Wander radius hidden") + end + else + print("|cFFFF0000[ATA]|r Wander radius error: " .. (data.error or "Unknown error")) + end + end) +end + +-- Handler for clear all markers response +if AMS then + AMS.RegisterHandler("CLEAR_WANDER_MARKERS_RESPONSE", function(data) + if data and data.success then + if data.cleared and data.cleared > 0 then + print("|cFF00FF00[ATA]|r " .. (data.message or "Markers cleared")) + else + print("|cFFFFFF00[ATA]|r " .. (data.message or "No markers found to clear")) + end + else + print("|cFFFF0000[ATA]|r Clear markers error: " .. (data.error or "Unknown error")) + end + end) +end + +-- Handler for clear nearby orphaned markers response +if AMS then + AMS.RegisterHandler("CLEAR_NEARBY_MARKERS_RESPONSE", function(data) + if data and data.success then + if data.cleared and data.cleared > 0 then + print("|cFF00FF00[ATA]|r " .. data.message) + else + print("|cFFFFFF00[ATA]|r No orphaned markers found nearby") + end + else + print("|cFFFF0000[ATA]|r Clear orphaned error: " .. (data.error or "Unknown error")) + end + end) +end + +-- Function to update wander settings display +local function UpdateWanderSettings(wanderRadius) + wanderCurrentValue:SetText(string.format("%.1f yards", wanderRadius or 0)) + wanderEditBox:SetText(string.format("%.1f", wanderRadius or 5)) +end + +-- Function to show waypoint or wander content based on movement type +local function UpdateMovementContent(movementType, wanderRadius, hasWaypointPath) + if movementType == 1 and not hasWaypointPath then + -- Random movement without waypoints - show wander settings + waypointListContainer:Hide() + waypointDivider:Hide() + waypointDetailPanel:Hide() + wanderSettingsContainer:Show() + UpdateWanderSettings(wanderRadius) + waypointButton:Disable() + else + -- Waypoint movement or has waypoint path - show waypoint list + wanderSettingsContainer:Hide() + waypointListContainer:Show() + waypointDivider:Show() + waypointDetailPanel:Show() + if hasWaypointPath then + waypointButton:Enable() + else + waypointButton:Disable() + end + end + + -- Reset button states when switching targets + spawnMarkerVisible = false + spawnMarkerBtn:SetText("Show Spawn Point") + wanderRadiusVisible = false + wanderRadiusBtn:SetText("Show Radius") + + -- Re-enable all buttons (in case they were disabled waiting for a response) + EnableWanderButtons() +end + -- Request waypoint details from server local function RequestWaypointDetails(pathId) if not AMS then return end @@ -1331,6 +1707,14 @@ function npcPanel:Update(requestServerData, forceRefresh) else waypointButton:Disable() end + + -- Update Waypoints tab to show either waypoint list or wander settings + if data and data.movement then + local movementType = data.movement.defaultType or 0 + local wanderRadius = data.behavior and data.behavior.wanderRadius or 0 + local hasWaypointPath = data.movement.waypointPath ~= nil + UpdateMovementContent(movementType, wanderRadius, hasWaypointPath) + end end, forceRefresh) -- Pass forceRefresh to bypass cache end @@ -1356,6 +1740,10 @@ end -- ============================================================================ refreshButton:SetScript("OnClick", function() + -- Re-enable buttons in case they got stuck disabled from a failed request + pcall(function() + if EnableWanderButtons then EnableWanderButtons() end + end) npcPanel:Update(true, true) -- Always force refresh from server, bypass cache end) diff --git a/araxiaonline/lua_scripts/admin_handlers.lua b/araxiaonline/lua_scripts/admin_handlers.lua index 959919774f..b0ab8c5b4b 100644 --- a/araxiaonline/lua_scripts/admin_handlers.lua +++ b/araxiaonline/lua_scripts/admin_handlers.lua @@ -374,6 +374,314 @@ AMS.RegisterHandler("CLEAR_ALL_WAYPOINT_MARKERS", function(player, data) AMS.Send(player, "CLEAR_WAYPOINTS_RESPONSE", { success = true }) end) +-- ============================================================================ +-- Spawn Point Marker Operations +-- ============================================================================ + +--[[ + SHOW_SPAWN_MARKER - Show a marker at the selected creature's spawn point + Request: { displayId = optional number } + Response: { success = bool, x, y, z, o } +]] +AMS.RegisterHandler("SHOW_SPAWN_MARKER", function(player, data) + print("[Admin Handlers] SHOW_SPAWN_MARKER request received") + + local creature = player:GetSelection() + if not creature then + AMS.Send(player, "SPAWN_MARKER_RESPONSE", { success = false, error = "No target selected" }) + return + end + + creature = creature:ToCreature() + if not creature then + AMS.Send(player, "SPAWN_MARKER_RESPONSE", { success = false, error = "Target is not a creature" }) + return + end + + -- Get home position for response + local x, y, z, o = creature:GetHomePosition() + + -- Show spawn marker (pass player for phase, optional displayId) + local displayId = data and data.displayId or nil + local success, marker = pcall(function() + if displayId then + return creature:ShowSpawnPointMarker(player, displayId) + else + return creature:ShowSpawnPointMarker(player) + end + end) + + if success and marker then + print("[Admin Handlers] SHOW_SPAWN_MARKER: Marker created at " .. x .. ", " .. y .. ", " .. z) + AMS.Send(player, "SPAWN_MARKER_RESPONSE", { + success = true, + x = x, y = y, z = z, o = o, + message = "Spawn marker shown" + }) + else + print("[Admin Handlers] SHOW_SPAWN_MARKER: Failed to create marker") + AMS.Send(player, "SPAWN_MARKER_RESPONSE", { + success = false, + error = "Failed to show marker (rebuild server required)" + }) + end +end) + +--[[ + HIDE_SPAWN_MARKER - Hide the spawn point marker for the selected creature + Request: {} + Response: { success = bool } +]] +AMS.RegisterHandler("HIDE_SPAWN_MARKER", function(player, data) + print("[Admin Handlers] HIDE_SPAWN_MARKER request received") + + local creature = player:GetSelection() + if not creature then + AMS.Send(player, "SPAWN_MARKER_RESPONSE", { success = false, error = "No target selected" }) + return + end + + creature = creature:ToCreature() + if not creature then + AMS.Send(player, "SPAWN_MARKER_RESPONSE", { success = false, error = "Target is not a creature" }) + return + end + + local success = pcall(function() return creature:HideSpawnPointMarker() end) + + if success then + print("[Admin Handlers] HIDE_SPAWN_MARKER: Marker hidden") + AMS.Send(player, "SPAWN_MARKER_RESPONSE", { success = true, message = "Spawn marker hidden" }) + else + print("[Admin Handlers] HIDE_SPAWN_MARKER: Failed to hide marker") + AMS.Send(player, "SPAWN_MARKER_RESPONSE", { success = false, error = "Failed to hide marker" }) + end +end) + +-- Local cache for wander radius markers (for same-session cleanup) +-- Also stored in ElunaSharedData for visibility, but Lua object refs only work same-session +local wanderRadiusMarkers = {} + +--[[ + SHOW_WANDER_RADIUS - Show visual markers in a circle at the creature's wander radius + Request: { segments = optional number (default 12) } + Response: { success = bool, radius = number, x, y, z } +]] +AMS.RegisterHandler("SHOW_WANDER_RADIUS", function(player, data) + print("[Admin Handlers] SHOW_WANDER_RADIUS request received") + + local creature = player:GetSelection() + if not creature then + AMS.Send(player, "WANDER_RADIUS_RESPONSE", { success = false, error = "No target selected" }) + return + end + + creature = creature:ToCreature() + if not creature then + AMS.Send(player, "WANDER_RADIUS_RESPONSE", { success = false, error = "Target is not a creature" }) + return + end + + local spawnId = creature:GetDBTableGUIDLow() + local spawnIdStr = tostring(spawnId) -- Convert to string for table key + local wanderRadius = creature:GetWanderRadius() + + if wanderRadius <= 0 then + AMS.Send(player, "WANDER_RADIUS_RESPONSE", { success = false, error = "Creature has no wander radius (0)" }) + return + end + + -- Get spawn position + local homeX, homeY, homeZ, homeO = creature:GetHomePosition() + local markerKey = "wander_markers_" .. spawnIdStr + + -- Clear any existing markers for this creature (from local cache) + if wanderRadiusMarkers[spawnIdStr] then + for _, marker in ipairs(wanderRadiusMarkers[spawnIdStr]) do + if marker and marker:IsInWorld() then + marker:DespawnOrUnsummon() + end + end + wanderRadiusMarkers[spawnIdStr] = nil + end + ClearSharedData(markerKey) -- Also clear shared data + + -- Number of markers around the circle (default 12 = every 30 degrees) + local segments = (data and data.segments) or 12 + if segments < 4 then segments = 4 end + if segments > 36 then segments = 36 end + + local markers = {} + local markerGuids = {} + local angleStep = (2 * math.pi) / segments + + -- Spawn markers around the circle perimeter + for i = 0, segments - 1 do + local angle = i * angleStep + local markerX = homeX + wanderRadius * math.cos(angle) + local markerY = homeY + wanderRadius * math.sin(angle) + local markerZ = homeZ -- Keep at spawn height + + -- Spawn a visual marker at this position + -- Using entry 1 (VISUAL_WAYPOINT) with a distinct display + local marker = creature:SpawnCreature(1, markerX, markerY, markerZ, 0, 8) -- 8 = TEMPSUMMON_MANUAL_DESPAWN + if marker then + marker:SetDisplayId(31366) -- Green circle like spawn marker + marker:SetScale(0.3) -- Smaller for radius markers + table.insert(markers, marker) + table.insert(markerGuids, tostring(marker:GetGUIDLow())) + end + end + + -- Also spawn a marker at the center (spawn point) + local centerMarker = creature:SpawnCreature(1, homeX, homeY, homeZ, homeO, 8) + if centerMarker then + centerMarker:SetDisplayId(31366) -- Green circle + centerMarker:SetScale(1.0) -- Larger for center + table.insert(markers, centerMarker) + table.insert(markerGuids, tostring(centerMarker:GetGUIDLow())) + end + + -- Store in local cache for same-session cleanup (use string key) + wanderRadiusMarkers[spawnIdStr] = markers + + -- Also store GUIDs in shared data (for debugging/tracking) + SetSharedData(markerKey, Smallfolk.dumps(markerGuids)) + + print("[Admin Handlers] SHOW_WANDER_RADIUS: Created " .. #markers .. " markers for radius " .. wanderRadius) + AMS.Send(player, "WANDER_RADIUS_RESPONSE", { + success = true, + radius = wanderRadius, + segments = segments, + markerCount = #markers, + x = homeX, y = homeY, z = homeZ, + message = "Wander radius shown (" .. wanderRadius .. " yards)" + }) +end) + +--[[ + HIDE_WANDER_RADIUS - Hide the wander radius markers for the selected creature + Request: {} + Response: { success = bool } +]] +AMS.RegisterHandler("HIDE_WANDER_RADIUS", function(player, data) + print("[Admin Handlers] HIDE_WANDER_RADIUS request received") + + local creature = player:GetSelection() + if not creature then + AMS.Send(player, "WANDER_RADIUS_RESPONSE", { success = false, error = "No target selected" }) + return + end + + creature = creature:ToCreature() + if not creature then + AMS.Send(player, "WANDER_RADIUS_RESPONSE", { success = false, error = "Target is not a creature" }) + return + end + + local spawnId = creature:GetDBTableGUIDLow() + local spawnIdStr = tostring(spawnId) + local markerKey = "wander_markers_" .. spawnIdStr + + if wanderRadiusMarkers[spawnIdStr] then + local count = #wanderRadiusMarkers[spawnIdStr] + for _, marker in ipairs(wanderRadiusMarkers[spawnIdStr]) do + if marker and marker:IsInWorld() then + marker:DespawnOrUnsummon() + end + end + wanderRadiusMarkers[spawnIdStr] = nil + ClearSharedData(markerKey) + print("[Admin Handlers] HIDE_WANDER_RADIUS: Removed " .. count .. " markers") + AMS.Send(player, "WANDER_RADIUS_RESPONSE", { success = true, message = "Wander radius hidden" }) + else + -- No local cache - markers may have been lost on reload + -- They'll despawn naturally, just clear any tracking data + ClearSharedData(markerKey) + AMS.Send(player, "WANDER_RADIUS_RESPONSE", { success = true, message = "Wander radius markers cleared (may already be despawned)" }) + end +end) + +--[[ + CLEAR_WANDER_MARKERS - Clear both spawn marker and wander radius markers + Request: {} + Response: { success = bool } +]] +AMS.RegisterHandler("CLEAR_WANDER_MARKERS", function(player, data) + print("[Admin Handlers] CLEAR_WANDER_MARKERS request received") + + local creature = player:GetSelection() + if not creature then + AMS.Send(player, "CLEAR_WANDER_MARKERS_RESPONSE", { success = false, error = "No target selected" }) + return + end + + creature = creature:ToCreature() + if not creature then + AMS.Send(player, "CLEAR_WANDER_MARKERS_RESPONSE", { success = false, error = "Target is not a creature" }) + return + end + + local spawnId = creature:GetDBTableGUIDLow() + local spawnIdStr = tostring(spawnId) + local cleared = 0 + local messages = {} + + -- Clear spawn marker (via C++ method if available) + local spawnSuccess, spawnResult = pcall(function() return creature:HideSpawnPointMarker() end) + if spawnSuccess and spawnResult then + cleared = cleared + 1 + table.insert(messages, "spawn marker") + end + + -- Clear wander radius markers (from local cache, use string key) + if wanderRadiusMarkers[spawnIdStr] then + local count = #wanderRadiusMarkers[spawnIdStr] + for _, marker in ipairs(wanderRadiusMarkers[spawnIdStr]) do + if marker and marker:IsInWorld() then + marker:DespawnOrUnsummon() + cleared = cleared + 1 + end + end + wanderRadiusMarkers[spawnIdStr] = nil + table.insert(messages, count .. " radius markers") + end + + -- Clear shared data + ClearSharedData("wander_markers_" .. spawnIdStr) + + local msg = cleared > 0 and ("Cleared: " .. table.concat(messages, ", ")) or "No active markers in cache (may have been lost on reload)" + print("[Admin Handlers] CLEAR_WANDER_MARKERS: " .. msg .. " for creature " .. tostring(spawnId)) + AMS.Send(player, "CLEAR_WANDER_MARKERS_RESPONSE", { success = true, cleared = cleared, message = msg }) +end) + +--[[ + CLEAR_NEARBY_MARKERS - Emergency clear of all VISUAL_WAYPOINT creatures near player + This clears orphaned markers from before tracking was implemented + Request: { range = optional number (default 100) } + Response: { success = bool, cleared = number } +]] +AMS.RegisterHandler("CLEAR_NEARBY_MARKERS", function(player, data) + print("[Admin Handlers] CLEAR_NEARBY_MARKERS request received") + + local range = (data and data.range) or 100 + local cleared = 0 + + -- Get all creatures near player with entry 1 (VISUAL_WAYPOINT) + local creatures = player:GetCreaturesInRange(range, 1) -- entry 1 = VISUAL_WAYPOINT + if creatures then + for _, creature in ipairs(creatures) do + if creature and creature:IsInWorld() then + creature:DespawnOrUnsummon() + cleared = cleared + 1 + end + end + end + + print("[Admin Handlers] CLEAR_NEARBY_MARKERS: Cleared " .. cleared .. " markers within " .. range .. " yards") + AMS.Send(player, "CLEAR_NEARBY_MARKERS_RESPONSE", { success = true, cleared = cleared, message = "Cleared " .. cleared .. " orphaned markers" }) +end) + -- Get detailed waypoint data for a path AMS.RegisterHandler("GET_WAYPOINT_DETAILS", function(player, data) if not data or not data.pathId then @@ -527,6 +835,151 @@ AMS.RegisterHandler("TELEPORT_TO_WAYPOINT", function(player, data) player:SendBroadcastMessage("Teleported to waypoint") end) +-- ============================================================================ +-- Creature Edit Operations (Araxia Write Operations) +-- ============================================================================ + +--[[ + SET_WANDER_DISTANCE - Set the wander distance for the selected creature + Request: { distance = 10.0 } + Response: { success = bool, message = string, newDistance = number } + Note: Creature must be selected (same as GET_NPC_DATA) +]] +AMS.RegisterHandler("SET_WANDER_DISTANCE", function(player, data) + if not data or data.distance == nil then + print("[Admin Handlers] SET_WANDER_DISTANCE: Missing distance") + AMS.Send(player, "SET_WANDER_DISTANCE_RESPONSE", { + success = false, + message = "Missing distance parameter" + }) + return + end + + print("[Admin Handlers] SET_WANDER_DISTANCE: Setting distance to " .. data.distance) + + -- Use player's selection (same pattern as GET_NPC_DATA) + local creature = player:GetSelection() + if not creature then + AMS.Send(player, "SET_WANDER_DISTANCE_RESPONSE", { + success = false, + message = "No creature selected" + }) + return + end + + creature = creature:ToCreature() + if not creature then + AMS.Send(player, "SET_WANDER_DISTANCE_RESPONSE", { + success = false, + message = "Selected object is not a creature" + }) + return + end + + local spawnId = creature:GetDBTableGUIDLow() + local spawnIdStr = tostring(spawnId) -- Convert to string for table key + local homeX, homeY, homeZ, homeO = creature:GetHomePosition() + + -- Check if markers were visible and hide them + local hadSpawnMarker = false + local hadRadiusMarkers = false + + -- Try to hide spawn marker (via C++) - check if it returns true + local spawnHideSuccess, spawnHideResult = pcall(function() return creature:HideSpawnPointMarker() end) + if spawnHideSuccess and spawnHideResult then + hadSpawnMarker = true + print("[Admin Handlers] SET_WANDER_DISTANCE: Hid spawn marker") + end + + -- Hide wander radius markers if they exist (use string key) + if wanderRadiusMarkers[spawnIdStr] then + hadRadiusMarkers = true + for _, marker in ipairs(wanderRadiusMarkers[spawnIdStr]) do + if marker and marker:IsInWorld() then + marker:DespawnOrUnsummon() + end + end + wanderRadiusMarkers[spawnIdStr] = nil + ClearSharedData("wander_markers_" .. spawnIdStr) + print("[Admin Handlers] SET_WANDER_DISTANCE: Hid radius markers") + end + + -- Use the Araxia writer method to save to database + local success, message = creature:SaveWanderDistance(data.distance, player) + + print("[Admin Handlers] SET_WANDER_DISTANCE: Result - " .. tostring(success) .. ", " .. message) + + -- Despawn and respawn the creature so the new settings take effect immediately + if success then + -- Set a very short respawn delay (1 second), then despawn + creature:SetRespawnDelay(1) + creature:Respawn() -- Mark for respawn + creature:DespawnOrUnsummon(0) -- Remove from world + print("[Admin Handlers] SET_WANDER_DISTANCE: Creature despawned, respawning in 1 second") + + -- Tell client markers need to be re-shown (client will handle the delay) + if hadSpawnMarker or hadRadiusMarkers then + print("[Admin Handlers] SET_WANDER_DISTANCE: Markers will need to be re-shown after respawn") + end + end + + AMS.Send(player, "SET_WANDER_DISTANCE_RESPONSE", { + success = success, + message = message, + newDistance = data.distance, + markersCleared = hadSpawnMarker or hadRadiusMarkers, + hadSpawnMarker = hadSpawnMarker, + hadRadiusMarkers = hadRadiusMarkers + }) +end) + +--[[ + SET_MOVEMENT_TYPE - Set the movement type for the selected creature + Request: { movementType = 0|1|2 } + Response: { success = bool, message = string, newMovementType = number } + Note: Creature must be selected +]] +AMS.RegisterHandler("SET_MOVEMENT_TYPE", function(player, data) + if not data or data.movementType == nil then + print("[Admin Handlers] SET_MOVEMENT_TYPE: Missing movementType") + AMS.Send(player, "SET_MOVEMENT_TYPE_RESPONSE", { + success = false, + message = "Missing movementType parameter" + }) + return + end + + print("[Admin Handlers] SET_MOVEMENT_TYPE: Setting type to " .. data.movementType) + + local creature = player:GetSelection() + if not creature then + AMS.Send(player, "SET_MOVEMENT_TYPE_RESPONSE", { + success = false, + message = "No creature selected" + }) + return + end + + creature = creature:ToCreature() + if not creature then + AMS.Send(player, "SET_MOVEMENT_TYPE_RESPONSE", { + success = false, + message = "Selected object is not a creature" + }) + return + end + + local success, message = creature:SaveMovementType(data.movementType, player) + + print("[Admin Handlers] SET_MOVEMENT_TYPE: Result - " .. tostring(success) .. ", " .. message) + + AMS.Send(player, "SET_MOVEMENT_TYPE_RESPONSE", { + success = success, + message = message, + newMovementType = data.movementType + }) +end) + -- ============================================================================ -- Initialization -- ============================================================================ @@ -538,8 +991,16 @@ print(" - SHOW_WAYPOINTS") print(" - HIDE_WAYPOINTS") print(" - HIDE_WAYPOINTS_BY_GUID") print(" - CLEAR_ALL_WAYPOINT_MARKERS") +print(" - SHOW_SPAWN_MARKER") +print(" - HIDE_SPAWN_MARKER") +print(" - SHOW_WANDER_RADIUS") +print(" - HIDE_WANDER_RADIUS") +print(" - CLEAR_WANDER_MARKERS") +print(" - CLEAR_NEARBY_MARKERS") print(" - GET_WAYPOINT_DETAILS") print(" - SELECT_WAYPOINT") print(" - GET_WAYPOINT_FOR_GUID") print(" - GET_PLAYER_DATA") print(" - TELEPORT_TO_WAYPOINT") +print(" - SET_WANDER_DISTANCE") +print(" - SET_MOVEMENT_TYPE") diff --git a/src/araxiaonline/CMakeLists.txt b/src/araxiaonline/CMakeLists.txt new file mode 100644 index 0000000000..0ee6cf867e --- /dev/null +++ b/src/araxiaonline/CMakeLists.txt @@ -0,0 +1,25 @@ +# Araxia Online - Custom Code +# This folder contains all Araxia-specific customizations + +CollectSourceFiles( + ${CMAKE_CURRENT_SOURCE_DIR} + PRIVATE_SOURCES) + +GroupSources(${CMAKE_CURRENT_SOURCE_DIR}) + +# Add include directories for araxiaonline code +target_include_directories(game + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/game + ${CMAKE_CURRENT_SOURCE_DIR}/game/Entities + ${CMAKE_CURRENT_SOURCE_DIR}/game/Entities/Creature +) + +# Add sources to the game target +target_sources(game + PRIVATE + ${PRIVATE_SOURCES} +) + +message(STATUS "[Araxia] Custom code sources added") diff --git a/src/araxiaonline/game/Entities/Creature/AraxiaCreatureWriter.cpp b/src/araxiaonline/game/Entities/Creature/AraxiaCreatureWriter.cpp new file mode 100644 index 0000000000..2bbb286a05 --- /dev/null +++ b/src/araxiaonline/game/Entities/Creature/AraxiaCreatureWriter.cpp @@ -0,0 +1,159 @@ +/* + * AraxiaCreatureWriter.cpp + * + * Araxia Online - Custom Write Operations for Creatures + */ + +#include "AraxiaCreatureWriter.h" +#include "Creature.h" +#include "Player.h" +#include "DatabaseEnv.h" +#include "Log.h" +#include "ObjectMgr.h" +#include "Map.h" + +namespace Araxia +{ + +CreatureWriter& CreatureWriter::Instance() +{ + static CreatureWriter instance; + return instance; +} + +CreatureWriter::CreatureWriter() +{ + TC_LOG_INFO("server.loading", "[Araxia] CreatureWriter initialized"); +} + +void CreatureWriter::LogChange(std::string const& table, std::string const& field, + std::string const& oldValue, std::string const& newValue, + ObjectGuid::LowType affectedGuid, Player* changedBy) +{ + // TODO: Store changes in araxia_change_log table for export + // For now, just log to the server log + std::string changerName = changedBy ? changedBy->GetName() : "SYSTEM"; + + TC_LOG_INFO("araxia.changes", "[Araxia Change] Table: %s, Field: %s, GUID: " UI64FMTD ", " + "Old: %s, New: %s, ChangedBy: %s", + table.c_str(), field.c_str(), affectedGuid, + oldValue.c_str(), newValue.c_str(), changerName.c_str()); +} + +WriteResult CreatureWriter::SetWanderDistance(ObjectGuid::LowType spawnGuid, float distance, Player* changedBy) +{ + WriteResult result; + result.success = false; + result.changeId = 0; + + // Validate distance + if (distance < 0.0f) + { + result.message = "Wander distance cannot be negative"; + return result; + } + + if (distance > 100.0f) + { + result.message = "Wander distance too large (max 100 yards)"; + return result; + } + + // Get current value for logging + float oldDistance = GetWanderDistance(spawnGuid); + + // Update database + WorldDatabasePreparedStatement* stmt = WorldDatabase.GetPreparedStatement(WORLD_UPD_CREATURE_WANDER_DISTANCE); + stmt->setFloat(0, distance); + stmt->setUInt64(1, spawnGuid); + WorldDatabase.Execute(stmt); + + // Log the change + LogChange("creature", "wander_distance", + std::to_string(oldDistance), std::to_string(distance), + spawnGuid, changedBy); + + // CRITICAL: Also update the ObjectMgr cache so respawned creatures get the new value + CreatureData const* data = sObjectMgr->GetCreatureData(spawnGuid); + if (data) + { + // Cast away const to update the cached value + // This is safe because we own the data and are updating it consistently with DB + CreatureData* mutableData = const_cast(data); + mutableData->wander_distance = distance; + TC_LOG_DEBUG("araxia.changes", "[Araxia] Updated ObjectMgr cache for spawn " UI64FMTD " wander_distance to %.2f", spawnGuid, distance); + } + + result.success = true; + result.message = "Wander distance updated successfully"; + + TC_LOG_INFO("araxia.changes", "[Araxia] SetWanderDistance: GUID " UI64FMTD " set to %.2f yards by %s", + spawnGuid, distance, changedBy ? changedBy->GetName().c_str() : "SYSTEM"); + + return result; +} + +float CreatureWriter::GetWanderDistance(ObjectGuid::LowType spawnGuid) +{ + CreatureData const* data = sObjectMgr->GetCreatureData(spawnGuid); + if (data) + return data->wander_distance; + + return 0.0f; +} + +WriteResult CreatureWriter::SetMovementType(ObjectGuid::LowType spawnGuid, uint8 movementType, Player* changedBy) +{ + WriteResult result; + result.success = false; + result.changeId = 0; + + // Validate movement type + if (movementType > 2) + { + result.message = "Invalid movement type (must be 0=Idle, 1=Random, 2=Waypoint)"; + return result; + } + + // Get current value for logging + uint8 oldType = GetMovementType(spawnGuid); + + // Update database + WorldDatabasePreparedStatement* stmt = WorldDatabase.GetPreparedStatement(WORLD_UPD_CREATURE_MOVEMENT_TYPE); + stmt->setUInt8(0, movementType); + stmt->setUInt64(1, spawnGuid); + WorldDatabase.Execute(stmt); + + // Log the change + LogChange("creature", "MovementType", + std::to_string(oldType), std::to_string(movementType), + spawnGuid, changedBy); + + // CRITICAL: Also update the ObjectMgr cache so respawned creatures get the new value + CreatureData const* data = sObjectMgr->GetCreatureData(spawnGuid); + if (data) + { + CreatureData* mutableData = const_cast(data); + mutableData->movementType = movementType; + TC_LOG_DEBUG("araxia.changes", "[Araxia] Updated ObjectMgr cache for spawn " UI64FMTD " movementType to %u", spawnGuid, movementType); + } + + result.success = true; + result.message = "Movement type updated successfully"; + + TC_LOG_INFO("araxia.changes", "[Araxia] SetMovementType: GUID " UI64FMTD " set to %u by %s", + spawnGuid, movementType, changedBy ? changedBy->GetName().c_str() : "SYSTEM"); + + return result; +} + +uint8 CreatureWriter::GetMovementType(ObjectGuid::LowType spawnGuid) +{ + CreatureData const* data = sObjectMgr->GetCreatureData(spawnGuid); + if (data) + return data->movementType; + + return 0; +} + +} // namespace Araxia diff --git a/src/araxiaonline/game/Entities/Creature/AraxiaCreatureWriter.h b/src/araxiaonline/game/Entities/Creature/AraxiaCreatureWriter.h new file mode 100644 index 0000000000..c16016c6e0 --- /dev/null +++ b/src/araxiaonline/game/Entities/Creature/AraxiaCreatureWriter.h @@ -0,0 +1,99 @@ +/* + * AraxiaCreatureWriter.h + * + * Araxia Online - Custom Write Operations for Creatures + * + * This class wraps all database write operations for creatures. + * All modifications to creature data should go through this class to enable: + * - Centralized logging of changes + * - Easy export of modifications + * - Audit trail for content changes + * - Future rollback capabilities + */ + +#ifndef ARAXIA_CREATURE_WRITER_H +#define ARAXIA_CREATURE_WRITER_H + +#include "Define.h" +#include "ObjectGuid.h" +#include + +class Creature; +class Player; + +namespace Araxia +{ + +// Result structure for write operations +struct WriteResult +{ + bool success; + std::string message; + uint64 changeId; // Future: unique ID for tracking changes +}; + +class CreatureWriter +{ +public: + static CreatureWriter& Instance(); + + // Prevent copying + CreatureWriter(CreatureWriter const&) = delete; + CreatureWriter& operator=(CreatureWriter const&) = delete; + + // ======================================================================== + // Movement Settings + // ======================================================================== + + /** + * Set the wander distance for a creature spawn. + * Only applies to creatures with MovementType = 1 (Random). + * + * @param spawnGuid The creature spawn GUID (from creature table) + * @param distance The wander radius in yards + * @param changedBy Optional player who made the change (for logging) + * @return WriteResult with success status and any error message + */ + WriteResult SetWanderDistance(ObjectGuid::LowType spawnGuid, float distance, Player* changedBy = nullptr); + + /** + * Get the current wander distance for a creature spawn. + * + * @param spawnGuid The creature spawn GUID + * @return The wander distance, or 0 if not found + */ + float GetWanderDistance(ObjectGuid::LowType spawnGuid); + + /** + * Set the movement type for a creature spawn. + * + * @param spawnGuid The creature spawn GUID + * @param movementType 0=Idle, 1=Random, 2=Waypoint + * @param changedBy Optional player who made the change + * @return WriteResult with success status + */ + WriteResult SetMovementType(ObjectGuid::LowType spawnGuid, uint8 movementType, Player* changedBy = nullptr); + + /** + * Get the movement type for a creature spawn. + * + * @param spawnGuid The creature spawn GUID + * @return Movement type (0=Idle, 1=Random, 2=Waypoint), or 0 if not found + */ + uint8 GetMovementType(ObjectGuid::LowType spawnGuid); + +private: + CreatureWriter(); + ~CreatureWriter() = default; + + // Log a change for future export capabilities + void LogChange(std::string const& table, std::string const& field, + std::string const& oldValue, std::string const& newValue, + ObjectGuid::LowType affectedGuid, Player* changedBy); +}; + +#define sAraxiaCreatureWriter Araxia::CreatureWriter::Instance() + +} // namespace Araxia + +#endif // ARAXIA_CREATURE_WRITER_H diff --git a/src/server/game/CMakeLists.txt b/src/server/game/CMakeLists.txt index d659e202a8..2071e423cd 100644 --- a/src/server/game/CMakeLists.txt +++ b/src/server/game/CMakeLists.txt @@ -81,6 +81,9 @@ if(BUILD_SHARED_LIBS) endif() endif() +# Add Araxia Online custom code +add_subdirectory(${CMAKE_SOURCE_DIR}/src/araxiaonline ${CMAKE_BINARY_DIR}/araxiaonline) + # Generate precompiled header if(USE_COREPCH) add_cxx_pch(game ${PRIVATE_PCH_HEADER}) diff --git a/src/server/game/LuaEngine/methods/TrinityCore/CreatureMethods.h b/src/server/game/LuaEngine/methods/TrinityCore/CreatureMethods.h index cdbba93066..77c33d73a9 100644 --- a/src/server/game/LuaEngine/methods/TrinityCore/CreatureMethods.h +++ b/src/server/game/LuaEngine/methods/TrinityCore/CreatureMethods.h @@ -7,6 +7,10 @@ #ifndef CREATUREMETHODS_H #define CREATUREMETHODS_H +// Araxia Online - Custom write operations +#include "AraxiaCreatureWriter.h" +#include "ElunaSharedData.h" + /*** * Non-[Player] controlled [Unit]s (i.e. NPCs). * @@ -1627,6 +1631,108 @@ namespace LuaCreature return 1; } + /** + * Shows a visual marker at the [Creature]'s spawn/home position. + * The marker is a creature with the specified display ID (default: spawn point marker). + * + * @param [Player] phaseSource = nil : optional player to inherit phase from + * @param uint32 displayId = 31366 : display ID for the marker (default is green circle) + * @return Creature marker : the spawned marker creature, or nil on failure + */ + int ShowSpawnPointMarker(Eluna* E, Creature* creature) + { + float x, y, z, o; + creature->GetHomePosition(x, y, z, o); + + // Optional player parameter for phase inheritance (arg 2) + Player* phaseSource = E->CHECKOBJ(2, false); + + // Display ID - default to green circle (31366) or specified + uint32 displayId = 31366; // Green targeting circle + int displayIdArg = phaseSource ? 3 : 2; + if (!lua_isnoneornil(E->L, displayIdArg)) + { + displayId = E->CHECKVAL(displayIdArg); + } + + // First, hide any existing spawn marker for this creature + ObjectGuid::LowType spawnId = creature->GetSpawnId(); + std::string markerKey = "spawn_marker_" + std::to_string(spawnId); + std::string existingCounter; + if (sElunaSharedData->Get(markerKey, existingCounter)) + { + try { + ObjectGuid::LowType counter = std::stoull(existingCounter); + ObjectGuid oldGuid = ObjectGuid::Create(creature->GetMapId(), VISUAL_WAYPOINT, counter); + if (Creature* oldMarker = ObjectAccessor::GetCreature(*creature, oldGuid)) + { + oldMarker->DespawnOrUnsummon(); + } + } catch (...) { + // Ignore invalid stored GUID + } + sElunaSharedData->Clear(markerKey); + } + + // Use SummonCreature like waypoint visualization does (VISUAL_WAYPOINT = 1) + TempSummon* marker = creature->SummonCreature(VISUAL_WAYPOINT, x, y, z, o); + if (!marker) + { + E->Push(); + return 1; + } + + // Set custom display + marker->SetDisplayId(displayId, true); + marker->SetObjectScale(1.5f); // Make it visible + + // Inherit phase from player if provided + if (phaseSource) + { + PhasingHandler::InheritPhaseShift(marker, phaseSource); + } + + // Store marker's counter for later removal + sElunaSharedData->Set(markerKey, std::to_string(marker->GetGUID().GetCounter())); + + E->Push(marker); + return 1; + } + + /** + * Hides/removes the spawn point marker for this [Creature]. + * + * @return bool success : true if marker was removed + */ + int HideSpawnPointMarker(Eluna* E, Creature* creature) + { + ObjectGuid::LowType spawnId = creature->GetSpawnId(); + std::string markerKey = "spawn_marker_" + std::to_string(spawnId); + std::string counterStr; + + if (!sElunaSharedData->Get(markerKey, counterStr)) + { + E->Push(false); + return 1; + } + + try { + ObjectGuid::LowType counter = std::stoull(counterStr); + ObjectGuid markerGuid = ObjectGuid::Create(creature->GetMapId(), VISUAL_WAYPOINT, counter); + + if (Creature* marker = ObjectAccessor::GetCreature(*creature, markerGuid)) + { + marker->DespawnOrUnsummon(); + } + } catch (...) { + // Ignore invalid stored GUID + } + + sElunaSharedData->Clear(markerKey); + E->Push(true); + return 1; + } + /** * Returns a table containing the [Creature]'s waypoint path data. * Includes path info and all waypoint nodes. @@ -1820,6 +1926,74 @@ namespace LuaCreature return 1; } + // ======================================================================== + // Araxia Online - Database Write Operations + // These methods persist changes to the database using AraxiaCreatureWriter + // ======================================================================== + + /** + * [Araxia] Saves the wander distance to the database for this creature spawn. + * This persists the change across server restarts. + * Only applies to creatures with MovementType = 1 (Random). + * + * @param float distance : the wander radius in yards (0-100) + * @param [Player] changedBy : optional player who made the change (for logging) + * @return bool success : true if the change was saved + * @return string message : result message + */ + int SaveWanderDistance(Eluna* E, Creature* creature) + { + float distance = E->CHECKVAL(2); + Player* changedBy = E->CHECKOBJ(3, false); + + ObjectGuid::LowType spawnId = creature->GetSpawnId(); + if (spawnId == 0) + { + E->Push(false); + E->Push("Creature has no spawn ID (not a database spawn)"); + return 2; + } + + Araxia::WriteResult result = sAraxiaCreatureWriter.SetWanderDistance(spawnId, distance, changedBy); + + // Also update the live creature's in-memory value + if (result.success) + creature->SetWanderDistance(distance); + + E->Push(result.success); + E->Push(result.message); + return 2; + } + + /** + * [Araxia] Saves the movement type to the database for this creature spawn. + * This persists the change across server restarts. + * + * @param uint8 movementType : 0=Idle, 1=Random, 2=Waypoint + * @param [Player] changedBy : optional player who made the change (for logging) + * @return bool success : true if the change was saved + * @return string message : result message + */ + int SaveMovementType(Eluna* E, Creature* creature) + { + uint8 movementType = E->CHECKVAL(2); + Player* changedBy = E->CHECKOBJ(3, false); + + ObjectGuid::LowType spawnId = creature->GetSpawnId(); + if (spawnId == 0) + { + E->Push(false); + E->Push("Creature has no spawn ID (not a database spawn)"); + return 2; + } + + Araxia::WriteResult result = sAraxiaCreatureWriter.SetMovementType(spawnId, movementType, changedBy); + + E->Push(result.success); + E->Push(result.message); + return 2; + } + ElunaRegister CreatureMethods[] = { // Getters @@ -1865,6 +2039,8 @@ namespace LuaCreature { "GetWaypointPathData", &LuaCreature::GetWaypointPathData }, { "VisualizeWaypointPath", &LuaCreature::VisualizeWaypointPath }, { "DevisualizeWaypointPath", &LuaCreature::DevisualizeWaypointPath }, + { "ShowSpawnPointMarker", &LuaCreature::ShowSpawnPointMarker }, + { "HideSpawnPointMarker", &LuaCreature::HideSpawnPointMarker }, // Setters { "SetRegeneratingHealth", &LuaCreature::SetRegeneratingHealth }, @@ -1940,7 +2116,11 @@ namespace LuaCreature { "ResetAllThreat", &LuaCreature::ResetAllThreat }, { "FixateTarget", &LuaCreature::FixateTarget }, { "ClearFixate", &LuaCreature::ClearFixate }, - { "RemoveFromWorld", &LuaCreature::RemoveFromWorld } + { "RemoveFromWorld", &LuaCreature::RemoveFromWorld }, + + // Araxia Online - Database Write Operations + { "SaveWanderDistance", &LuaCreature::SaveWanderDistance }, + { "SaveMovementType", &LuaCreature::SaveMovementType } }; }; #endif