local _, shared = ... ---@cast shared HeimdallShared local ModuleName = "Whoer" ---@diagnostic disable-next-line: missing-fields shared.Whoer = {} function shared.Whoer.Init() if not Heimdall_Data.who then Heimdall_Data.who = {} end if not Heimdall_Data.who.data then Heimdall_Data.who.data = {} end local whoWaiting = false ---@type table HeimdallStinkies = {} ---@class Player ---@field name string ---@field guild string ---@field race string ---@field class string ---@field zone string ---@field lastSeenInternal number ---@field lastSeen string ---@field firstSeen string ---@field seenCount number ---@field stinky boolean? Player = { ---@param name string ---@param guild string ---@param race string ---@param class string ---@param zone string ---@return Player new = function(name, guild, race, class, zone) local self = setmetatable({}, { __index = Player, }) self.name = name self.guild = guild self.race = race self.class = class self.zone = zone self.lastSeenInternal = GetTime() self.lastSeen = "never" self.firstSeen = "never" self.seenCount = 0 return self end, ---@return string ToString = function(self) local out = string.format( "%s %s %s\nFirst: %s Last: %s Seen: %3d", shared.padString(self.name, 16, true), shared.padString(self.guild, 26, false), shared.padString(self.zone, 26, false), shared.padString(self.firstSeen, 10, true), shared.padString(self.lastSeen, 10, true), self.seenCount ) return string.format("|cFF%s%s|r", shared.classColors[self.class], out) end, } ---@class WHOQuery ---@field query string ---@field filters WHOFilter[] WHOQuery = { ---@param query string ---@param filters WHOFilter[] ---@return WHOQuery new = function(query, filters) local self = setmetatable({}, { __index = WHOQuery, }) self.query = query self.filters = filters return self end, } ---@class WHOFilter ---@field Run fun(name: string, guild: string, level: number, race: string, class: string, zone: string): boolean ---@field key string ---@type WHOFilter local NotSiegeOfOrgrimmarFilter = { Run = function(name, guild, level, race, class, zone) if not zone then return false end return zone ~= "Siege of Orgrimmar" end, key = "notsoo", } ---@type WHOFilter local AllianceFilter = { Run = function(name, guild, level, race, class, zone) if not race then return false end if not shared.raceMap[race] then return false end return shared.raceMap[race] == "Alliance" end, key = "ally", } ---@class WhoQueryService ---@field queries WHOQuery[] ---@field filters WHOFilter[] ---@field getFilter fun(key: string): WHOFilter? ---@field WhoQueryToString fun(query: WHOQuery): string ---@field WhoQueryFromString fun(query: string): WHOQuery ---@field WhoQueriesToString fun(queries: WHOQuery[]): string ---@field WhoQueriesFromString fun(queries: string): WHOQuery[] shared.WhoQueryService = { queries = {}, filters = { NotSiegeOfOrgrimmarFilter, AllianceFilter, }, ---@param key string ---@return WHOFilter? getFilter = function(key) for _, filter in pairs(shared.WhoQueryService.filters) do if filter.key == key then return filter end end return nil end, ---@param query WHOQuery ---@return string WhoQueryToString = function(query) local ret = "" ret = ret .. query.query ret = ret .. ";" for _, filter in pairs(query.filters) do ret = ret .. filter.key .. ";" end return ret end, ---@param queries WHOQuery[] ---@return string WhoQueriesToString = function(queries) local ret = "" for _, query in pairs(queries) do ret = ret .. shared.WhoQueryService.WhoQueryToString(query) .. "\n" end return ret end, ---@param query string ---@return WHOQuery WhoQueryFromString = function(query) local queryParts = shared.Split(query, ";") local filters = {} for _, filterKey in pairs(queryParts) do local filter = shared.WhoQueryService.getFilter(filterKey) if not filter then if Heimdall_Data.config.who.debug then print(string.format("[%s] Filter %s not found", ModuleName, filterKey)) end else if Heimdall_Data.config.who.debug then print(string.format("[%s] Filter %s found", ModuleName, filterKey)) end table.insert(filters, filter) end end if Heimdall_Data.config.who.debug then print(string.format("[%s] WHO query: %s with %d filters", ModuleName, queryParts[1], #filters)) end shared.dump(filters) return WHOQuery.new(queryParts[1], filters) end, ---@param queryStr string ---@return WHOQuery[] WhoQueriesFromString = function(queryStr) local queries = shared.Split(queryStr, "\n") local ret = {} for _, query in pairs(queries) do table.insert(ret, shared.WhoQueryService.WhoQueryFromString(query)) end return ret end, } shared.WhoQueryService.queries = shared.WhoQueryService.WhoQueriesFromString(Heimdall_Data.config.who.queries) ---@param inputZone string ---@return boolean shared.Whoer.ShouldNotifyForZone = shared.Memoize(function(inputZone) if not Heimdall_Data.config.who.debug then print(string.format("[%s] ShouldNotifyForZone %s", ModuleName, inputZone)) end for zone, _ in pairs(Heimdall_Data.config.who.zoneNotifyFor) do if Heimdall_Data.config.who.debug then print(string.format("[%s] Checking zone %s", ModuleName, zone)) end if zone == "*" then return true end if string.find(inputZone, zone) then if not Heimdall_Data.config.who.debug then print( string.format("[%s] ShouldNotifyForZone %s is true thanks to %s", ModuleName, inputZone, zone) ) end return true end end if not Heimdall_Data.config.who.debug then print(string.format("[%s] ShouldNotifyForZone %s is false", ModuleName, inputZone)) end return false end) -----@type WHOQuery[] --local whoQueries = { -- WHOQuery.new("g-\"БеспредеЛ\"", {}), -- WHOQuery.new("g-\"ЗАО бещёки\"", {}), -- WHOQuery.new("g-\"КОНИЛИНГУСЫ\"", {}), -- --WHOQuery.new("g-\"Dovahkin\"", {}), -- WHOQuery.new( -- "z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" r-\"Human\" r-\"Dwarf\" r-\"Night Elf\"", -- { NotSiegeOfOrgrimmarFilter, AllianceFilter }), -- WHOQuery.new( -- "z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" r-\"Gnome\" r-\"Draenei\" r-\"Worgen\"", -- { NotSiegeOfOrgrimmarFilter, AllianceFilter }), -- WHOQuery.new( -- "z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" r-\"Kul Tiran\" r-\"Dark Iron Dwarf\" r-\"Void Elf\"", -- { NotSiegeOfOrgrimmarFilter, AllianceFilter }), -- WHOQuery.new( -- "z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" r-\"Lightforged Draenei\" r-\"Mechagnome\"", -- { NotSiegeOfOrgrimmarFilter, AllianceFilter }), -- WHOQuery.new("Kekv Firobot Tomoki Mld Alltros", {}) --} local whoQueryIdx = 1 ---@type WHOQuery? local lastQuery = nil ---@param player Player ---@return string? local function Notify(player) if Heimdall_Data.config.who.debug then print(string.format("[%s] Processing notification for player: %s", ModuleName, player.name)) print( string.format( "[%s] Player details - Guild: %s, Race: %s, Class: %s, Zone: %s", ModuleName, player.guild, player.race, player.class, player.zone ) ) print( string.format( "[%s] Player history - First seen: %s, Last seen: %s, Seen count: %d", ModuleName, player.firstSeen, player.lastSeen, player.seenCount ) ) end if not Heimdall_Data.config.who.enabled then if Heimdall_Data.config.who.debug then print(string.format("[%s] Module disabled, skipping notification", ModuleName)) end return end if not player then if Heimdall_Data.config.who.debug then print(string.format("[%s] Error: Cannot notify for nil player", ModuleName)) end return string.format("Cannot notify for nil player %s", tostring(player)) end if not shared.Whoer.ShouldNotifyForZone(player.zone) then --if not Heimdall_Data.config.who.zoneNotifyFor[player.zone] then if Heimdall_Data.config.who.debug then print( string.format("[%s] Skipping notification - Zone '%s' not in notify list", ModuleName, player.zone) ) end return string.format("Not notifying for zone %s", tostring(player.zone)) end for _, channel in pairs(Heimdall_Data.config.who.channels) do local locale = shared.GetLocaleForChannel(channel) local text = string.format( shared._L("whoerNew", locale), player.name, player.stinky and "(!!!!)" or "", shared._L(player.class, locale), --shared._L(player.race, locale), shared._L(shared.raceMap[player.race] or "unknown", locale), player.guild, shared._L(player.zone, locale) ) ---@type Message local msg = { channel = "C", data = channel, message = text, } if Heimdall_Data.config.who.debug then print(string.format("[%s] Queuing channel notification", ModuleName)) shared.dump(msg) end table.insert(shared.networkMessenger.queue, msg) end --if Heimdall_Data.config.who.doWhisper then -- if Heimdall_Data.config.who.debug then -- print(string.format("[%s] Processing whisper notifications for %d recipients", ModuleName, -- #Heimdall_Data.config.whisperNotify)) -- end -- for _, name in pairs(Heimdall_Data.config.whisperNotify) do -- ---@type Message -- local msg = { -- channel = "W", -- data = name, -- message = text -- } -- if Heimdall_Data.config.who.debug then -- print(string.format("[%s] Queuing whisper to %s", ModuleName, name)) -- end -- --table.insert(shared.messenger.queue, msg) -- table.insert(shared.networkMessenger.queue, msg) -- end --end return nil end ---@param player Player ---@param zone string ---@return string? local function NotifyZoneChanged(player, zone) if not Heimdall_Data.config.who.enabled then return end if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end --if not Heimdall_Data.config.who.zoneNotifyFor[zone] -- and not Heimdall_Data.config.who.zoneNotifyFor[player.zone] then if not shared.Whoer.ShouldNotifyForZone(zone) and not shared.Whoer.ShouldNotifyForZone(player.zone) then return string.format("Not notifying for zones %s and %s", tostring(zone), tostring(player.zone)) end for _, channel in pairs(Heimdall_Data.config.who.channels) do local locale = shared.GetLocaleForChannel(channel) local text = string.format( shared._L("whoerMoved", locale), player.name, player.stinky and "(!!!!)" or "", shared._L(player.class, locale), --shared._L(player.race, locale), shared._L(shared.raceMap[player.race] or "unknown", locale), player.guild, shared._L(zone, locale) ) ---@type Message local msg = { channel = "C", data = channel, message = text, } if Heimdall_Data.config.who.debug then print(string.format("[%s] Queuing channel notification", ModuleName)) shared.dump(msg) end table.insert(shared.networkMessenger.queue, msg) end --if Heimdall_Data.config.who.doWhisper then -- for _, name in pairs(Heimdall_Data.config.whisperNotify) do -- ---@type Message -- local msg = { -- channel = "W", -- data = name, -- message = text -- } -- --table.insert(shared.messenger.queue, msg) -- table.insert(shared.networkMessenger.queue, msg) -- end --end return nil end ---@param player Player ---@return string? local function NotifyGone(player) if not Heimdall_Data.config.who.enabled then return end if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end --if not Heimdall_Data.config.who.zoneNotifyFor[player.zone] then if not shared.Whoer.ShouldNotifyForZone(player.zone) then return string.format("Not notifying for zone %s", tostring(player.zone)) end for _, channel in pairs(Heimdall_Data.config.who.channels) do local locale = shared.GetLocaleForChannel(channel) local text = string.format( shared._L("whoerGone", locale), player.name, player.stinky and "(!!!!)" or "", shared._L(player.class, locale), --shared._L(player.race, locale), shared._L(shared.raceMap[player.race] or "unknown", locale), player.guild, shared._L(player.zone, locale) ) ---@type Message local msg = { channel = "C", data = channel, message = text, } if Heimdall_Data.config.who.debug then print(string.format("[%s] Queuing channel notification", ModuleName)) shared.dump(msg) end --table.insert(shared.messenger.queue, msg) table.insert(shared.networkMessenger.queue, msg) end --if Heimdall_Data.config.who.doWhisper then -- for _, name in pairs(Heimdall_Data.config.whisperNotify) do -- ---@type Message -- local msg = { -- channel = "W", -- data = name, -- message = text -- } -- --table.insert(shared.messenger.queue, msg) -- table.insert(shared.networkMessenger.queue, msg) -- end --end return nil end local frame = CreateFrame("Frame") frame:RegisterEvent("WHO_LIST_UPDATE") frame:SetScript("OnEvent", function(self, event, ...) if Heimdall_Data.config.who.debug then print(string.format("[%s] WHO list update received", ModuleName)) print(string.format("[%s] Query index: %d/%d", ModuleName, whoQueryIdx, #shared.WhoQueryService.queries)) end if not Heimdall_Data.config.who.enabled then if Heimdall_Data.config.who.debug then print(string.format("[%s] Module disabled, ignoring WHO update", ModuleName)) end return end ---@type WHOQuery? local query = lastQuery if not query then if Heimdall_Data.config.who.debug then print(string.format("[%s] Error: No active WHO query found", ModuleName)) end return end local results = GetNumWhoResults() if Heimdall_Data.config.who.debug then print(string.format("[%s] Processing %d WHO results for query: %s", ModuleName, results, query.query)) end for i = 1, results do local name, guild, level, race, class, zone = GetWhoInfo(i) if Heimdall_Data.config.who.debug then print( string.format("[%s] Processing result %d/%d: %s/%s/%s", ModuleName, i, results, name, class, zone) ) end local continue = false ---@type WHOFilter[] local filters = query.filters for _, filter in pairs(filters) do if Heimdall_Data.config.who.debug then print( string.format("[%s] Running filter %s on %s/%s/%s", ModuleName, filter.key, name, class, zone) ) end if not filter.Run(name, guild, level, race, class, zone) then if Heimdall_Data.config.who.debug then print( string.format("[%s] Player %s filtered out by WHO filter %s", ModuleName, name, filter.key) ) end continue = true break end end if Heimdall_Data.config.who.ignored[name] then if Heimdall_Data.config.who.debug then print(string.format("[%s] Ignoring blacklisted player: %s", ModuleName, name)) end continue = true end if Heimdall_Data.config.who.debug then print(string.format("[%s] Player %s is not blacklisted", ModuleName, name)) end if not continue then if Heimdall_Data.config.who.debug then print(string.format("[%s] Player %s is not filtered out", ModuleName, name)) end local timestamp = date("%Y-%m-%dT%H:%M:%S") local player = HeimdallStinkies[name] if not player then if Heimdall_Data.config.who.debug then print(string.format("[%s] New player detected: %s (%s) in %s", ModuleName, name, class, zone)) end player = Player.new(name, guild, race, class, zone) if not Heimdall_Data.who then Heimdall_Data.who = {} end if not Heimdall_Data.who.data then Heimdall_Data.who.data = {} end local existing = Heimdall_Data.who.data[name] if existing then if Heimdall_Data.config.who.debug then print( string.format( "[%s] Found existing data for %s - Last seen: %s, Count: %d", ModuleName, name, existing.lastSeen or "never", existing.seenCount or 0 ) ) end player.lastSeen = existing.lastSeen or "never" player.firstSeen = existing.firstSeen or "never" player.seenCount = existing.seenCount or 0 end if player.firstSeen == "never" then player.firstSeen = timestamp if Heimdall_Data.config.who.debug then print(string.format("[%s] First time seeing player: %s at %s", ModuleName, name, timestamp)) end end local stinky = shared.IsStinky(name) if stinky then if Heimdall_Data.config.who.debug then print(string.format("[%s] Player %s marked as stinky!", ModuleName, name)) end player.stinky = true --PlaySoundFile("Interface\\Sounds\\Domination.ogg", "Master") -- else -- PlaySoundFile("Interface\\Sounds\\Cloak.ogg", "Master") end local err = Notify(player) if err then print( string.format("[%s] Error notifying for %s: %s", ModuleName, tostring(name), tostring(err)) ) end player.lastSeen = timestamp player.seenCount = player.seenCount + 1 HeimdallStinkies[name] = player end player.lastSeenInternal = GetTime() if player.zone ~= zone then if Heimdall_Data.config.who.debug then print( string.format( "[%s] Player %s zone changed from %s to %s", ModuleName, name, player.zone, zone ) ) end local err = NotifyZoneChanged(player, zone) if err then print(string.format("Error notifying for %s: %s", tostring(name), tostring(err))) end end player.zone = zone player.lastSeen = timestamp HeimdallStinkies[name] = player if not Heimdall_Data.who then Heimdall_Data.who = {} end if not Heimdall_Data.who.data then Heimdall_Data.who.data = {} end Heimdall_Data.who.data[name] = player end end -- Turns out WA cannot do this ( -- aura_env.UpdateMacro() -- We MAY not need this -- _G["FriendsFrameCloseButton"]:Click() end) do local function UpdateStinkies() for name, player in pairs(HeimdallStinkies) do if player.lastSeenInternal + Heimdall_Data.config.who.ttl < GetTime() then NotifyGone(player) --PlaySoundFile("Interface\\Sounds\\Uncloak.ogg", "Master") HeimdallStinkies[name] = nil end end end local function Tick() UpdateStinkies() C_Timer.NewTimer(0.5, Tick, 1) end Tick() end do local function DoQuery() if not Heimdall_Data.config.who.enabled then return end local query = shared.WhoQueryService.queries[whoQueryIdx] if not query then if Heimdall_Data.config.who.debug then print(string.format("[%s] Error: No WHO query found to run at index %d", ModuleName, whoQueryIdx)) end return end if Heimdall_Data.config.who.debug then print( string.format( "[%s] Running WHO query %d/%d: %s", ModuleName, whoQueryIdx, #shared.WhoQueryService.queries, query.query ) ) print(string.format("[%s] Query has %d filters", ModuleName, #query.filters)) for i, filter in ipairs(query.filters) do print(string.format("[%s] Filter %d: %s", ModuleName, i, filter.key)) end end whoQueryIdx = whoQueryIdx + 1 if whoQueryIdx > #shared.WhoQueryService.queries then whoQueryIdx = 1 end lastQuery = query ---@diagnostic disable-next-line: param-type-mismatch SetWhoToUI(1) SetWhoToUI(1) SendWho(query.query) whoWaiting = true end local function Tick() DoQuery() C_Timer.NewTimer(1, Tick, 1) end Tick() local original_FriendsFrame_OnEvent = FriendsFrame_OnEvent local function my_FriendsFrame_OnEvent(event) if not (event == "WHO_LIST_UPDATE" and whoWaiting) then original_FriendsFrame_OnEvent() end end FriendsFrame_OnEvent = my_FriendsFrame_OnEvent end print("[Heimdall] Whoer loaded") end