local addonname, shared = ... ---@cast shared HeimdallShared ---@cast addonname string 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 ---@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, ---@return string NotifyMessage = function(self) local text = string.format(shared.L.en.whoerNew, self.name, self.stinky and "(!!!!)" or "", self.class, self.race, tostring(shared.raceMap[self.race]), self.guild, self.zone, self.firstSeen, self.lastSeen, self.seenCount) return text end, ---@return string NotifyRu = function(self) local ruClass = shared.L.ru.classes[self.class] if not ruClass then print(string.format("[%s] Class %s not found in ru.classes", ModuleName, self.class)) end local ruRace = shared.L.ru.races[self.race] if not ruRace then print(string.format("[%s] Race %s not found in ru.races", ModuleName, self.race)) end local faction = shared.raceMap[self.race] local ruFaction = shared.L.ru.factions[faction] if not ruFaction then print(string.format("[%s] Faction %s not found in ru.factions", ModuleName, faction)) end local text = string.format(shared.L.ru.whoerNew, self.name, self.stinky and "(!!!!)" or "", ruClass or self.class, ruRace or self.race, ruFaction or faction, self.guild, self.zone, self.firstSeen, self.lastSeen, self.seenCount) return text 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 } ---@alias WHOFilter fun(name: string, guild: string, level: number, race: string, class: string, zone: string): boolean ---@type WHOFilter local NotSiegeOfOrgrimmarFilter = function(name, guild, level, race, class, zone) if not zone then return false end return zone ~= "Siege of Orgrimmar" end ---@type WHOFilter local AllianceFilter = 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 local whoQueryIdx = 1 ---@type WHOQuery[] local whoQueries = { 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 ttl = #whoQueries * 2 ---@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)) 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 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 local text = player:NotifyMessage() if Heimdall_Data.config.who.debug then print(string.format("[%s] Queuing channel notification: '%s'", ModuleName, text)) end ---@type Message local msg = { channel = "CHANNEL", data = Heimdall_Data.config.who.notifyChannel, message = text } table.insert(shared.messenger.queue, msg) 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 = "WHISPER", 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) 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 return string.format("Not notifying for zones %s and %s", tostring(zone), tostring(player.zone)) end local text = string.format(shared.L.en.whoerMoved, player.name, player.class, player.race, shared.raceMap[player.race] or "Unknown", player.guild, zone) ---@type Message local msg = { channel = "CHANNEL", data = Heimdall_Data.config.who.notifyChannel, message = text } table.insert(shared.messenger.queue, msg) text = string.format(shared.L.ru.whoerMoved, player.name, player.class, player.race, shared.raceMap[player.race] or "Unknown", player.guild, zone) ---@type Message msg = { channel = "CHANNEL", data = Heimdall_Data.config.who.notifyChannel .. "ru", message = text } table.insert(shared.messenger.queue, msg) if Heimdall_Data.config.who.doWhisper then for _, name in pairs(Heimdall_Data.config.whisperNotify) do ---@type Message local msg = { channel = "WHISPER", data = name, message = text } table.insert(shared.messenger.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 return string.format("Not notifying for zone %s", tostring(player.zone)) end local text = string.format(shared.L.en.whoerGone, player.name, player.class, player.guild, player.zone) ---@type Message local msg = { channel = "CHANNEL", data = Heimdall_Data.config.who.notifyChannel, message = text } table.insert(shared.messenger.queue, msg) text = string.format(shared.L.ru.whoerGone, player.name, player.class, player.guild, player.zone) ---@type Message msg = { channel = "CHANNEL", data = Heimdall_Data.config.who.notifyChannel .. "ru", message = text } table.insert(shared.messenger.queue, msg) if Heimdall_Data.config.who.doWhisper then for _, name in pairs(Heimdall_Data.config.whisperNotify) do ---@type Message local msg = { channel = "WHISPER", data = name, message = text } table.insert(shared.messenger.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)) 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 not filter(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", ModuleName, name)) 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 not continue then 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 = Heimdall_Data.config.stinkies[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 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() _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 = whoQueries[whoQueryIdx] whoQueryIdx = whoQueryIdx + 1 if whoQueryIdx > #whoQueries then whoQueryIdx = 1 end lastQuery = query --print(string.format("Running who query: %s", tostring(query.query))) ---@diagnostic disable-next-line: param-type-mismatch SetWhoToUI(1) SendWho(query.query) end local function Tick() DoQuery() C_Timer.NewTimer(1, Tick, 1) end Tick() end print("[Heimdall] Whoer loaded") end