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:
@@ -267,6 +267,7 @@ fn registerLuaFunctions() void {
|
|||||||
registerFunction("SetMarkerDef", @intFromPtr(&markers.luaSetMarkerDef));
|
registerFunction("SetMarkerDef", @intFromPtr(&markers.luaSetMarkerDef));
|
||||||
registerFunction("ClearMarkerDef", @intFromPtr(&markers.luaClearMarkerDef));
|
registerFunction("ClearMarkerDef", @intFromPtr(&markers.luaClearMarkerDef));
|
||||||
registerFunction("GetMarkerDef", @intFromPtr(&markers.luaGetMarkerDef));
|
registerFunction("GetMarkerDef", @intFromPtr(&markers.luaGetMarkerDef));
|
||||||
|
registerFunction("GetCurrentAreaId", @intFromPtr(&markers.luaGetCurrentAreaId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+109
-19
@@ -127,7 +127,7 @@ local RawWorldMarker = WorldMarker
|
|||||||
function WorldMarker(index, ...)
|
function WorldMarker(index, ...)
|
||||||
log("WorldMarker(" .. tostring(index) .. ")")
|
log("WorldMarker(" .. tostring(index) .. ")")
|
||||||
-- Cancel pending sync request — we're actively placing marks
|
-- Cancel pending sync request — we're actively placing marks
|
||||||
if syncTimer.pending then
|
if syncTimer and syncTimer.pending then
|
||||||
syncTimer:Hide()
|
syncTimer:Hide()
|
||||||
syncTimer.pending = false
|
syncTimer.pending = false
|
||||||
log("sync timer cancelled (local mark placed)")
|
log("sync timer cancelled (local mark placed)")
|
||||||
@@ -164,10 +164,33 @@ function ClearWorldMarker(index)
|
|||||||
end
|
end
|
||||||
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
|
-- 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)
|
local function onAddonMessage(prefix, message, channel, sender)
|
||||||
if prefix ~= MSG_PREFIX then return end
|
if prefix ~= MSG_PREFIX then return end
|
||||||
|
|
||||||
@@ -202,13 +225,16 @@ local function onAddonMessage(prefix, message, channel, sender)
|
|||||||
broadcastAllDefs()
|
broadcastAllDefs()
|
||||||
return
|
return
|
||||||
elseif cmd == "SF" then
|
elseif cmd == "SF" then
|
||||||
-- Sync response: accept from anyone (they already checked canSetMarkers on their end)
|
-- Sync response: only accept from the first responder
|
||||||
local idx = tonumber(parts[2])
|
if syncSender == nil then
|
||||||
local x = tonumber(parts[3])
|
syncSender = sender
|
||||||
local y = tonumber(parts[4])
|
log(" sync responder locked: " .. sender)
|
||||||
local z = tonumber(parts[5])
|
elseif syncSender ~= sender then
|
||||||
local areaId = tonumber(parts[6])
|
log(" ignoring SF from " .. sender .. " (locked to " .. syncSender .. ")")
|
||||||
if idx and x and y and z and areaId then
|
return
|
||||||
|
end
|
||||||
|
local idx, x, y, z, areaId = parseMarkerFields(parts)
|
||||||
|
if idx then
|
||||||
log(" SetMarkerDef(" .. idx .. "," .. x .. "," .. y .. "," .. z .. "," .. areaId .. ")")
|
log(" SetMarkerDef(" .. idx .. "," .. x .. "," .. y .. "," .. z .. "," .. areaId .. ")")
|
||||||
SetMarkerDef(idx, x, y, z, areaId)
|
SetMarkerDef(idx, x, y, z, areaId)
|
||||||
else
|
else
|
||||||
@@ -224,12 +250,8 @@ local function onAddonMessage(prefix, message, channel, sender)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if cmd == "P" then
|
if cmd == "P" then
|
||||||
local idx = tonumber(parts[2])
|
local idx, x, y, z, areaId = parseMarkerFields(parts)
|
||||||
local x = tonumber(parts[3])
|
if idx then
|
||||||
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
|
|
||||||
log(" SetMarkerDef(" .. idx .. "," .. x .. "," .. y .. "," .. z .. "," .. areaId .. ")")
|
log(" SetMarkerDef(" .. idx .. "," .. x .. "," .. y .. "," .. z .. "," .. areaId .. ")")
|
||||||
SetMarkerDef(idx, x, y, z, areaId)
|
SetMarkerDef(idx, x, y, z, areaId)
|
||||||
else
|
else
|
||||||
@@ -259,6 +281,8 @@ local function broadcastSyncRequest()
|
|||||||
log("syncReq: no channel (not in group)")
|
log("syncReq: no channel (not in group)")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
-- Reset first-responder lock before requesting
|
||||||
|
syncSender = nil
|
||||||
local cmd = canSetMarkers() and "LSR" or "SR"
|
local cmd = canSetMarkers() and "LSR" or "SR"
|
||||||
log("SEND [" .. ch .. "] " .. cmd)
|
log("SEND [" .. ch .. "] " .. cmd)
|
||||||
SendAddonMessage(MSG_PREFIX, cmd, ch)
|
SendAddonMessage(MSG_PREFIX, cmd, ch)
|
||||||
@@ -266,7 +290,7 @@ end
|
|||||||
|
|
||||||
-- Delayed sync request: roster isn't populated at PLAYER_LOGIN/ENTERING_WORLD,
|
-- 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.
|
-- 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.elapsed = 0
|
||||||
syncTimer.pending = false
|
syncTimer.pending = false
|
||||||
syncTimer:Hide()
|
syncTimer:Hide()
|
||||||
@@ -287,6 +311,69 @@ local function scheduleSyncRequest()
|
|||||||
log("sync request scheduled (5s)")
|
log("sync request scheduled (5s)")
|
||||||
end
|
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")
|
local frame = CreateFrame("Frame")
|
||||||
frame:RegisterEvent("PLAYER_LOGIN")
|
frame:RegisterEvent("PLAYER_LOGIN")
|
||||||
frame:RegisterEvent("CHAT_MSG_ADDON")
|
frame:RegisterEvent("CHAT_MSG_ADDON")
|
||||||
@@ -296,13 +383,16 @@ frame:RegisterEvent("RAID_ROSTER_UPDATE")
|
|||||||
frame:SetScript("OnEvent", function()
|
frame:SetScript("OnEvent", function()
|
||||||
if event == "PLAYER_LOGIN" then
|
if event == "PLAYER_LOGIN" then
|
||||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Markers|r v" .. MARKERS_VERSION .. " loaded")
|
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Markers|r v" .. MARKERS_VERSION .. " loaded")
|
||||||
|
lastGroupSize = getGroupSize()
|
||||||
scheduleSyncRequest()
|
scheduleSyncRequest()
|
||||||
elseif event == "CHAT_MSG_ADDON" then
|
elseif event == "CHAT_MSG_ADDON" then
|
||||||
onAddonMessage(arg1, arg2, arg3, arg4)
|
onAddonMessage(arg1, arg2, arg3, arg4)
|
||||||
elseif event == "PARTY_MEMBERS_CHANGED" or event == "RAID_ROSTER_UPDATE" then
|
elseif event == "RAID_ROSTER_UPDATE" then
|
||||||
log("EVT " .. event .. " r=" .. GetNumRaidMembers() .. " p=" .. GetNumPartyMembers())
|
onRosterChange()
|
||||||
if canSetMarkers() then
|
elseif event == "PARTY_MEMBERS_CHANGED" then
|
||||||
broadcastAllDefs()
|
-- Skip party events when in a raid (RAID_ROSTER_UPDATE handles it)
|
||||||
|
if GetNumRaidMembers() == 0 then
|
||||||
|
onRosterChange()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -535,9 +535,11 @@ fn tickAnimations() void {
|
|||||||
if (player != 0) {
|
if (player != 0) {
|
||||||
const player_pos = getUnitPosition(player);
|
const player_pos = getUnitPosition(player);
|
||||||
if (player_pos.x != 0 or player_pos.y != 0 or player_pos.z != 0) {
|
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| {
|
for (0..NUM_MARKERS) |i| {
|
||||||
if (!marker_defs[i].active) continue;
|
if (!marker_defs[i].active) continue;
|
||||||
if (marker_entities[i] != null) continue; // entity alive, skip
|
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 dx = player_pos.x - marker_defs[i].pos.x;
|
||||||
const dy = player_pos.y - marker_defs[i].pos.y;
|
const dy = player_pos.y - marker_defs[i].pos.y;
|
||||||
@@ -660,6 +662,14 @@ pub fn luaGetMarkerDef(L: u32) callconv(.c) u32 {
|
|||||||
return 4;
|
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
|
// World teardown hook
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user