Files
WeirdUtils/src/markers/addon/WorldMarkers.lua
T
MarcelineVQ 9d51ff1c77 Rename sync functions and drop redundant GetMarkerDef
Rename SetMarkerDef → SetMarkerSync, ClearMarkerDef → ClearMarkerSync
to clarify their purpose. Remove GetMarkerDef from the table since
GetWorldMarker is now a public global.
2026-03-07 14:13:41 -08:00

398 lines
12 KiB
Lua

-- WorldMarkers addon (embedded in DLL, loaded from memory)
WORLDMARKERS_VERSION = 4
BINDING_HEADER_WORLDMARKERS = "World Markers"
-- =============================================================================
-- Addon message protocol
-- Delimiter is ":" (pipe "|" is WoW's escape char for color codes)
--
-- Messages:
-- P:idx:x:y:z:area - live placement (everyone processes)
-- C:idx - clear one marker
-- CA - clear all markers
-- SR - sync request (non-leader asking leader to send defs)
-- LSR - leader sync request (leader asking anyone to send defs)
-- SF:idx:x:y:z:area - sync fill (only processed by requester in sync mode)
--
-- Permission model: ALL permission checks are enforced DLL-side.
-- WorldMarker/ClearWorldMarker: DLL checks local player is leader/assist.
-- SetMarkerSync/ClearMarkerSync: DLL checks sender name against roster.
-- CanSetWorldMarker(): DLL returns 1 if local player has permission.
-- =============================================================================
local MSG_PREFIX = "WMark"
local NUM_MARKERS = 5
local function getChannel()
if GetNumRaidMembers() > 0 then
return "RAID"
elseif GetNumPartyMembers() > 0 then
return "PARTY"
end
return nil
end
-- =============================================================================
-- Sync state: after sending SR/LSR, accept SF from the first responder only.
-- Cleared by a 5s timer, or when we send our own placement/clear messages.
-- =============================================================================
local syncing = false
local syncSender = nil
-- General-purpose one-shot timer. Set .delay, .callback, then :Show().
local syncTimer = CreateFrame("Frame")
syncTimer.elapsed = 0
syncTimer.delay = 5
syncTimer.callback = nil
syncTimer:Hide()
syncTimer:SetScript("OnUpdate", function()
syncTimer.elapsed = syncTimer.elapsed + arg1
if syncTimer.elapsed >= syncTimer.delay then
syncTimer:Hide()
if syncTimer.callback then
syncTimer.callback()
end
end
end)
local function clearSyncState()
syncing = false
syncSender = nil
syncTimer:Hide()
end
local function startSyncing()
syncing = true
syncSender = nil
syncTimer.elapsed = 0
syncTimer.delay = 5
syncTimer.callback = function()
if syncing then
syncing = false
syncSender = nil
end
end
syncTimer:Show()
end
-- =============================================================================
-- Broadcast helpers
-- =============================================================================
local function broadcastPlace(index, x, y, z, areaId)
clearSyncState()
local ch = getChannel()
if not ch then return end
local msg = "P:" .. index .. ":" .. x .. ":" .. y .. ":" .. z .. ":" .. areaId
SendAddonMessage(MSG_PREFIX, msg, ch)
end
local function broadcastClear(index)
clearSyncState()
local ch = getChannel()
if not ch then return end
local msg = "C:" .. index
SendAddonMessage(MSG_PREFIX, msg, ch)
end
local function broadcastClearAll()
clearSyncState()
local ch = getChannel()
if not ch then return end
SendAddonMessage(MSG_PREFIX, "CA", ch)
end
local function broadcastAllDefs()
local ch = getChannel()
if not ch then return end
for i = 1, NUM_MARKERS do
local x, y, z, areaId = GetWorldMarker(i)
if x then
local msg = "SF:" .. i .. ":" .. x .. ":" .. y .. ":" .. z .. ":" .. areaId
SendAddonMessage(MSG_PREFIX, msg, ch)
end
end
end
-- =============================================================================
-- Permission denial feedback (cooldown + max 3 per login, reset on success)
-- =============================================================================
local denyCount = 0
local denyLastTime = 0
local DENY_COOLDOWN = 5
local DENY_MAX = 3
local function showDenyMessage()
if denyCount >= DENY_MAX then return end
local now = GetTime()
if now - denyLastTime < DENY_COOLDOWN then return end
denyLastTime = now
denyCount = denyCount + 1
if GetNumRaidMembers() > 0 or GetNumPartyMembers() > 0 then
DEFAULT_CHAT_FRAME:AddMessage("|cffffff00You must be leader or assist to use world markers.|r")
else
DEFAULT_CHAT_FRAME:AddMessage("|cffffff00You must be in a group to use world markers.|r")
end
end
local failLastTime = 0
local FAIL_COOLDOWN = 2
local function showFailMessage()
local now = GetTime()
if now - failLastTime < FAIL_COOLDOWN then return end
failLastTime = now
DEFAULT_CHAT_FRAME:AddMessage("|cffffff00Unable to create World Marker at that location.|r")
end
-- =============================================================================
-- Wrap DLL functions to broadcast on group placement/clear
-- =============================================================================
local RawWorldMarker = WorldMarker
function WorldMarker(index, ...)
local x, y, z, areaId = RawWorldMarker(index, unpack(arg))
if x == nil then
showDenyMessage()
return
end
if x < 0 and (y == nil) then
showFailMessage()
return
end
denyCount = 0
local ch = getChannel()
if ch then
broadcastPlace(index, x, y, z, areaId)
end
return x, y, z, areaId
end
local RawClearWorldMarker = ClearWorldMarker
function ClearWorldMarker(index)
local ok = RawClearWorldMarker(index)
if not ok then
showDenyMessage()
return
end
denyCount = 0
local ch = getChannel()
if ch then
if index then
broadcastClear(index)
else
broadcastClearAll()
end
end
end
-- =============================================================================
-- Addon message handler
-- All mutation commands pass sender name to the DLL for permission check.
-- =============================================================================
local function parseMarkerFields(parts)
local idx = tonumber(parts[2])
local x = tonumber(parts[3])
local y = tonumber(parts[4])
local z = tonumber(parts[5])
local areaId = tonumber(parts[6])
if idx and x and y and z and areaId then
return idx, x, y, z, areaId
end
return nil
end
local function onAddonMessage(prefix, message, channel, sender)
if prefix ~= MSG_PREFIX then return end
if sender == UnitName("player") then return end
-- Parse colon-delimited message
local parts = {}
for part in string.gfind(message, "[^:]+") do
table.insert(parts, part)
end
local cmd = parts[1]
if cmd == "SR" then
if CanSetWorldMarker() then
broadcastAllDefs()
end
return
elseif cmd == "LSR" then
broadcastAllDefs()
return
elseif cmd == "SF" then
if not syncing then return end
if syncSender == nil then
syncSender = sender
elseif syncSender ~= sender then
return
end
local idx, x, y, z, areaId = parseMarkerFields(parts)
if idx then
WorldMarkers.SetMarkerSync(idx, x, y, z, areaId, sender)
end
return
end
if cmd == "P" then
local idx, x, y, z, areaId = parseMarkerFields(parts)
if idx then
WorldMarkers.SetMarkerSync(idx, x, y, z, areaId, sender)
end
elseif cmd == "C" then
local idx = tonumber(parts[2])
if idx then
WorldMarkers.ClearMarkerSync(idx, sender)
end
elseif cmd == "CA" then
WorldMarkers.ClearMarkerSync(sender)
end
end
-- =============================================================================
-- Event frame
-- =============================================================================
local function broadcastSyncRequest()
local ch = getChannel()
if not ch then return end
startSyncing()
local cmd = CanSetWorldMarker() and "LSR" or "SR"
SendAddonMessage(MSG_PREFIX, cmd, ch)
end
-- Delayed sync request: roster isn't populated at PLAYER_LOGIN/ENTERING_WORLD,
-- so we fire a one-shot 5s timer to request markers after joining the group.
local function scheduleSyncRequest()
syncTimer.elapsed = 0
syncTimer.delay = 5
syncTimer.callback = function()
broadcastSyncRequest()
end
syncTimer:Show()
end
-- =============================================================================
-- Roster change tracking - retriggerable debounce timer
-- Each roster event that increases group size adds 1s to the timer (starts at
-- 5s, caps at 10s). When the timer expires, one broadcast fires. This
-- guarantees delivery after the storm of events settles.
-- =============================================================================
local lastGroupSize = 0
local ROSTER_INITIAL_DELAY = 5
local ROSTER_EXTEND_SEC = 1
local ROSTER_MAX_DELAY = 10
local function getGroupSize()
local raid = GetNumRaidMembers()
if raid > 0 then return raid end
return GetNumPartyMembers()
end
local rosterTimer = CreateFrame("Frame")
rosterTimer.remaining = 0
rosterTimer.pending = false
rosterTimer.extensions = 0
rosterTimer:Hide()
rosterTimer:SetScript("OnUpdate", function()
rosterTimer.remaining = rosterTimer.remaining - arg1
if rosterTimer.remaining <= 0 then
rosterTimer:Hide()
rosterTimer.pending = false
rosterTimer.extensions = 0
if CanSetWorldMarker() then
broadcastAllDefs()
end
end
end)
local MAX_EXTENSIONS = 5
local function onRosterChange()
local newSize = getGroupSize()
local oldSize = lastGroupSize
lastGroupSize = newSize
if newSize <= oldSize then return end
if not rosterTimer.pending then
rosterTimer.remaining = ROSTER_INITIAL_DELAY
rosterTimer.pending = true
rosterTimer.extensions = 0
rosterTimer:Show()
elseif rosterTimer.extensions < MAX_EXTENSIONS then
rosterTimer.remaining = rosterTimer.remaining + ROSTER_EXTEND_SEC
rosterTimer.extensions = rosterTimer.extensions + 1
end
end
local frame = CreateFrame("Frame")
frame:RegisterEvent("PLAYER_LOGIN")
frame:RegisterEvent("CHAT_MSG_ADDON")
frame:RegisterEvent("PARTY_MEMBERS_CHANGED")
frame:RegisterEvent("RAID_ROSTER_UPDATE")
frame:SetScript("OnEvent", function()
if event == "PLAYER_LOGIN" then
lastGroupSize = getGroupSize()
scheduleSyncRequest()
elseif event == "CHAT_MSG_ADDON" then
onAddonMessage(arg1, arg2, arg3, arg4)
elseif event == "RAID_ROSTER_UPDATE" then
onRosterChange()
elseif event == "PARTY_MEMBERS_CHANGED" then
if GetNumRaidMembers() == 0 then
onRosterChange()
end
end
end)
-- =============================================================================
-- Slash commands
-- =============================================================================
SLASH_WORLDMARKER1 = "/worldmarker"
SLASH_WORLDMARKER2 = "/wm"
SlashCmdList["WORLDMARKER"] = function(msg)
msg = string.lower(msg or "")
local parts = {}
for word in string.gfind(msg, "%S+") do
table.insert(parts, word)
end
local index = tonumber(parts[1])
if not index then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00World Markers|r commands:")
DEFAULT_CHAT_FRAME:AddMessage(" /wm 1-5 - Place marker at cursor")
DEFAULT_CHAT_FRAME:AddMessage(" /wm 1-5 <unit> - Place marker at unit")
DEFAULT_CHAT_FRAME:AddMessage(" /cwm [1-5] - Clear one or all markers")
return
end
if parts[2] then
WorldMarker(index, parts[2])
else
WorldMarker(index)
end
end
SLASH_CLEARWORLDMARKER1 = "/clearworldmarker"
SLASH_CLEARWORLDMARKER2 = "/cwm"
SlashCmdList["CLEARWORLDMARKER"] = function(msg)
local index = tonumber(msg or "")
if index then
ClearWorldMarker(index)
else
ClearWorldMarker()
end
end