From 38e5872591e832e25a33c360c95145ff0e120e06 Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Tue, 3 Mar 2026 18:52:28 -0800 Subject: [PATCH] Unify marker sync protocol: remove SetMarkerDefSync, use SF/SD messages SetMarkerDefSync checked canSetMarkers() on the local player, which blocked non-leaders from receiving sync data. Now SF messages use the same SetMarkerDef (senderHasPermission) check as P messages. Protocol change: sync responder sends SF: for each marker then SD to signal completion. Requester enters sync mode with a 5s fallback timer, locks to first SF sender, and exits on SD. Non-syncing clients ignore SF/SD. Consolidated login delay and sync timeout into one timer frame. --- src/main.zig | 1 - src/markers/addon/Markers.lua | 113 +++++++++++++++++++++++++--------- src/markers/markers.zig | 78 ----------------------- 3 files changed, 83 insertions(+), 109 deletions(-) diff --git a/src/main.zig b/src/main.zig index eec7819..48190dc 100644 --- a/src/main.zig +++ b/src/main.zig @@ -117,7 +117,6 @@ fn registerLuaFunctions() void { // Internal functions in WorldMarkers table (via luaL_openlib) const lib = [_]lua.LuaReg{ .{ .name = "SetMarkerDef", .func = @intFromPtr(&markers.luaSetMarkerDef) }, - .{ .name = "SetMarkerDefSync", .func = @intFromPtr(&markers.luaSetMarkerDefSync) }, .{ .name = "ClearMarkerDef", .func = @intFromPtr(&markers.luaClearMarkerDef) }, .{ .name = "GetMarkerDef", .func = @intFromPtr(&markers.luaGetMarkerDef) }, diff --git a/src/markers/addon/Markers.lua b/src/markers/addon/Markers.lua index d1a974b..e59cd4b 100644 --- a/src/markers/addon/Markers.lua +++ b/src/markers/addon/Markers.lua @@ -10,13 +10,22 @@ BINDING_HEADER_WORLDMARKERS = "World Markers" -- ============================================================================= local function log(msg) - DEFAULT_CHAT_FRAME:AddMessage("|cff88aaff[WMark]|r " .. msg) + -- DEFAULT_CHAT_FRAME:AddMessage("|cff88aaff[WMark]|r " .. msg) end -- ============================================================================= -- 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) +-- SD — sync done (ends sync mode on requester) +-- -- Permission model: ALL permission checks are enforced DLL-side. -- WorldMarker/ClearWorldMarker: DLL checks local player is leader/assist. -- SetMarkerDef/ClearMarkerDef: DLL checks sender name against roster. @@ -35,7 +44,58 @@ local function getChannel() return nil end +-- ============================================================================= +-- Sync state: after sending SR/LSR, accept SF from the first responder only. +-- Cleared by SD from the locked sender, or by a 5s fallback timer. +-- Also cleared 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 + log("sync timeout (5s), clearing sync state") + 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 @@ -44,6 +104,7 @@ local function broadcastPlace(index, x, y, z, areaId) end local function broadcastClear(index) + clearSyncState() local ch = getChannel() if not ch then return end local msg = "C:" .. index @@ -52,6 +113,7 @@ local function broadcastClear(index) end local function broadcastClearAll() + clearSyncState() local ch = getChannel() if not ch then return end log("SEND [" .. ch .. "] CA") @@ -71,6 +133,8 @@ local function broadcastAllDefs() count = count + 1 end end + log("SEND [" .. ch .. "] SD") + SendAddonMessage(MSG_PREFIX, "SD", ch) log("syncAll: " .. count .. " on " .. ch) end @@ -135,17 +199,6 @@ 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 -- All mutation commands pass sender name to the DLL for permission check. @@ -195,7 +248,11 @@ local function onAddonMessage(prefix, message, channel, sender) broadcastAllDefs() return elseif cmd == "SF" then - -- Sync response: only accept from the first responder + -- Sync fill: only process if we're in sync mode + if not syncing then + log(" ignoring SF (not syncing)") + return + end if syncSender == nil then syncSender = sender log(" sync responder locked: " .. sender) @@ -205,11 +262,18 @@ local function onAddonMessage(prefix, message, channel, sender) end local idx, x, y, z, areaId = parseMarkerFields(parts) if idx then - WorldMarkers.SetMarkerDefSync(idx, x, y, z, areaId, sender) + WorldMarkers.SetMarkerDef(idx, x, y, z, areaId, sender) else log(" PARSE FAIL") end return + elseif cmd == "SD" then + -- Sync done: clear sync state if from the locked sender + if syncing and (syncSender == nil or syncSender == sender) then + log(" sync done from " .. tostring(sender)) + clearSyncState() + end + return end -- P, C, CA — DLL checks sender permission via SetMarkerDef/ClearMarkerDef @@ -242,8 +306,7 @@ local function broadcastSyncRequest() log("syncReq: no channel (not in group)") return end - -- Reset first-responder lock before requesting - syncSender = nil + startSyncing() local cmd = CanSetWorldMarkers() and "LSR" or "SR" log("SEND [" .. ch .. "] " .. cmd) SendAddonMessage(MSG_PREFIX, cmd, ch) @@ -251,23 +314,13 @@ 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. -syncTimer = CreateFrame("Frame") -syncTimer.elapsed = 0 -syncTimer.pending = false -syncTimer:Hide() -syncTimer:SetScript("OnUpdate", function() - syncTimer.elapsed = syncTimer.elapsed + arg1 - if syncTimer.elapsed >= 5 then - syncTimer:Hide() - syncTimer.pending = false +local function scheduleSyncRequest() + syncTimer.elapsed = 0 + syncTimer.delay = 5 + syncTimer.callback = function() log("login sync timer fired") broadcastSyncRequest() end -end) - -local function scheduleSyncRequest() - syncTimer.elapsed = 0 - syncTimer.pending = true syncTimer:Show() log("sync request scheduled (5s)") end diff --git a/src/markers/markers.zig b/src/markers/markers.zig index f91f557..ccea640 100644 --- a/src/markers/markers.zig +++ b/src/markers/markers.zig @@ -15,7 +15,6 @@ //! //! Lua API (WorldMarkers table — internal, used by addon): //! WorldMarkers.SetMarkerDef(i, x, y, z, area, sender) -//! WorldMarkers.SetMarkerDefSync(i, x, y, z, area, sender) //! WorldMarkers.ClearMarkerDef([index,] sender) //! WorldMarkers.GetMarkerDef(index) — returns x, y, z, areaId or nil @@ -225,42 +224,6 @@ fn senderHasPermission(sender: [*:0]const u8) bool { return std.mem.eql(u8, std.mem.span(leader_name), sender_span); } -/// Check if a named sender is in the group (any rank). -/// Weaker than senderHasPermission — used for sync relay (SF) where the -/// sender is just echoing stored data, not issuing a command. -fn senderInGroup(sender: [*:0]const u8) bool { - const sender_span = std.mem.span(sender); - if (sender_span.len == 0) return false; - - const raid_count = hook.readMem(u32, o.RAID_MEMBER_COUNT); - if (raid_count > 0) { - // Raid: find sender anywhere in roster (any rank) - const count = @min(raid_count, 40); - for (0..count) |i| { - const entry = hook.readMem(u32, o.RAID_ROSTER_ARRAY + i * 4); - if (entry == 0 or entry < 0x10000) continue; - const guid_lo = hook.readMem(u32, entry); - const guid_hi = hook.readMem(u32, entry + 4); - const name = getNameFromGUID(guid_lo, guid_hi) orelse continue; - if (std.mem.eql(u8, std.mem.span(name), sender_span)) { - return true; - } - } - return false; - } - - // Party: check if sender is any party member - for (0..4) |i| { - const guid_lo = hook.readMem(u32, o.PARTY_MEMBER_GUIDS + i * 8); - const guid_hi = hook.readMem(u32, o.PARTY_MEMBER_GUIDS + i * 8 + 4); - if (guid_lo == 0 and guid_hi == 0) continue; - const name = getNameFromGUID(guid_lo, guid_hi) orelse continue; - if (std.mem.eql(u8, std.mem.span(name), sender_span)) { - return true; - } - } - return false; -} // ============================================================================= // Position helpers @@ -710,47 +673,6 @@ pub fn luaSetMarkerDef(L: lua.State) callconv(.c) u32 { return 0; } -/// Lua: SetMarkerDefSync(index, x, y, z, areaId, senderName) -/// Like SetMarkerDef but for sync relay (SF messages). Two checks: -/// 1. Local player must be leader/assist (only they request syncs) -/// 2. Sender must be in the group (any rank — they're just relaying data) -pub fn luaSetMarkerDefSync(L: lua.State) callconv(.c) u32 { - const nargs = lua.gettop(L); - if (nargs < 6) return 0; - if (!lua.isnumber(L, 1) or !lua.isnumber(L, 2) or !lua.isnumber(L, 3) or !lua.isnumber(L, 4) or !lua.isnumber(L, 5) or !lua.isstring(L, 6)) return 0; - - if (!canSetMarkers()) { - con.print("[worldmarkers] SetMarkerDefSync: local player not leader/assist\n"); - return 0; - } - - const sender = lua.tostring(L, 6) orelse return 0; - if (!senderInGroup(sender)) { - con.fmt("[worldmarkers] SetMarkerDefSync: sender '{s}' not in group\n", .{std.mem.span(sender)}); - return 0; - } - - const raw_index = @as(i32, @intFromFloat(lua.tonumber(L, 1))); - if (raw_index < 1 or raw_index > NUM_MARKERS) return 0; - const index: usize = @intCast(raw_index - 1); - - const x: f32 = @floatCast(lua.tonumber(L, 2)); - const y: f32 = @floatCast(lua.tonumber(L, 3)); - const z: f32 = @floatCast(lua.tonumber(L, 4)); - const area_id: u32 = @intFromFloat(lua.tonumber(L, 5)); - - clearEntity(index); - - marker_defs[index] = .{ - .pos = .{ .x = x, .y = y, .z = z }, - .area_id = area_id, - .active = true, - }; - - con.fmt("[worldmarkers] SetMarkerDefSync [{d}] at {d:.1},{d:.1},{d:.1} area={d}\n", .{ index + 1, x, y, z, area_id }); - return 0; -} - /// Lua: ClearMarkerDef(senderName) — clear all /// Lua: ClearMarkerDef(index, senderName) — clear one /// senderName is verified against the group roster for leader/officer permission.