From bf718b66db6ee86b1f020221d07b3ff2c5030e49 Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Mon, 2 Mar 2026 11:26:10 -0800 Subject: [PATCH] Harden group sync: SF dedup, roster debounce, area ID filter - SF first-responder lock: after sending SR/LSR, only accept SF messages from the first player to respond, ignore duplicates from other members to prevent entity flicker. - Roster change debounce: retriggerable 5s timer with up to 5 one-second extensions (10s max). Only fires on group size increase. Party events skipped when in raid. - Area ID check in DLL respawn: markers only spawn when the player is in the same zone (area_id match against 0xB4E314). - New GetCurrentAreaId() Lua function for addon zone awareness. --- src/main.zig | 1 + src/markers/addon/Markers.lua | 128 +++++++++++++++++++++++++++++----- src/markers/markers.zig | 10 +++ 3 files changed, 120 insertions(+), 19 deletions(-) diff --git a/src/main.zig b/src/main.zig index 862b08f..a47a080 100644 --- a/src/main.zig +++ b/src/main.zig @@ -267,6 +267,7 @@ fn registerLuaFunctions() void { registerFunction("SetMarkerDef", @intFromPtr(&markers.luaSetMarkerDef)); registerFunction("ClearMarkerDef", @intFromPtr(&markers.luaClearMarkerDef)); registerFunction("GetMarkerDef", @intFromPtr(&markers.luaGetMarkerDef)); + registerFunction("GetCurrentAreaId", @intFromPtr(&markers.luaGetCurrentAreaId)); } } diff --git a/src/markers/addon/Markers.lua b/src/markers/addon/Markers.lua index 1728a7c..232b5ab 100644 --- a/src/markers/addon/Markers.lua +++ b/src/markers/addon/Markers.lua @@ -127,7 +127,7 @@ local RawWorldMarker = WorldMarker function WorldMarker(index, ...) log("WorldMarker(" .. tostring(index) .. ")") -- Cancel pending sync request — we're actively placing marks - if syncTimer.pending then + if syncTimer and syncTimer.pending then syncTimer:Hide() syncTimer.pending = false log("sync timer cancelled (local mark placed)") @@ -164,10 +164,33 @@ function ClearWorldMarker(index) end end +-- ============================================================================= +-- Sync response deduplication +-- Only accept SF from the first responder after we send SR/LSR. +-- ============================================================================= + +local syncSender = nil -- name of the first SF responder + +local function resetSyncSender() + syncSender = nil +end + -- ============================================================================= -- Addon message handler -- ============================================================================= +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 @@ -202,13 +225,16 @@ local function onAddonMessage(prefix, message, channel, sender) broadcastAllDefs() return elseif cmd == "SF" then - -- Sync response: accept from anyone (they already checked canSetMarkers on their end) - 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 + -- Sync response: only accept from the first responder + if syncSender == nil then + syncSender = sender + log(" sync responder locked: " .. sender) + elseif syncSender ~= sender then + log(" ignoring SF from " .. sender .. " (locked to " .. syncSender .. ")") + return + end + local idx, x, y, z, areaId = parseMarkerFields(parts) + if idx then log(" SetMarkerDef(" .. idx .. "," .. x .. "," .. y .. "," .. z .. "," .. areaId .. ")") SetMarkerDef(idx, x, y, z, areaId) else @@ -224,12 +250,8 @@ local function onAddonMessage(prefix, message, channel, sender) end if cmd == "P" then - 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 + local idx, x, y, z, areaId = parseMarkerFields(parts) + if idx then log(" SetMarkerDef(" .. idx .. "," .. x .. "," .. y .. "," .. z .. "," .. areaId .. ")") SetMarkerDef(idx, x, y, z, areaId) else @@ -259,6 +281,8 @@ local function broadcastSyncRequest() log("syncReq: no channel (not in group)") return end + -- Reset first-responder lock before requesting + syncSender = nil local cmd = canSetMarkers() and "LSR" or "SR" log("SEND [" .. ch .. "] " .. cmd) SendAddonMessage(MSG_PREFIX, cmd, ch) @@ -266,7 +290,7 @@ 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 syncTimer = CreateFrame("Frame") +syncTimer = CreateFrame("Frame") syncTimer.elapsed = 0 syncTimer.pending = false syncTimer:Hide() @@ -287,6 +311,69 @@ local function scheduleSyncRequest() log("sync request scheduled (5s)") 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 + log("roster timer fired, broadcasting") + if canSetMarkers() then + broadcastAllDefs() + end + end +end) + +local MAX_EXTENSIONS = 5 + +local function onRosterChange() + local newSize = getGroupSize() + local oldSize = lastGroupSize + lastGroupSize = newSize + + if newSize <= oldSize then + log("roster: " .. oldSize .. "->" .. newSize .. " (no increase)") + return + end + + log("roster: " .. oldSize .. "->" .. newSize .. " (grew)") + + if not rosterTimer.pending then + rosterTimer.remaining = ROSTER_INITIAL_DELAY + rosterTimer.pending = true + rosterTimer.extensions = 0 + rosterTimer:Show() + log("roster timer started (" .. ROSTER_INITIAL_DELAY .. "s)") + elseif rosterTimer.extensions < MAX_EXTENSIONS then + rosterTimer.remaining = rosterTimer.remaining + ROSTER_EXTEND_SEC + rosterTimer.extensions = rosterTimer.extensions + 1 + log("roster timer +" .. ROSTER_EXTEND_SEC .. "s (ext " .. rosterTimer.extensions .. "/" .. MAX_EXTENSIONS .. ")") + end +end + local frame = CreateFrame("Frame") frame:RegisterEvent("PLAYER_LOGIN") frame:RegisterEvent("CHAT_MSG_ADDON") @@ -296,13 +383,16 @@ frame:RegisterEvent("RAID_ROSTER_UPDATE") frame:SetScript("OnEvent", function() if event == "PLAYER_LOGIN" then DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Markers|r v" .. MARKERS_VERSION .. " loaded") + lastGroupSize = getGroupSize() scheduleSyncRequest() elseif event == "CHAT_MSG_ADDON" then onAddonMessage(arg1, arg2, arg3, arg4) - elseif event == "PARTY_MEMBERS_CHANGED" or event == "RAID_ROSTER_UPDATE" then - log("EVT " .. event .. " r=" .. GetNumRaidMembers() .. " p=" .. GetNumPartyMembers()) - if canSetMarkers() then - broadcastAllDefs() + elseif event == "RAID_ROSTER_UPDATE" then + onRosterChange() + elseif event == "PARTY_MEMBERS_CHANGED" then + -- Skip party events when in a raid (RAID_ROSTER_UPDATE handles it) + if GetNumRaidMembers() == 0 then + onRosterChange() end end end) diff --git a/src/markers/markers.zig b/src/markers/markers.zig index c28c6a8..2b0cda2 100644 --- a/src/markers/markers.zig +++ b/src/markers/markers.zig @@ -535,9 +535,11 @@ fn tickAnimations() void { if (player != 0) { const player_pos = getUnitPosition(player); if (player_pos.x != 0 or player_pos.y != 0 or player_pos.z != 0) { + const current_area = hook.readMem(u32, o.ZONE_AREA_ID); for (0..NUM_MARKERS) |i| { if (!marker_defs[i].active) continue; if (marker_entities[i] != null) continue; // entity alive, skip + if (marker_defs[i].area_id != current_area) continue; // wrong zone const dx = player_pos.x - marker_defs[i].pos.x; const dy = player_pos.y - marker_defs[i].pos.y; @@ -660,6 +662,14 @@ pub fn luaGetMarkerDef(L: u32) callconv(.c) u32 { return 4; } +/// Lua: local areaId = GetCurrentAreaId() +/// Returns the current zone area ID from the game global. +pub fn luaGetCurrentAreaId(L: u32) callconv(.c) u32 { + const area_id = hook.readMem(u32, o.ZONE_AREA_ID); + lapi.pushnumber(L, @floatCast(@as(f64, @floatFromInt(area_id)))); + return 1; +} + // ============================================================================= // World teardown hook // =============================================================================