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.
This commit is contained in:
MarcelineVQ
2026-03-02 11:26:10 -08:00
parent 93e4354d7b
commit bf718b66db
3 changed files with 120 additions and 19 deletions
+1
View File
@@ -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));
}
}
+109 -19
View File
@@ -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)
+10
View File
@@ -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
// =============================================================================