diff --git a/Heimdall.lua b/Heimdall.lua index 60ac6b1..9909da9 100644 --- a/Heimdall.lua +++ b/Heimdall.lua @@ -1,12 +1,19 @@ local _, data = ... ---@cast data HeimdallData +---@class Heimdall_Data +---@field who { data: table } if not Heimdall_Data then Heimdall_Data = {} end +-- We don't care about these persisting +-- Actually we don't want some of them to persist +-- For those we DO we use (global) Heimdall_Data + ---@class HeimdallData ---@field config HeimdallConfig ---@field raceMap table ----@field stinkies table +---@field classColors table +---@field stinkies table ---@field messenger HeimdallMessengerData ---@field who HeimdallWhoData ---@field dumpTable fun(table: any, depth?: number): nil @@ -31,6 +38,10 @@ if not Heimdall_Data then Heimdall_Data = {} end ---@class HeimdallWhoConfig ---@field enabled boolean +---@field ignored table +---@field notifyChannel string +---@field zoneNotifyFor table +---@field ttl number ---@class HeimdallMessengerConfig ---@field enabled boolean @@ -41,7 +52,9 @@ if not Heimdall_Data then Heimdall_Data = {} end ---@field ticker number? ---@class HeimdallWhoData ----@field ticker number? +---@field updateTicker number? +---@field whoTicker number? +---@field ignored table data.messenger = { queue = {} @@ -59,7 +72,18 @@ data.config = { throttleTime = 1 }, who = { - enabled = true + enabled = true, + ignored = {}, + notifyChannel = "Foobar", + ttl = 10, + zoneNotifyFor = { + ["Orgrimmar"] = true, + ["Thunder Bluff"] = true, + ["Undercity"] = true, + ["Durotar"] = true, + ["Echo Isles"] = true, + ["Valley of Trials"] = true, + } }, messenger = { enabled = true @@ -90,6 +114,22 @@ data.raceMap = { ["Mag'har Orc"] = "Horde" } +---@type table +data.classColors = { + ["Warrior"] = "C69B6D", + ["Paladin"] = "F48CBA", + ["Hunter"] = "AAD372", + ["Rogue"] = "FFF468", + ["Priest"] = "FFFFFF", + ["Death Knight"] = "C41E3A", + ["Shaman"] = "0070DD", + ["Mage"] = "3FC7EB", + ["Warlock"] = "8788EE", + ["Monk"] = "00FF98", + ["Druid"] = "FF7C0A", + ["Demon Hunter"] = "A330C9" +} + data.stinkies = {} ---@param input string diff --git a/Whoer.lua b/Whoer.lua new file mode 100644 index 0000000..928f541 --- /dev/null +++ b/Whoer.lua @@ -0,0 +1,333 @@ +local _, data = ... +---@cast data HeimdallData + +if not data.config.who.enabled then return end + +---@type table +local players = {} + +---@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 +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 and guild %s in %s, first seen: %s, last seen: %s, times seen: %d", + self.name, + self.class, + 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 + local text = player:NotifyMessage() + ---@type Message + local msg = { + channel = "CHANNEL", + data = data.config.who.notifyChannel, + message = text + } + table.insert(data.messenger.queue, msg) + 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) + return nil +end +---@param player Player +---@return string? +local function NotifyGone(player) + 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) + return nil +end + +local frame = CreateFrame("Frame") +frame:RegisterEvent("WHO_LIST_UPDATE") +frame:SetScript("OnEvent", function(self, event, ...) + for i = 1, GetNumWhoResults() do + local name, guild, level, race, class, zone = GetWhoInfo(i) + if data.who.ignored[name] then return end + + ---@type WHOQuery? + local query = lastQuery + if not query then + print("No query wtf?") + return + end + + ---@type WHOFilter[] + local filters = query.filters + for _, filter in pairs(filters) do + if not filter(name, guild, level, race, class, zone) then + return + end + end + + local timestamp = date("%Y-%m-%dT%H:%M:%S") + local player = players[name] + if not player then + player = Player.new(name, guild, race, class, zone) + 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.stinkies[name] + if stinky then + 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 + players[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 + players[name] = player + end + -- Turns out WA cannot do this ( + -- aura_env.UpdateMacro() + _G["FriendsFrameCloseButton"]:Click() +end) + +if not data.who.updateTicker then + data.who.updateTicker = C_Timer.NewTicker(0.5, function() + for name, player in pairs(players) do + if player.lastSeenInternal + data.config.who.ttl < GetTime() then + NotifyGone(player) + PlaySoundFile("Interface\\Sounds\\Uncloak.ogg", "Master") + players[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 + 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(players) 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(players) do + local text = player:NotifyMessage() + ---@type Message + local msg = { + channel = "CHANNEL", + data = sender, + message = text + } + table.insert(data.messenger.queue, msg) + end + end +end)