local addonname, data = ... ---@cast data HeimdallData ---@cast addonname string data.Whoer = {} function data.Whoer.Init() if not data.config.who.enabled then print("Heimdall - Whoer disabled") return end 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", data.padString(self.name, 16, true), data.padString(self.guild, 26, false), data.padString(self.zone, 26, false), data.padString(self.firstSeen, 10, true), data.padString(self.lastSeen, 10, true), self.seenCount) return string.format("|cFF%s%s|r", data.classColors[self.class], out) end, ---@return string NotifyMessage = function(self) local text = string.format( "%s of class %s, race %s (%s) and guild %s in %s, first seen: %s, last seen: %s, times seen: %d", self.name, self.class, self.race, tostring(data.raceMap[self.race]), 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 data.raceMap[race] then return false end return data.raceMap[race] == "Alliance" end local whoQueryIdx = 1 ---@type table local whoQueries = { WHOQuery.new("g-\"БеспредеЛ\"", {}), WHOQuery.new( "z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" z-\"Echo Isles\" r-\"Human\" r-\"Dwarf\" r-\"Night Elf\"", { NotSiegeOfOrgrimmarFilter, AllianceFilter }), WHOQuery.new( "z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" z-\"Echo Isles\" r-\"Gnome\" r-\"Draenei\" r-\"Worgen\"", { NotSiegeOfOrgrimmarFilter, AllianceFilter }), WHOQuery.new( "z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" z-\"Echo Isles\" r-\"Kul Tiran\" r-\"Dark Iron Dwarf\" r-\"Void Elf\"", { NotSiegeOfOrgrimmarFilter, AllianceFilter }), WHOQuery.new( "z-\"Orgrimmar\" z-\"Durotar\" z-\"Valley of Trials\" z-\"Echo Isles\" r-\"Lightforged Draenei\" r-\"Mechagnome\"", { NotSiegeOfOrgrimmarFilter, AllianceFilter }), WHOQuery.new("Kekv Demonboo Dotmada Firobot Verminal", {}) } local queryPending = false local ttl = #whoQueries * 2 ---@type WHOQuery? local lastQuery = nil ---@param player Player ---@return string? local function Notify(player) if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end if not data.config.who.zoneNotifyFor[player.zone] then return string.format("Not notifying for zone %s", tostring(player.zone)) end local text = player:NotifyMessage() ---@type Message local msg = { channel = "CHANNEL", data = data.config.who.notifyChannel, message = text } table.insert(data.messenger.queue, msg) if data.config.who.doWhisper then for _, name in pairs(data.config.whisperNotify) do ---@type Message local msg = { channel = "WHISPER", data = name, message = text } table.insert(data.messenger.queue, msg) end end return nil end ---@param player Player ---@param zone string ---@return string? local function NotifyZoneChanged(player, zone) if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end if not data.config.who.zoneNotifyFor[zone] and not 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("%s of class %s and guild %s moved to %s", player.name, player.class, player.guild, zone) ---@type Message local msg = { channel = "CHANNEL", data = data.config.who.notifyChannel, message = text } table.insert(data.messenger.queue, msg) if data.config.who.doWhisper then for _, name in pairs(data.config.whisperNotify) do ---@type Message local msg = { channel = "WHISPER", data = name, message = text } table.insert(data.messenger.queue, msg) end end return nil end ---@param player Player ---@return string? local function NotifyGone(player) if not player then return string.format("Cannot notify for nil player %s", tostring(player)) end if not data.config.who.zoneNotifyFor[player.zone] then return string.format("Not notifying for zone %s", tostring(player.zone)) end local text = string.format("%s of class %s and guild %s left %s", player.name, player.class, player.guild, player.zone) ---@type Message local msg = { channel = "CHANNEL", data = data.config.who.notifyChannel, message = text } table.insert(data.messenger.queue, msg) if data.config.who.doWhisper then for _, name in pairs(data.config.whisperNotify) do ---@type Message local msg = { channel = "WHISPER", data = name, message = text } table.insert(data.messenger.queue, msg) end end return nil end local frame = CreateFrame("Frame") frame:RegisterEvent("WHO_LIST_UPDATE") frame:SetScript("OnEvent", function(self, event, ...) ---@type WHOQuery? local query = lastQuery if not query then print("No query wtf?") return end for i = 1, GetNumWhoResults() do local name, guild, level, race, class, zone = GetWhoInfo(i) if data.who.ignored[name] then return 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 -- Mega scuffed, yes... -- But wow does not have gotos continue = true end end if not continue then local timestamp = date("%Y-%m-%dT%H:%M:%S") local player = HeimdallStinkies[name] if not player then 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 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 end local stinky = data.config.stinkies[name] if stinky then 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("Error notifying for %s: %s", 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() queryPending = false end) if not data.who.updateTicker then data.who.updateTicker = C_Timer.NewTicker(0.5, function() for name, player in pairs(HeimdallStinkies) do if player.lastSeenInternal + data.config.who.ttl < GetTime() then NotifyGone(player) PlaySoundFile("Interface\\Sounds\\Uncloak.ogg", "Master") HeimdallStinkies[name] = nil end end end) end if not data.who.whoTicker then data.who.whoTicker = C_Timer.NewTicker(1, function() if queryPending then print("Tried running a who query while one is already pending, previous query:") data.dumpTable(lastQuery) return end queryPending = true 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))) SetWhoToUI(1) SendWho(query.query) end) end local whoQueryWhisperFrame = CreateFrame("Frame") whoQueryWhisperFrame:RegisterEvent("CHAT_MSG_WHISPER") whoQueryWhisperFrame:SetScript("OnEvent", function(self, event, msg, sender) if msg == "who" then for _, player in pairs(HeimdallStinkies) do local text = player:NotifyMessage() ---@type Message local msg = { channel = "WHISPER", data = sender, message = text } table.insert(data.messenger.queue, msg) end end end) local whoQueryChannelFrame = CreateFrame("Frame") whoQueryChannelFrame:RegisterEvent("CHAT_MSG_CHANNEL") whoQueryChannelFrame:SetScript("OnEvent", function(self, event, msg, sender, ...) local channelId = select(6, ...) local channelname = "" ---@type any[] local channels = { GetChannelList() } for i = 1, #channels, 2 do ---@type number local id = channels[i] ---@type string local name = channels[i + 1] if id == channelId then channelname = name end end if channelname ~= data.config.who.notifyChannel then return end if msg == "who" then for _, player in pairs(HeimdallStinkies) do local text = player:NotifyMessage() ---@type Message local msg = { channel = "CHANNEL", data = channelname, message = text } table.insert(data.messenger.queue, msg) end end end) print("Heimdall - Whoer loaded") end