From a0a7e58186ee50f8a3be618b1a29d253d6b8e88a Mon Sep 17 00:00:00 2001 From: Taylor Petrychyn Date: Mon, 17 Apr 2023 19:15:40 -0700 Subject: [PATCH 1/8] wip: deathlog refactor testable --- Dockerfile | 9 + Hardcore_Classic.toc | 4 +- Modules/blah | 0 Modules/deathlog/deathlog.lua | 528 +++++++++++++++++++++++++++++ Modules/deathlog/deathlog_spec.lua | 106 ++++++ Modules/deathlog/init.lua | 3 + Modules/deathlog/ui.lua | 363 ++++++++++++++++++++ Modules/deathlog/ui_mock.lua | 15 + docker-compose.yml | 13 + 9 files changed, 1040 insertions(+), 1 deletion(-) create mode 100644 Dockerfile create mode 100644 Modules/blah create mode 100644 Modules/deathlog/deathlog.lua create mode 100644 Modules/deathlog/deathlog_spec.lua create mode 100644 Modules/deathlog/init.lua create mode 100644 Modules/deathlog/ui.lua create mode 100644 Modules/deathlog/ui_mock.lua create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..1572b08d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +#syntax=docker/dockerfile:1.2 + +FROM akorn/luarocks:lua5.4-alpine AS builder + +RUN apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/testing \ + dumb-init gcc libc-dev + +RUN luarocks install bit32 +RUN luarocks install busted \ No newline at end of file diff --git a/Hardcore_Classic.toc b/Hardcore_Classic.toc index 96fbfab0..bffc871d 100644 --- a/Hardcore_Classic.toc +++ b/Hardcore_Classic.toc @@ -161,7 +161,9 @@ TextureInfo.lua AchievementAlertFrame.lua id_to_npc_classic.lua npc_to_id_classic.lua -DeathLog.lua +Modules/deathlog/deathlog.lua +Modules/deathlog/ui.lua +Modules/deathlog/init.lua Hardcore.lua Libs/GreenWall/Constants.lua diff --git a/Modules/blah b/Modules/blah new file mode 100644 index 00000000..e69de29b diff --git a/Modules/deathlog/deathlog.lua b/Modules/deathlog/deathlog.lua new file mode 100644 index 00000000..c6bff063 --- /dev/null +++ b/Modules/deathlog/deathlog.lua @@ -0,0 +1,528 @@ +local _, addon = ... +local deathlog = {} + +-- check if we are running in wow +if type(addon) == "table" then + deathlog = CreateFrame("Frame", "Hardcore", nil, "BackdropTemplate") + addon.deathlog = deathlog +end + +local COMM_COMMANDS = { + ["BROADCAST_DEATH_PING"] = "1", + ["BROADCAST_DEATH_PING_CHECKSUM"] = "2", + ["LAST_WORDS"] = "3", +} +local COMM_COMMAND_DELIM = "$" +local COMM_FIELD_DELIM = "~" +local HC_REQUIRED_ACKS = 3 +local HC_DEATH_LOG_MAX = 1000 + +local death_alerts_channel = "hcdeathalertschannel" +local death_alerts_channel_pw = "hcdeathalertschannelpw" + +local throttle_player = {} +local shadowbanned = {} + +-- [checksum -> {name, guild, source, race, class, level, F's, location, last_words, location}] +deathlog.death_ping_lru_cache_tbl = {} +deathlog.last_attack_source = "" +deathlog.last_words = "" +deathlog.broadcast_death_ping_queue = {} +deathlog.last_words_queue = {} +deathlog.death_alert_out_queue = {} +local f_log = {} + +function fletcher16(player_name, player_guild, player_level) + local data = player_name .. player_guild .. player_level + local sum1 = 0 + local sum2 = 0 + for index = 1, #data do + sum1 = (sum1 + string.byte(string.sub(data, index, index))) % 255; + sum2 = (sum2 + sum1) % 255; + end + return player_name .. "-" .. bit.bor(bit.lshift(sum2, 8), sum1) +end + +local function isValidEntry(_player_data) + if _player_data == nil then return false end + if _player_data["source_id"] == nil then return false end + if _player_data["race_id"] == nil or tonumber(_player_data["race_id"]) == nil or C_CreatureInfo.GetRaceInfo(_player_data["race_id"]) == nil then return false end + if _player_data["class_id"] == nil or tonumber(_player_data["class_id"]) == nil or GetClassInfo(_player_data["class_id"]) == nil then return false end + if _player_data["level"] == nil or _player_data["level"] < 0 or _player_data["level"] > 80 then return false end + if _player_data["instance_id"] == nil and _player_data["map_id"] == nil then return false end + return true +end + +local function shouldCreateEntry(checksum) + if deathlog.death_ping_lru_cache_tbl[checksum] == nil then return false end + if deathlog.death_ping_lru_cache_tbl[checksum]["player_data"] == nil then return false end + if hardcore_settings.death_log_types == nil or hardcore_settings.death_log_types == "faction_wide" and isValidEntry(deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]) then + if deathlog.death_ping_lru_cache_tbl[checksum]["peer_report"] and deathlog.death_ping_lru_cache_tbl[checksum]["peer_report"] > HC_REQUIRED_ACKS then + return true + else + if debug then + print("not enough peers for " .. + checksum .. ": " .. (deathlog.death_ping_lru_cache_tbl[checksum]["peer_report"] or "0")) + end + end + end + if hardcore_settings.death_log_types ~= nil and hardcore_settings.death_log_types == "greenwall_guilds_only" and deathlog.death_ping_lru_cache_tbl[checksum]["player_data"] and deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]["guild"] and hc_peer_guilds[deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]["guild"]] then return true end + if deathlog.death_ping_lru_cache_tbl[checksum]["in_guild"] then return true end + + return false +end + +function deathlog.alertIfValid(_player_data) + local race_info = C_CreatureInfo.GetRaceInfo(_player_data["race_id"]) + local race_str = race_info.raceName + local class_str, _, _ = GetClassInfo(_player_data["class_id"]) + if class_str and RAID_CLASS_COLORS[class_str:upper()] then + class_str = "|c" .. RAID_CLASS_COLORS[class_str:upper()].colorStr .. class_str .. "|r" + end + + local level_str = tostring(_player_data["level"]) + local level_num = tonumber(_player_data["level"]) + local min_level = tonumber(hardcore_settings.minimum_show_death_alert_lvl) or 0 + if level_num < tonumber(min_level) then + return + end + + local map_info = nil + local map_name = "?" + if _player_data["map_id"] then + map_info = C_Map.GetMapInfo(_player_data["map_id"]) + end + if map_info then + map_name = map_info.name + end + + local msg = _player_data["name"] .. + " the " .. + (race_str or "") .. " " .. (class_str or "") .. " has died at level " .. level_str .. " in " .. map_name + Hardcore:TriggerDeathAlert(msg) +end + +function deathlog.ApplySettings(_settings) + hardcore_settings = _settings + + if hardcore_settings["death_log_show"] == nil or hardcore_settings["death_log_show"] == true then + deathlog.ui.Show() + else + deathlog.ui.Hide() + end + + deathlog.ui.Refresh() +end + +function deathlog:JoinChannel() + JoinChannelByName(death_alerts_channel, death_alerts_channel_pw) + local channel_num = GetChannelName(death_alerts_channel) + if channel_num == 0 then + print("Failed to join death alerts channel") + else + print("Successfully joined deathlog channel.") + end + + for i = 1, 10 do + if _G['ChatFrame' .. i] then + ChatFrame_RemoveChannel(_G['ChatFrame' .. i], death_alerts_channel) + end + end +end + +function deathlog.PlayerData(name, guild, source_id, race_id, class_id, level, instance_id, map_id, map_pos, date, + last_words) + return { + ["name"] = name, + ["guild"] = guild, + ["source_id"] = source_id, + ["race_id"] = race_id, + ["class_id"] = class_id, + ["level"] = level, + ["instance_id"] = instance_id, + ["map_id"] = map_id, + ["map_pos"] = map_pos, + ["date"] = date, + ["last_words"] = last_words, + } +end + +local encodeMessageParams = { + name = "", + guild = "", + source_id = "", + race_id = "", + class_id = -1, + level = "", + instance_id = -1, + map_id = -1, + map_pos = {}, + loc_str = "", + guid = "" +} + +function deathlog.encodeMessage(params) + params = setmetatable(params or {}, { __index = encodeMessageParams }) + if params.name == nil or params.name == "" then return end + -- if guild == nil then return end -- TODO + if tonumber(params.source_id) == nil then return end + if tonumber(params.race_id) == nil then return end + if tonumber(params.level) == nil then return end + + if params.map_pos then + params.loc_str = deathlog.mapPosToString(params.map_pos) + end + + local comm_message = + params.name .. + COMM_FIELD_DELIM .. + (params.guild or "") .. + COMM_FIELD_DELIM .. + params.source_id .. + COMM_FIELD_DELIM .. + params.race_id .. + COMM_FIELD_DELIM .. + params.class_id .. + COMM_FIELD_DELIM .. + params.level .. + COMM_FIELD_DELIM .. + (params.instance_id or "") .. + COMM_FIELD_DELIM .. + (params.map_id or "") .. + COMM_FIELD_DELIM .. + params.loc_str .. + COMM_FIELD_DELIM .. + params.guid .. -- guid added to the end to maintain backwards compatibility + COMM_FIELD_DELIM + return comm_message +end + +function deathlog.decodeMessage(msg) + local values = {} + for w in msg:gmatch("(.-)" .. COMM_FIELD_DELIM) do table.insert(values, w) end + local date = nil + local last_words = nil + local name = values[1] + local guild = values[2] + local source_id = tonumber(values[3]) + local race_id = tonumber(values[4]) + local class_id = tonumber(values[5]) + local level = tonumber(values[6]) + local instance_id = tonumber(values[7]) + local map_id = tonumber(values[8]) + local map_pos = values[9] + local player_data = deathlog.PlayerData(name, guild, source_id, race_id, class_id, level, instance_id, map_id, + map_pos, date, last_words) + return player_data +end + +function deathlog.mapPosToString(map_pos) + return string.format("%.4f,%.4f", map_pos.x, map_pos.y) +end + +function deathlog.createEntry(checksum) + if deathlog.death_ping_lru_cache_tbl[checksum] == nil then + return + end + + deathlog.ui.InsertEntry(deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]) + deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]["date"] = date() + deathlog.death_ping_lru_cache_tbl[checksum]["committed"] = 1 + + -- Record to hardcore_settings + if hardcore_settings["death_log_entries"] == nil then + hardcore_settings["death_log_entries"] = {} + end + table.insert(hardcore_settings["death_log_entries"], deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]) + + -- Cap list size, otherwise loading time will increase + if hardcore_settings["death_log_entries"] and #hardcore_settings["death_log_entries"] > HC_DEATH_LOG_MAX then -- TODO parameterize + table.remove(hardcore_settings["death_log_entries"], 1) + end + + -- Save in-guilds for next part of migration TODO + if deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]["in_guild"] then return end + if hardcore_settings.alert_subset ~= nil and hardcore_settings.alert_subset == "greenwall_guilds_only" and deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]["guild"] and hc_peer_guilds[deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]["guild"]] then + deathlog.alertIfValid(deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]) + return + end + if hardcore_settings.alert_subset ~= nil and hardcore_settings.alert_subset == "faction_wide" then + deathlog.alertIfValid(deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]) + return + end + + -- Override if players are in greenwall + if deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]["guild"] and hc_peer_guilds[deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]["guild"]] then + deathlog.alertIfValid(deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]) + return + end +end + +function deathlog.receiveChannelMessage(sender, data) + if data == nil then return end + local decoded_player_data = deathlog.decodeMessage(data) + if sender ~= decoded_player_data["name"] then return end + if isValidEntry(decoded_player_data) == false then return end + + local checksum = fletcher16(decoded_player_data["name"], decoded_player_data["guild"], decoded_player_data["level"]) + + if deathlog.death_ping_lru_cache_tbl[checksum] == nil then + deathlog.death_ping_lru_cache_tbl[checksum] = {} + end + + if deathlog.death_ping_lru_cache_tbl[checksum]["player_data"] == nil then + deathlog.death_ping_lru_cache_tbl[checksum]["player_data"] = decoded_player_data + end + + if deathlog.death_ping_lru_cache_tbl[checksum]["committed"] then return end + + -- local guildName, guildRankName, guildRankIndex = GetGuildInfo("player"); + -- if decoded_player_data['guild'] == guildName then + -- local name_long = sender .. "-" .. GetNormalizedRealmName() + -- for i = 1, GetNumGuildMembers() do + -- local name, _, _, level, class_str, _, _, _, _, _, class = GetGuildRosterInfo(i) + -- if name_long == name and level == decoded_player_data["level"] then + -- deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]["in_guild"] = 1 + -- local delay = math.random(0, 10) + -- C_Timer.After(delay, function() + -- if deathlog.death_ping_lru_cache_tbl[checksum] and deathlog.death_ping_lru_cache_tbl[checksum]["committed"] then return end + -- table.insert(broadcast_death_ping_queue, checksum) -- Must be added to queue to be broadcasted to network + -- end) + -- break + -- end + -- end + -- end + + deathlog.death_ping_lru_cache_tbl[checksum]["self_report"] = 1 + if shouldCreateEntry(checksum) then + deathlog.createEntry(checksum) + end +end + +function deathlog.receiveChannelMessageChecksum(sender, checksum) + if checksum == nil then return end + if deathlog.death_ping_lru_cache_tbl[checksum] == nil then + deathlog.death_ping_lru_cache_tbl[checksum] = {} + end + if deathlog.death_ping_lru_cache_tbl[checksum]["committed"] then return end + + if deathlog.death_ping_lru_cache_tbl[checksum]["peers"] == nil then + deathlog.death_ping_lru_cache_tbl[checksum]["peers"] = {} + end + + if deathlog.death_ping_lru_cache_tbl[checksum]["peers"][sender] then return end + deathlog.death_ping_lru_cache_tbl[checksum]["peers"][sender] = 1 + + if deathlog.death_ping_lru_cache_tbl[checksum]["peer_report"] == nil then + deathlog.death_ping_lru_cache_tbl[checksum]["peer_report"] = 0 + end + + deathlog.death_ping_lru_cache_tbl[checksum]["peer_report"] = deathlog.death_ping_lru_cache_tbl[checksum] + ["peer_report"] + 1 + if shouldCreateEntry(checksum) then + deathlog.createEntry(checksum) + end +end + +function deathlog:sendNextInQueue() + if #self.broadcast_death_ping_queue > 0 then + local channel_num = GetChannelName(death_alerts_channel) + if channel_num == 0 then + self:JoinChannel() + return + end + + local commMessage = COMM_COMMANDS["BROADCAST_DEATH_PING_CHECKSUM"] .. + COMM_COMMAND_DELIM .. self.broadcast_death_ping_queue[1] + CTL:SendChatMessage("BULK", COMM_NAME, commMessage, "CHANNEL", nil, channel_num) + table.remove(self.broadcast_death_ping_queue, 1) + return + end + + if #self.death_alert_out_queue > 0 then + local channel_num = GetChannelName(death_alerts_channel) + if channel_num == 0 then + self:JoinChannel() + return + end + local commMessage = COMM_COMMANDS["BROADCAST_DEATH_PING"] .. COMM_COMMAND_DELIM .. self.death_alert_out_queue[1] + CTL:SendChatMessage("BULK", COMM_NAME, commMessage, "CHANNEL", nil, channel_num) + table.remove(self.death_alert_out_queue, 1) + return + end + + if #self.last_words_queue > 0 then + local channel_num = GetChannelName(death_alerts_channel) + if channel_num == 0 then + self:JoinChannel() + return + end + local commMessage = COMM_COMMANDS["LAST_WORDS"] .. COMM_COMMAND_DELIM .. self.last_words_queue[1] + CTL:SendChatMessage("BULK", COMM_NAME, commMessage, "CHANNEL", nil, channel_num) + table.remove(self.last_words_queue, 1) + return + end +end + +function deathlog:CHAT_MSG_CHANNEL(...) + local arg = { ... } + local _, channel_name = string.split(" ", arg[4]) + if channel_name ~= death_alerts_channel then return end + local command, msg = string.split(COMM_COMMAND_DELIM, arg[1]) + if command == COMM_COMMANDS["BROADCAST_DEATH_PING_CHECKSUM"] then + local player_name_short, _ = string.split("-", arg[2]) + if shadowbanned[player_name_short] then return end + + if throttle_player[player_name_short] == nil then throttle_player[player_name_short] = 0 end + throttle_player[player_name_short] = throttle_player[player_name_short] + 1 + if throttle_player[player_name_short] > 1000 then + shadowbanned[player_name_short] = 1 + end + + deathlog.receiveChannelMessageChecksum(player_name_short, msg) + if debug then print("checksum", msg) end + return + end + + if command == COMM_COMMANDS["BROADCAST_DEATH_PING"] then + local player_name_short, _ = string.split("-", arg[2]) + if shadowbanned[player_name_short] then return end + + if throttle_player[player_name_short] == nil then throttle_player[player_name_short] = 0 end + throttle_player[player_name_short] = throttle_player[player_name_short] + 1 + if throttle_player[player_name_short] > 1000 then + shadowbanned[player_name_short] = 1 + end + + deathlog.receiveChannelMessage(player_name_short, msg) + if debug then print("death ping", msg) end + return + end + + if command == COMM_COMMANDS["LAST_WORDS"] then + local player_name_short, _ = string.split("-", arg[2]) + if shadowbanned[player_name_short] then return end + + if throttle_player[player_name_short] == nil then throttle_player[player_name_short] = 0 end + throttle_player[player_name_short] = throttle_player[player_name_short] + 1 + if throttle_player[player_name_short] > 1000 then + shadowbanned[player_name_short] = 1 + end + + -- deathlogReceiveLastWords(player_name_short, msg) + if debug then print("last words", msg) end + return + end +end + +function deathlog:COMBAT_LOG_EVENT_UNFILTERED(...) + -- local time, token, hidding, source_serial, source_name, caster_flags, caster_flags2, target_serial, target_name, target_flags, target_flags2, ability_id, ability_name, ability_type, extraSpellID, extraSpellName, extraSchool = CombatLogGetCurrentEventInfo() + local _, ev, _, _, source_name, _, _, target_guid, _, _, _, environmental_type, _, _, _, _, _ = + CombatLogGetCurrentEventInfo() + + if not (source_name == PLAYER_NAME) then + if not (source_name == nil) then + if string.find(ev, "DAMAGE") ~= nil then + self.last_attack_source = source_name + end + end + end + if ev == "ENVIRONMENTAL_DAMAGE" then + if target_guid == UnitGUID("player") then + if environmental_type == "Drowning" then + self.last_attack_source = -2 + elseif environmental_type == "Falling" then + self.last_attack_source = -3 + elseif environmental_type == "Fatigue" then + self.last_attack_source = -4 + elseif environmental_type == "Fire" then + self.last_attack_source = -5 + elseif environmental_type == "Lava" then + self.last_attack_source = -6 + elseif environmental_type == "Slime" then + self.last_attack_source = -7 + end + end + end +end + +function deathlog:PLAYER_DEAD() + local map = C_Map.GetBestMapForUnit("player") + local instance_id = nil + local position = nil + if map then + position = C_Map.GetPlayerMapPosition(map, "player") + local continentID, worldPosition = C_Map.GetWorldPosFromMapPos(map, position) + else + local _, _, _, _, _, _, _, _instance_id, _, _ = GetInstanceInfo() + instance_id = _instance_id + end + + local guildName, guildRankName, guildRankIndex = GetGuildInfo("player"); + local _, _, race_id = UnitRace("player") + local _, _, class_id = UnitClass("player") + local death_source = "-1" + if self.last_attack_source then + death_source = npc_to_id[self.last_attack_source] + end + + msg = deathlog.encodeMessage(UnitName("player"), guildName, death_source, race_id, class_id, UnitLevel("player"), + instance_id, + map, position) + if msg == nil then return end + + table.insert(deathlog.death_alert_out_queue, msg) + + if deathlog.last_words == nil then return end + local _, _, race_id = UnitRace("player") + local _, _, class_id = UnitClass("player") + local guildName, guildRankName, guildRankIndex = GetGuildInfo("player"); + if guildName == nil then guildName = "" end + + local player_data = deathlog.PlayerData(UnitName("player"), guildName, nil, nil, nil, UnitLevel("player"), nil, nil, + nil, nil, nil) + local checksum = fletcher16(player_data) + local msg = checksum .. COMM_FIELD_DELIM .. deathlog.last_words .. COMM_FIELD_DELIM + + table.insert(last_words_queue, msg) +end + +function deathlog:setLastWords(...) + local text, sn, LN, CN, p2, sF, zcI, cI, cB, unu, lI, senderGUID = ... + if PLAYERGUID == nil then + PLAYERGUID = UnitGUID("player") + end + + if senderGUID ~= PLAYERGUID then + return + end + + self.last_words["text"] = text +end + +function deathlog:CHAT_MSG_SAY(...) + self:setLastWords(...) +end + +function deathlog:CHAT_MSG_GUILD(...) + self:setLastWords(...) +end + +function deathlog:CHAT_MSG_PARTY(...) + self:setLastWords(...) +end + +function deathlog:startup() + -- event handling helper + self:SetScript("OnEvent", function(self, event, ...) + self[event](self, ...) + end) + self:RegisterEvent("PLAYER_DEAD") + self:RegisterEvent("CHAT_MSG_CHANNEL") + self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") + self:RegisterEvent("CHAT_MSG_PARTY") + self:RegisterEvent("CHAT_MSG_SAY") + self:RegisterEvent("CHAT_MSG_GUILD") +end + +return deathlog diff --git a/Modules/deathlog/deathlog_spec.lua b/Modules/deathlog/deathlog_spec.lua new file mode 100644 index 00000000..b5dad552 --- /dev/null +++ b/Modules/deathlog/deathlog_spec.lua @@ -0,0 +1,106 @@ +local deathlog = require('Modules/deathlog/deathlog') +local ui_mock = require('Modules.deathlog.ui_mock') + +_G.hardcore_settings = {} +_G.hc_peer_guilds = {} +_G.date = os.date +_G.bit = require("bit32") +_G.GetChannelName = function(name) return 1 end +_G.CTL = {} + +local testPlayer = { + name = "Zunter", + guild = "Hardcore Academy", + source_id = "435", + race_id = "1", + class_id = 9, + level = "17", + instance_id = nil, + map_id = 1433, + map_pos = { + x = 0.3122, + y = 0.1521 + }, + loc_str = "", + guid = "Player-5139-020C481D" +} + +local testPlayerData = deathlog.PlayerData( + testPlayer.name, + testPlayer.guild, + testPlayer.source_id, + testPlayer.race_id, + testPlayer.class_id, + testPlayer.level, + testPlayer.instance_id, + testPlayer.map_id, + deathlog.mapPosToString(testPlayer.map_pos), + nil, + nil) + +describe('Deathlog', function() + before_each(function() + deathlog.ui = ui_mock + end) + + after_each(function() + hardcore_settings["death_log_entries"] = {} + end) + + it('should encodeMessage and decodeMessage correct data', function() + local params = testPlayer + local message = deathlog.encodeMessage(params) + local playerData = deathlog.decodeMessage(message) + + assert.are.equal(params.name, playerData["name"]) + assert.are.equal(params.guild, playerData["guild"]) + assert.are.equal(params.source_id, tostring(playerData["source_id"])) + assert.are.equal(params.race_id, tostring(playerData["race_id"])) + assert.are.equal(params.class_id, playerData["class_id"]) + assert.are.equal(params.level, tostring(playerData["level"])) + assert.are.equal(params.instance_id, playerData["instance_id"]) + assert.are.equal(params.map_id, playerData["map_id"]) + assert.are.equal(deathlog.mapPosToString(params.map_pos), playerData["map_pos"]) + end) + + it('should generate a valid fletcher16 checksum', function() + local expectedChecksum = "Zunter-17652" + + local params = testPlayer + local checksum = fletcher16(params.name, params.guild, params.level) + + assert.are.equal(expectedChecksum, checksum) + end) + + it('should create a death entry', function() + local checksum = "Zunter-17652" + deathlog.death_ping_lru_cache_tbl[checksum] = {} + deathlog.death_ping_lru_cache_tbl[checksum]["player_data"] = testPlayerData + deathlog.createEntry(checksum) + + assert.are.equal(1, #hardcore_settings["death_log_entries"]) + end) + + it('should alert a player death if faction_wide is enabled', function() + hardcore_settings.alert_subset = "faction_wide" + stub(deathlog, "alertIfValid") + + local checksum = "Zunter-17652" + deathlog.death_ping_lru_cache_tbl[checksum] = {} + deathlog.death_ping_lru_cache_tbl[checksum]["player_data"] = testPlayerData + deathlog.createEntry(checksum) + + assert.are.equal(1, #hardcore_settings["death_log_entries"]) + assert.stub(deathlog.alertIfValid).was_called_with(testPlayerData) + end) + + it('should send the next message in the queue', function() + stub(_G.CTL, "SendChatMessage") + + local checksum = "Zunter-17652" + table.insert(deathlog.broadcast_death_ping_queue, checksum) + deathlog:sendNextInQueue() + assert.stub(_G.CTL.SendChatMessage).was_called() + assert.are.equal(0, #deathlog.broadcast_death_ping_queue) + end) +end) \ No newline at end of file diff --git a/Modules/deathlog/init.lua b/Modules/deathlog/init.lua new file mode 100644 index 00000000..e2110246 --- /dev/null +++ b/Modules/deathlog/init.lua @@ -0,0 +1,3 @@ +local _, addon = ... + +addon.deathlog:startup() \ No newline at end of file diff --git a/Modules/deathlog/ui.lua b/Modules/deathlog/ui.lua new file mode 100644 index 00000000..3afc721b --- /dev/null +++ b/Modules/deathlog/ui.lua @@ -0,0 +1,363 @@ +local _, addon = ... +if addon == nil then + -- stubs out for uni test suite + addon = {} +end + +local ui = {} +addon.deathlog.ui = ui + +local AceGUI = LibStub("AceGUI-3.0") + +-- Death log icon +local death_log_icon_frame = CreateFrame("frame") +death_log_icon_frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0) +death_log_icon_frame:SetSize(40, 40) +death_log_icon_frame:SetMovable(true) +death_log_icon_frame:EnableMouse(true) +death_log_icon_frame:Show() + +local black_round_tex = death_log_icon_frame:CreateTexture(nil, "OVERLAY") +black_round_tex:SetPoint("CENTER", death_log_icon_frame, "CENTER", -5, 4) +black_round_tex:SetDrawLayer("OVERLAY", 2) +black_round_tex:SetHeight(40) +black_round_tex:SetWidth(40) +black_round_tex:SetTexture("Interface\\PVPFrame\\PVP-Separation-Circle-Cooldown-overlay") + +local hc_fire_tex = death_log_icon_frame:CreateTexture(nil, "OVERLAY") +hc_fire_tex:SetPoint("CENTER", death_log_icon_frame, "CENTER", -6, 4) +hc_fire_tex:SetDrawLayer("OVERLAY", 3) +hc_fire_tex:SetHeight(25) +hc_fire_tex:SetWidth(25) +hc_fire_tex:SetTexture("Interface\\AddOns\\Hardcore\\Media\\wowhc-emblem-white-red.blp") + +local gold_ring_tex = death_log_icon_frame:CreateTexture(nil, "OVERLAY") +gold_ring_tex:SetPoint("CENTER", death_log_icon_frame, "CENTER", 0, 0) +gold_ring_tex:SetDrawLayer("OVERLAY", 2) +gold_ring_tex:SetHeight(50) +gold_ring_tex:SetWidth(50) +gold_ring_tex:SetTexture("Interface\\COMMON\\BlueMenuRing") + +-- Death log frame +local death_log_frame = AceGUI:Create("Deathlog") + +death_log_frame.frame:SetMovable(false) +death_log_frame.frame:EnableMouse(false) +death_log_frame:SetTitle("Hardcore Death Log") +local subtitle_data = { + { "Name", 70, function(_entry) return _entry.player_data["name"] or "" end }, + { "Class", 60, function(_entry) + local class_str, _, _ = GetClassInfo(_entry.player_data["class_id"]) + if RAID_CLASS_COLORS[class_str:upper()] then + return "|c" .. RAID_CLASS_COLORS[class_str:upper()].colorStr .. class_str .. "|r" + end + return class_str or "" + end }, + { "Race", 60, function(_entry) + local race_info = C_CreatureInfo.GetRaceInfo(_entry.player_data["race_id"]) + return race_info.raceName or "" + end }, + { "Lvl", 30, function(_entry) return _entry.player_data["level"] or "" end }, +} +death_log_frame:SetSubTitle(subtitle_data) +death_log_frame:SetLayout("Fill") +death_log_frame.frame:SetSize(255, 125) +death_log_frame:Show() + +local scroll_frame = AceGUI:Create("ScrollFrame") +scroll_frame:SetLayout("List") +death_log_frame:AddChild(scroll_frame) + +death_log_icon_frame:RegisterForDrag("LeftButton") +death_log_icon_frame:SetScript("OnDragStart", function(self, button) + self:StartMoving() +end) +death_log_icon_frame:SetScript("OnDragStop", function(self) + self:StopMovingOrSizing() + local x, y = self:GetCenter() + local px = (GetScreenWidth() * UIParent:GetEffectiveScale()) / 2 + local py = (GetScreenHeight() * UIParent:GetEffectiveScale()) / 2 + if hardcore_settings['death_log_pos'] == nil then + hardcore_settings['death_log_pos'] = {} + end + hardcore_settings['death_log_pos']['x'] = x - px + hardcore_settings['death_log_pos']['y'] = y - py +end) + +local WorldMapButton = WorldMapFrame:GetCanvas() +local death_tomb_frame = CreateFrame('frame', nil, WorldMapButton) +death_tomb_frame:SetAllPoints() +death_tomb_frame:SetFrameLevel(15000) + +local death_tomb_frame_tex = death_tomb_frame:CreateTexture(nil, 'OVERLAY') +death_tomb_frame_tex:SetTexture("Interface\\TARGETINGFRAME\\UI-TargetingFrame-Skull") +death_tomb_frame_tex:SetDrawLayer("OVERLAY", 4) +death_tomb_frame_tex:SetHeight(25) +death_tomb_frame_tex:SetWidth(25) +death_tomb_frame_tex:Hide() + +local death_tomb_frame_tex_glow = death_tomb_frame:CreateTexture(nil, 'OVERLAY') +death_tomb_frame_tex_glow:SetTexture("Interface\\Glues/Models/UI_HUMAN/GenericGlow64") +death_tomb_frame_tex_glow:SetDrawLayer("OVERLAY", 3) +death_tomb_frame_tex_glow:SetHeight(55) +death_tomb_frame_tex_glow:SetWidth(55) +death_tomb_frame_tex_glow:Hide() + +local function deathFrameDropdown(frame, level, menuList) + local info = UIDropDownMenu_CreateInfo() + + local function minimize() + death_log_frame:Minimize() + end + + local function maximize() + death_log_frame:Maximize() + end + + local function hide() + death_log_frame.frame:Hide() + death_log_icon_frame:Hide() + hardcore_settings["death_log_show"] = false + end + + local function openSettings() + InterfaceOptionsFrame_Show() + InterfaceOptionsFrame_OpenToCategory("Hardcore") + end + + if level == 1 then + if death_log_frame:IsMinimized() then + info.text, info.hasArrow, info.func = "Maximize", false, maximize + UIDropDownMenu_AddButton(info) + else + info.text, info.hasArrow, info.func = "Minimize", false, minimize + UIDropDownMenu_AddButton(info) + end + + info.text, info.hasArrow, info.func = "Settings", false, openSettings + UIDropDownMenu_AddButton(info) + + info.text, info.hasArrow, info.func = "Hide", false, hide + UIDropDownMenu_AddButton(info) + end +end + +death_log_icon_frame:SetScript("OnMouseDown", function(self, button) + if button == 'RightButton' then + local dropDown = CreateFrame("Frame", "death_frame_dropdown_menu", UIParent, "UIDropDownMenuTemplate") + UIDropDownMenu_Initialize(dropDown, deathFrameDropdown, "MENU") + ToggleDropDownMenu(1, nil, dropDown, "cursor", 3, -3) + end +end) + +local function setEntry(player_data, _entry) + _entry.player_data = player_data + for _, v in ipairs(subtitle_data) do + _entry.font_strings[v[1]]:SetText(v[3](_entry)) + end +end + +local function shiftEntry(_entry_from, _entry_to) + setEntry(_entry_from.player_data, _entry_to) +end + +local selected = nil +local row_entry = {} +function WPDropDownDemo_Menu(frame, level, menuList) + local info = UIDropDownMenu_CreateInfo() + + local function openWorldMap() + if not (death_tomb_frame.map_id and death_tomb_frame.coordinates) then return end + if C_Map.GetMapInfo(death_tomb_frame["map_id"]) == nil then return end + if tonumber(death_tomb_frame.coordinates[1]) == nil or tonumber(death_tomb_frame.coordinates[2]) == nil then return end + + WorldMapFrame:SetShown(not WorldMapFrame:IsShown()) + WorldMapFrame:SetMapID(death_tomb_frame.map_id) + WorldMapFrame:GetCanvas() + local mWidth, mHeight = WorldMapFrame:GetCanvas():GetSize() + death_tomb_frame_tex:SetPoint('CENTER', WorldMapButton, 'TOPLEFT', mWidth * death_tomb_frame.coordinates[1], + -mHeight * death_tomb_frame.coordinates[2]) + death_tomb_frame_tex:Show() + + death_tomb_frame_tex_glow:SetPoint('CENTER', WorldMapButton, 'TOPLEFT', mWidth * death_tomb_frame.coordinates[1], + -mHeight * death_tomb_frame.coordinates[2]) + death_tomb_frame_tex_glow:Show() + death_tomb_frame:Show() + end + + + if level == 1 then + info.text, info.hasArrow, info.func, info.disabled = "Show death location (WIP)", false, openWorldMap, false + UIDropDownMenu_AddButton(info) + info.text, info.hasArrow, info.func, info.disabled = "Block user", false, openWorldMap, true + UIDropDownMenu_AddButton(info) + info.text, info.hasArrow, info.func, info.disabled = "Block user's guild", false, openWorldMap, true + UIDropDownMenu_AddButton(info) + end +end + +for i = 1, 20 do + local idx = 21 - i + row_entry[idx] = AceGUI:Create("InteractiveLabel") + local _entry = row_entry[idx] + _entry:SetHighlight("Interface\\Glues\\CharacterSelect\\Glues-CharacterSelect-Highlight") + _entry.font_strings = {} + local next_x = 0 + local current_column_offset = 15 + for idx, v in ipairs(subtitle_data) do + _entry.font_strings[v[1]] = _entry.frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + _entry.font_strings[v[1]]:SetPoint("LEFT", _entry.frame, "LEFT", current_column_offset, 0) + current_column_offset = current_column_offset + v[2] + _entry.font_strings[v[1]]:SetJustifyH("LEFT") + + if idx + 1 <= #subtitle_data then + _entry.font_strings[v[1]]:SetWidth(v[2]) + end + _entry.font_strings[v[1]]:SetTextColor(1, 1, 1) + _entry.font_strings[v[1]]:SetFont("Fonts\\FRIZQT__.TTF", 11, "") + end + + _entry.background = _entry.frame:CreateTexture(nil, "OVERLAY") + _entry.background:SetPoint("CENTER", _entry.frame, "CENTER", 0, 0) + _entry.background:SetDrawLayer("OVERLAY", 2) + _entry.background:SetVertexColor(.5, .5, .5, (i % 2) / 10) + _entry.background:SetHeight(16) + _entry.background:SetWidth(500) + _entry.background:SetTexture("Interface\\ChatFrame\\ChatFrameBackground") + + _entry:SetHeight(40) + _entry:SetFont("Fonts\\FRIZQT__.TTF", 16, "") + _entry:SetColor(1, 1, 1) + _entry:SetText(" ") + + function _entry:deselect() + for _, v in pairs(_entry.font_strings) do + v:SetTextColor(1, 1, 1) + end + end + + function _entry:select() + selected = idx + for _, v in pairs(_entry.font_strings) do + v:SetTextColor(1, 1, 0) + end + end + + _entry:SetCallback("OnLeave", function(widget) + if _entry.player_data == nil then return end + GameTooltip:Hide() + end) + + _entry:SetCallback("OnClick", function() + if _entry.player_data == nil then return end + local click_type = GetMouseButtonClicked() + + if click_type == "LeftButton" then + if selected then row_entry[selected]:deselect() end + _entry:select() + elseif click_type == "RightButton" then + local dropDown = CreateFrame("Frame", "WPDemoContextMenu", UIParent, "UIDropDownMenuTemplate") + -- Bind an initializer function to the dropdown; see previous sections for initializer function examples. + UIDropDownMenu_Initialize(dropDown, WPDropDownDemo_Menu, "MENU") + ToggleDropDownMenu(1, nil, dropDown, "cursor", 3, -3) + if _entry["player_data"]["map_id"] and _entry["player_data"]["map_pos"] then + death_tomb_frame.map_id = _entry["player_data"]["map_id"] + local x, y = strsplit(",", _entry["player_data"]["map_pos"], 2) + death_tomb_frame.coordinates = { x, y } + end + end + end) + + _entry:SetCallback("OnEnter", function(widget) + if _entry.player_data == nil then return end + GameTooltip_SetDefaultAnchor(GameTooltip, WorldFrame) + + if string.sub(_entry.player_data["name"], #_entry.player_data["name"]) == "s" then + GameTooltip:AddDoubleLine(_entry.player_data["name"] .. "' Death", "Lvl. " .. _entry.player_data["level"], 1, + 1, 1, .5, .5, .5); + else + GameTooltip:AddDoubleLine(_entry.player_data["name"] .. "'s Death", "Lvl. " .. _entry.player_data["level"], 1, + 1, 1, .5, .5, .5); + end + GameTooltip:AddLine("Name: " .. _entry.player_data["name"], 1, 1, 1) + GameTooltip:AddLine("Guild: " .. _entry.player_data["guild"], 1, 1, 1) + + local race_info = C_CreatureInfo.GetRaceInfo(_entry.player_data["race_id"]) + if race_info then GameTooltip:AddLine("Race: " .. race_info.raceName, 1, 1, 1) end + + if _entry.player_data["class_id"] then + local class_str, _, _ = GetClassInfo(_entry.player_data["class_id"]) + if class_str then GameTooltip:AddLine("Class: " .. class_str, 1, 1, 1) end + end + + if _entry.player_data["source_id"] then + local source_id = id_to_npc[_entry.player_data["source_id"]] + if source_id then + GameTooltip:AddLine("Killed by: " .. source_id, 1, 1, 1, true) + elseif environment_damage[_entry.player_data["source_id"]] then + GameTooltip:AddLine("Died from: " .. environment_damage[_entry.player_data["source_id"]], 1, 1, 1, true) + end + end + + if race_name then GameTooltip:AddLine("Race: " .. race_name, 1, 1, 1) end + + if _entry.player_data["map_id"] then + local map_info = C_Map.GetMapInfo(_entry.player_data["map_id"]) + if map_info then GameTooltip:AddLine("Zone: " .. map_info.name, 1, 1, 1, true) end + end + + if _entry.player_data["map_pos"] then + GameTooltip:AddLine("Loc: " .. _entry.player_data["map_pos"], 1, 1, 1, true) + end + + if _entry.player_data["date"] then + GameTooltip:AddLine("Date: " .. _entry.player_data["date"], 1, 1, 1, true) + end + + if _entry.player_data["last_words"] then + GameTooltip:AddLine("Last words: " .. _entry.player_data["last_words"], 1, 1, 0, true) + end + GameTooltip:Show() + end) + + scroll_frame:SetScroll(0) + scroll_frame.scrollbar:Hide() + scroll_frame:AddChild(_entry) +end + +function ui.Show() + death_log_frame.frame:Show() + death_log_icon_frame:Show() +end + +function ui.Hide() + death_log_frame.frame:Hide() + death_log_icon_frame:Hide() +end + +function ui.Refresh() + death_log_icon_frame:ClearAllPoints() + death_log_frame.frame:ClearAllPoints() + if death_log_frame.frame and hardcore_settings["death_log_pos"] then + death_log_icon_frame:SetPoint("CENTER", UIParent, "CENTER", hardcore_settings["death_log_pos"]['x'], + hardcore_settings["death_log_pos"]['y']) + else + death_log_icon_frame:SetPoint("CENTER", UIParent, "CENTER", 470, -100) + end + death_log_frame.frame:SetPoint("TOPLEFT", death_log_icon_frame, "TOPLEFT", 10, -10) + death_log_frame.frame:SetFrameStrata("BACKGROUND") + death_log_frame.frame:Lower() +end + +function ui.InsertEntry(player_data) + for i = 1, 19 do + if row_entry[i + 1].player_data ~= nil then + shiftEntry(row_entry[i + 1], row_entry[i]) + if selected and selected == i + 1 then + row_entry[i + 1]:deselect() + row_entry[i]:select() + end + end + end + setEntry(player_data, row_entry[20]) +end diff --git a/Modules/deathlog/ui_mock.lua b/Modules/deathlog/ui_mock.lua new file mode 100644 index 00000000..b485e225 --- /dev/null +++ b/Modules/deathlog/ui_mock.lua @@ -0,0 +1,15 @@ +local ui = {} + +function ui.Show() +end + +function ui.Hide() +end + +function ui.Refresh() +end + +function ui.InsertEntry(player_data) +end + +return ui \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..6b477ace --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.4' + +services: + test: + build: + context: . + dockerfile: Dockerfile + # image: imega/busted + environment: + - LUA_PATH=/app/?.lua + command: busted /app + volumes: + - ./:/app/ \ No newline at end of file From 1f4e3e04bb522b9e35af63b3fc57142f57f60a24 Mon Sep 17 00:00:00 2001 From: Taylor Petrychyn Date: Mon, 17 Apr 2023 19:21:17 -0700 Subject: [PATCH 2/8] remove unused file --- Modules/blah | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Modules/blah diff --git a/Modules/blah b/Modules/blah deleted file mode 100644 index e69de29b..00000000 From eea6f501ae057aaa8ab75d179377a0c6f558405d Mon Sep 17 00:00:00 2001 From: Taylor Petrychyn Date: Mon, 17 Apr 2023 19:25:10 -0700 Subject: [PATCH 3/8] remove init file, instead check if we are running in wow --- Hardcore_Classic.toc | 1 - Modules/deathlog/deathlog.lua | 7 +++++++ Modules/deathlog/init.lua | 3 --- 3 files changed, 7 insertions(+), 4 deletions(-) delete mode 100644 Modules/deathlog/init.lua diff --git a/Hardcore_Classic.toc b/Hardcore_Classic.toc index bffc871d..3d15af75 100644 --- a/Hardcore_Classic.toc +++ b/Hardcore_Classic.toc @@ -163,7 +163,6 @@ id_to_npc_classic.lua npc_to_id_classic.lua Modules/deathlog/deathlog.lua Modules/deathlog/ui.lua -Modules/deathlog/init.lua Hardcore.lua Libs/GreenWall/Constants.lua diff --git a/Modules/deathlog/deathlog.lua b/Modules/deathlog/deathlog.lua index c6bff063..6477ad9b 100644 --- a/Modules/deathlog/deathlog.lua +++ b/Modules/deathlog/deathlog.lua @@ -513,6 +513,11 @@ function deathlog:CHAT_MSG_PARTY(...) end function deathlog:startup() + if type(addon) ~= "table" then + -- only bind event listeners if we are inside wow + return + end + -- event handling helper self:SetScript("OnEvent", function(self, event, ...) self[event](self, ...) @@ -525,4 +530,6 @@ function deathlog:startup() self:RegisterEvent("CHAT_MSG_GUILD") end +deathlog:startup() + return deathlog diff --git a/Modules/deathlog/init.lua b/Modules/deathlog/init.lua deleted file mode 100644 index e2110246..00000000 --- a/Modules/deathlog/init.lua +++ /dev/null @@ -1,3 +0,0 @@ -local _, addon = ... - -addon.deathlog:startup() \ No newline at end of file From b537e1ac8fbbe29cca8c51dabb983ddbc7b013a1 Mon Sep 17 00:00:00 2001 From: Taylor Petrychyn Date: Mon, 17 Apr 2023 22:05:45 -0700 Subject: [PATCH 4/8] fix some errors, cleanup and better test --- Hardcore.lua | 32 ++---- Modules/deathlog/deathlog.lua | 151 ++++++++++++++--------------- Modules/deathlog/deathlog_spec.lua | 38 +++++--- Modules/deathlog/ui.lua | 12 +-- 4 files changed, 110 insertions(+), 123 deletions(-) diff --git a/Hardcore.lua b/Hardcore.lua index 8c91778a..7aea705a 100644 --- a/Hardcore.lua +++ b/Hardcore.lua @@ -20,6 +20,8 @@ along with the Hardcore AddOn. If not, see . --[[ Const variables ]] -- -- ERR_CHAT_PLAYER_NOT_FOUND_S = nil -- Disables warning when pinging non-hc player -- This clashes with other addons +local _, addon = ... + StaticPopupDialogs["CHAT_CHANNEL_PASSWORD"] = nil --CHAT_WRONG_PASSWORD_NOTICE = nil local DEATH_ALERT_COOLDOWN = 1800 @@ -212,7 +214,6 @@ local GENDER_POSSESSIVE_PRONOUN = { "Their", "His", "Her" } local recent_levelup = nil local recent_msg = {} local Last_Attack_Source = nil -DeathLog_Last_Attack_Source = nil local PICTURE_DELAY = 0.65 local HIDE_RTP_CHAT_MSG_BUFFER = 0 -- number of messages in queue local HIDE_RTP_CHAT_MSG_BUFFER_MAX = 2 -- number of maximum messages to wait for @@ -1694,11 +1695,11 @@ function Hardcore:PLAYER_ENTERING_WORLD() end C_Timer.After(1.0, function() - deathlogApplySettings(Hardcore_Settings) + addon.deathlog:ApplySettings(Hardcore_Settings) end) C_Timer.After(5.0, function() - deathlogJoinChannel() + addon.deathlog:JoinChannel() end) end @@ -1817,9 +1818,6 @@ function Hardcore:PLAYER_DEAD() end -- Send broadcast text messages to guild and greenwall - selfDeathAlert(DeathLog_Last_Attack_Source) - selfDeathAlertLastWords(recent_msg["text"]) - SendChatMessage(messageString, "GUILD") startXGuildChatMsgRelay(messageString) startXGuildDeathMsgRelay() @@ -2575,27 +2573,9 @@ function Hardcore:COMBAT_LOG_EVENT_UNFILTERED(...) if not (source_name == nil) then if string.find(ev, "DAMAGE") ~= nil then Last_Attack_Source = source_name - DeathLog_Last_Attack_Source = source_name end end end - if ev == "ENVIRONMENTAL_DAMAGE" then - if target_guid == UnitGUID("player") then - if environmental_type == "Drowning" then - DeathLog_Last_Attack_Source = -2 - elseif environmental_type == "Falling" then - DeathLog_Last_Attack_Source = -3 - elseif environmental_type == "Fatigue" then - DeathLog_Last_Attack_Source = -4 - elseif environmental_type == "Fire" then - DeathLog_Last_Attack_Source = -5 - elseif environmental_type == "Lava" then - DeathLog_Last_Attack_Source = -6 - elseif environmental_type == "Slime" then - DeathLog_Last_Attack_Source = -7 - end - end - end end function Hardcore:CHAT_MSG_SAY(...) @@ -3926,7 +3906,7 @@ local options = { Hardcore_Settings.death_log_show = true end Hardcore_Settings.death_log_show = not Hardcore_Settings.death_log_show - deathlogApplySettings(Hardcore_Settings) + addon.deathlog:ApplySettings(Hardcore_Settings) end, order = 1, }, @@ -4031,7 +4011,7 @@ local options = { desc = "Reset the death log pos.", func = function(info, value) hardcore_settings["death_log_pos"] = {['x'] = 0, ['y'] = 0} - deathlogApplySettings(Hardcore_Settings) + addon.deathlog:ApplySettings(Hardcore_Settings) end, order = 5, }, diff --git a/Modules/deathlog/deathlog.lua b/Modules/deathlog/deathlog.lua index 6477ad9b..af1d7b37 100644 --- a/Modules/deathlog/deathlog.lua +++ b/Modules/deathlog/deathlog.lua @@ -1,12 +1,16 @@ -local _, addon = ... local deathlog = {} +local _, addon = ... -- check if we are running in wow if type(addon) == "table" then - deathlog = CreateFrame("Frame", "Hardcore", nil, "BackdropTemplate") + deathlog = CreateFrame("Frame", "Deathlog", nil, "BackdropTemplate") addon.deathlog = deathlog end +local debug = false + +local CTL = _G.ChatThrottleLib +local COMM_NAME = "HCDeathAlerts" local COMM_COMMANDS = { ["BROADCAST_DEATH_PING"] = "1", ["BROADCAST_DEATH_PING_CHECKSUM"] = "2", @@ -30,7 +34,6 @@ deathlog.last_words = "" deathlog.broadcast_death_ping_queue = {} deathlog.last_words_queue = {} deathlog.death_alert_out_queue = {} -local f_log = {} function fletcher16(player_name, player_guild, player_level) local data = player_name .. player_guild .. player_level @@ -43,7 +46,7 @@ function fletcher16(player_name, player_guild, player_level) return player_name .. "-" .. bit.bor(bit.lshift(sum2, 8), sum1) end -local function isValidEntry(_player_data) +function deathlog.isValidEntry(_player_data) if _player_data == nil then return false end if _player_data["source_id"] == nil then return false end if _player_data["race_id"] == nil or tonumber(_player_data["race_id"]) == nil or C_CreatureInfo.GetRaceInfo(_player_data["race_id"]) == nil then return false end @@ -53,10 +56,10 @@ local function isValidEntry(_player_data) return true end -local function shouldCreateEntry(checksum) +function deathlog.shouldCreateEntry(checksum) if deathlog.death_ping_lru_cache_tbl[checksum] == nil then return false end if deathlog.death_ping_lru_cache_tbl[checksum]["player_data"] == nil then return false end - if hardcore_settings.death_log_types == nil or hardcore_settings.death_log_types == "faction_wide" and isValidEntry(deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]) then + if hardcore_settings.death_log_types == nil or hardcore_settings.death_log_types == "faction_wide" and deathlog.isValidEntry(deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]) then if deathlog.death_ping_lru_cache_tbl[checksum]["peer_report"] and deathlog.death_ping_lru_cache_tbl[checksum]["peer_report"] > HC_REQUIRED_ACKS then return true else @@ -72,37 +75,7 @@ local function shouldCreateEntry(checksum) return false end -function deathlog.alertIfValid(_player_data) - local race_info = C_CreatureInfo.GetRaceInfo(_player_data["race_id"]) - local race_str = race_info.raceName - local class_str, _, _ = GetClassInfo(_player_data["class_id"]) - if class_str and RAID_CLASS_COLORS[class_str:upper()] then - class_str = "|c" .. RAID_CLASS_COLORS[class_str:upper()].colorStr .. class_str .. "|r" - end - - local level_str = tostring(_player_data["level"]) - local level_num = tonumber(_player_data["level"]) - local min_level = tonumber(hardcore_settings.minimum_show_death_alert_lvl) or 0 - if level_num < tonumber(min_level) then - return - end - - local map_info = nil - local map_name = "?" - if _player_data["map_id"] then - map_info = C_Map.GetMapInfo(_player_data["map_id"]) - end - if map_info then - map_name = map_info.name - end - - local msg = _player_data["name"] .. - " the " .. - (race_str or "") .. " " .. (class_str or "") .. " has died at level " .. level_str .. " in " .. map_name - Hardcore:TriggerDeathAlert(msg) -end - -function deathlog.ApplySettings(_settings) +function deathlog:ApplySettings(_settings) hardcore_settings = _settings if hardcore_settings["death_log_show"] == nil or hardcore_settings["death_log_show"] == true then @@ -262,7 +235,7 @@ function deathlog.receiveChannelMessage(sender, data) if data == nil then return end local decoded_player_data = deathlog.decodeMessage(data) if sender ~= decoded_player_data["name"] then return end - if isValidEntry(decoded_player_data) == false then return end + if deathlog.isValidEntry(decoded_player_data) == false then return end local checksum = fletcher16(decoded_player_data["name"], decoded_player_data["guild"], decoded_player_data["level"]) @@ -294,7 +267,7 @@ function deathlog.receiveChannelMessage(sender, data) -- end deathlog.death_ping_lru_cache_tbl[checksum]["self_report"] = 1 - if shouldCreateEntry(checksum) then + if deathlog.shouldCreateEntry(checksum) then deathlog.createEntry(checksum) end end @@ -319,49 +292,68 @@ function deathlog.receiveChannelMessageChecksum(sender, checksum) deathlog.death_ping_lru_cache_tbl[checksum]["peer_report"] = deathlog.death_ping_lru_cache_tbl[checksum] ["peer_report"] + 1 - if shouldCreateEntry(checksum) then + if deathlog.shouldCreateEntry(checksum) then deathlog.createEntry(checksum) end end -function deathlog:sendNextInQueue() - if #self.broadcast_death_ping_queue > 0 then - local channel_num = GetChannelName(death_alerts_channel) - if channel_num == 0 then - self:JoinChannel() - return - end +function deathlog:sendNextInQueue(command, queue) + local channel_num = GetChannelName(death_alerts_channel) + if channel_num == 0 then + self:JoinChannel() + return + end + + local commMessage = command .. COMM_COMMAND_DELIM .. queue[1] + CTL:SendChatMessage("BULK", COMM_NAME, commMessage, "CHANNEL", nil, channel_num) + table.remove(queue, 1) +end - local commMessage = COMM_COMMANDS["BROADCAST_DEATH_PING_CHECKSUM"] .. - COMM_COMMAND_DELIM .. self.broadcast_death_ping_queue[1] - CTL:SendChatMessage("BULK", COMM_NAME, commMessage, "CHANNEL", nil, channel_num) - table.remove(self.broadcast_death_ping_queue, 1) +function deathlog:checkQueuesAndSend() + if #self.broadcast_death_ping_queue > 0 then + self:sendNextInQueue(COMM_COMMANDS["BROADCAST_DEATH_PING_CHECKSUM"], self.broadcast_death_ping_queue) return end if #self.death_alert_out_queue > 0 then - local channel_num = GetChannelName(death_alerts_channel) - if channel_num == 0 then - self:JoinChannel() - return - end - local commMessage = COMM_COMMANDS["BROADCAST_DEATH_PING"] .. COMM_COMMAND_DELIM .. self.death_alert_out_queue[1] - CTL:SendChatMessage("BULK", COMM_NAME, commMessage, "CHANNEL", nil, channel_num) - table.remove(self.death_alert_out_queue, 1) + self:sendNextInQueue(COMM_COMMANDS["BROADCAST_DEATH_PING"], self.death_alert_out_queue) return end if #self.last_words_queue > 0 then - local channel_num = GetChannelName(death_alerts_channel) - if channel_num == 0 then - self:JoinChannel() - return - end - local commMessage = COMM_COMMANDS["LAST_WORDS"] .. COMM_COMMAND_DELIM .. self.last_words_queue[1] - CTL:SendChatMessage("BULK", COMM_NAME, commMessage, "CHANNEL", nil, channel_num) - table.remove(self.last_words_queue, 1) + self:sendNextInQueue(COMM_COMMANDS["LAST_WORDS"], self.last_words_queue) + return + end +end + +function deathlog.alertIfValid(_player_data) + local race_info = C_CreatureInfo.GetRaceInfo(_player_data["race_id"]) + local race_str = race_info.raceName + local class_str, _, _ = GetClassInfo(_player_data["class_id"]) + if class_str and RAID_CLASS_COLORS[class_str:upper()] then + class_str = "|c" .. RAID_CLASS_COLORS[class_str:upper()].colorStr .. class_str .. "|r" + end + + local level_str = tostring(_player_data["level"]) + local level_num = tonumber(_player_data["level"]) + local min_level = tonumber(hardcore_settings.minimum_show_death_alert_lvl) or 0 + if level_num < tonumber(min_level) then return end + + local map_info = nil + local map_name = "?" + if _player_data["map_id"] then + map_info = C_Map.GetMapInfo(_player_data["map_id"]) + end + if map_info then + map_name = map_info.name + end + + local msg = _player_data["name"] .. + " the " .. + (race_str or "") .. " " .. (class_str or "") .. " has died at level " .. level_str .. " in " .. map_name + Hardcore:TriggerDeathAlert(msg) end function deathlog:CHAT_MSG_CHANNEL(...) @@ -452,7 +444,6 @@ function deathlog:PLAYER_DEAD() local position = nil if map then position = C_Map.GetPlayerMapPosition(map, "player") - local continentID, worldPosition = C_Map.GetWorldPosFromMapPos(map, position) else local _, _, _, _, _, _, _, _instance_id, _, _ = GetInstanceInfo() instance_id = _instance_id @@ -474,9 +465,6 @@ function deathlog:PLAYER_DEAD() table.insert(deathlog.death_alert_out_queue, msg) if deathlog.last_words == nil then return end - local _, _, race_id = UnitRace("player") - local _, _, class_id = UnitClass("player") - local guildName, guildRankName, guildRankIndex = GetGuildInfo("player"); if guildName == nil then guildName = "" end local player_data = deathlog.PlayerData(UnitName("player"), guildName, nil, nil, nil, UnitLevel("player"), nil, nil, @@ -484,7 +472,7 @@ function deathlog:PLAYER_DEAD() local checksum = fletcher16(player_data) local msg = checksum .. COMM_FIELD_DELIM .. deathlog.last_words .. COMM_FIELD_DELIM - table.insert(last_words_queue, msg) + table.insert(deathlog.last_words_queue, msg) end function deathlog:setLastWords(...) @@ -497,7 +485,7 @@ function deathlog:setLastWords(...) return end - self.last_words["text"] = text + self.last_words = text end function deathlog:CHAT_MSG_SAY(...) @@ -512,6 +500,15 @@ function deathlog:CHAT_MSG_PARTY(...) self:setLastWords(...) end +function deathlog:PLAYER_LOGIN() + self:RegisterEvent("PLAYER_DEAD") + self:RegisterEvent("CHAT_MSG_CHANNEL") + self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") + self:RegisterEvent("CHAT_MSG_PARTY") + self:RegisterEvent("CHAT_MSG_SAY") + self:RegisterEvent("CHAT_MSG_GUILD") +end + function deathlog:startup() if type(addon) ~= "table" then -- only bind event listeners if we are inside wow @@ -522,12 +519,8 @@ function deathlog:startup() self:SetScript("OnEvent", function(self, event, ...) self[event](self, ...) end) - self:RegisterEvent("PLAYER_DEAD") - self:RegisterEvent("CHAT_MSG_CHANNEL") - self:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") - self:RegisterEvent("CHAT_MSG_PARTY") - self:RegisterEvent("CHAT_MSG_SAY") - self:RegisterEvent("CHAT_MSG_GUILD") + + self:RegisterEvent("PLAYER_LOGIN") end deathlog:startup() diff --git a/Modules/deathlog/deathlog_spec.lua b/Modules/deathlog/deathlog_spec.lua index b5dad552..8e250a72 100644 --- a/Modules/deathlog/deathlog_spec.lua +++ b/Modules/deathlog/deathlog_spec.lua @@ -1,12 +1,12 @@ -local deathlog = require('Modules/deathlog/deathlog') -local ui_mock = require('Modules.deathlog.ui_mock') - _G.hardcore_settings = {} _G.hc_peer_guilds = {} _G.date = os.date _G.bit = require("bit32") _G.GetChannelName = function(name) return 1 end -_G.CTL = {} +_G.ChatThrottleLib = {} + +local deathlog = require('Modules/deathlog/deathlog') +local ui_mock = require('Modules.deathlog.ui_mock') local testPlayer = { name = "Zunter", @@ -94,13 +94,29 @@ describe('Deathlog', function() assert.stub(deathlog.alertIfValid).was_called_with(testPlayerData) end) - it('should send the next message in the queue', function() - stub(_G.CTL, "SendChatMessage") - - local checksum = "Zunter-17652" - table.insert(deathlog.broadcast_death_ping_queue, checksum) - deathlog:sendNextInQueue() - assert.stub(_G.CTL.SendChatMessage).was_called() + it('should drain the 3 outbox queues, one message sent per action, pioritizing death ping', function() + stub(_G.ChatThrottleLib, "SendChatMessage") + + local message = "foo" -- the message on the queues is irrelevant for this test + table.insert(deathlog.broadcast_death_ping_queue, message) + table.insert(deathlog.broadcast_death_ping_queue, message) + table.insert(deathlog.death_alert_out_queue, message) + table.insert(deathlog.last_words_queue, message) + + -- check that each time we call checkQueuesAndSend the number of chat messages sent only goes up by 1 + -- first 2 calls will drain death_ping_queue + deathlog:checkQueuesAndSend() + assert.stub(_G.ChatThrottleLib.SendChatMessage).was.called(1) + deathlog:checkQueuesAndSend() + assert.stub(_G.ChatThrottleLib.SendChatMessage).was.called(2) assert.are.equal(0, #deathlog.broadcast_death_ping_queue) + + deathlog:checkQueuesAndSend() + assert.stub(_G.ChatThrottleLib.SendChatMessage).was.called(3) + assert.are.equal(0, #deathlog.death_alert_out_queue) + + deathlog:checkQueuesAndSend() + assert.stub(_G.ChatThrottleLib.SendChatMessage).was.called(4) + assert.are.equal(0, #deathlog.last_words_queue) end) end) \ No newline at end of file diff --git a/Modules/deathlog/ui.lua b/Modules/deathlog/ui.lua index 3afc721b..0611f93d 100644 --- a/Modules/deathlog/ui.lua +++ b/Modules/deathlog/ui.lua @@ -1,12 +1,11 @@ +local ui = {} + local _, addon = ... -if addon == nil then - -- stubs out for uni test suite - addon = {} +-- check if we are running in wow +if type(addon) == "table" then + addon.deathlog.ui = ui end -local ui = {} -addon.deathlog.ui = ui - local AceGUI = LibStub("AceGUI-3.0") -- Death log icon @@ -202,7 +201,6 @@ for i = 1, 20 do local _entry = row_entry[idx] _entry:SetHighlight("Interface\\Glues\\CharacterSelect\\Glues-CharacterSelect-Highlight") _entry.font_strings = {} - local next_x = 0 local current_column_offset = 15 for idx, v in ipairs(subtitle_data) do _entry.font_strings[v[1]] = _entry.frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") From b44bdaad57dd556c07a3973e325c8f8cdf36906b Mon Sep 17 00:00:00 2001 From: Taylor Petrychyn Date: Thu, 20 Apr 2023 11:05:42 -0700 Subject: [PATCH 5/8] finish converting deathlog to module --- Modules/deathlog/deathlog.lua | 111 ++++++++++++++++++++--------- Modules/deathlog/deathlog_spec.lua | 91 ++++++++++++++++++++--- Modules/deathlog/ui.lua | 20 ++++++ 3 files changed, 179 insertions(+), 43 deletions(-) diff --git a/Modules/deathlog/deathlog.lua b/Modules/deathlog/deathlog.lua index af1d7b37..1fd26177 100644 --- a/Modules/deathlog/deathlog.lua +++ b/Modules/deathlog/deathlog.lua @@ -35,6 +35,10 @@ deathlog.broadcast_death_ping_queue = {} deathlog.last_words_queue = {} deathlog.death_alert_out_queue = {} +-- store the count of times a player has reported their death, +-- prevents a malicious actor from filling up the reported_deaths list +deathlog.death_reports_this_session = {} + function fletcher16(player_name, player_guild, player_level) local data = player_name .. player_guild .. player_level local sum1 = 0 @@ -104,7 +108,7 @@ function deathlog:JoinChannel() end function deathlog.PlayerData(name, guild, source_id, race_id, class_id, level, instance_id, map_id, map_pos, date, - last_words) + last_words, guid) return { ["name"] = name, ["guild"] = guild, @@ -117,6 +121,7 @@ function deathlog.PlayerData(name, guild, source_id, race_id, class_id, level, i ["map_pos"] = map_pos, ["date"] = date, ["last_words"] = last_words, + ["guid"] = guid } end @@ -125,10 +130,10 @@ local encodeMessageParams = { guild = "", source_id = "", race_id = "", - class_id = -1, + class_id = nil, level = "", - instance_id = -1, - map_id = -1, + instance_id = nil, + map_id = nil, map_pos = {}, loc_str = "", guid = "" @@ -142,6 +147,7 @@ function deathlog.encodeMessage(params) if tonumber(params.race_id) == nil then return end if tonumber(params.level) == nil then return end + if params.map_pos then params.loc_str = deathlog.mapPosToString(params.map_pos) end @@ -184,8 +190,9 @@ function deathlog.decodeMessage(msg) local instance_id = tonumber(values[7]) local map_id = tonumber(values[8]) local map_pos = values[9] + local guid = values[10] local player_data = deathlog.PlayerData(name, guild, source_id, race_id, class_id, level, instance_id, map_id, - map_pos, date, last_words) + map_pos, date, last_words, guid) return player_data end @@ -237,6 +244,17 @@ function deathlog.receiveChannelMessage(sender, data) if sender ~= decoded_player_data["name"] then return end if deathlog.isValidEntry(decoded_player_data) == false then return end + if decoded_player_data["guid"] ~= nil then + local guid = decoded_player_data["guid"] + local name = decoded_player_data["name"] + local key = string.format("%s-%s", guid, name) + if Recorded_Deaths[key] ~= nil or deathlog.death_reports_this_session[name] then + return -- already recorded this death + end + deathlog.death_reports_this_session[name] = true + Recorded_Deaths[key] = GetServerTime() + end + local checksum = fletcher16(decoded_player_data["name"], decoded_player_data["guild"], decoded_player_data["level"]) if deathlog.death_ping_lru_cache_tbl[checksum] == nil then @@ -249,22 +267,22 @@ function deathlog.receiveChannelMessage(sender, data) if deathlog.death_ping_lru_cache_tbl[checksum]["committed"] then return end - -- local guildName, guildRankName, guildRankIndex = GetGuildInfo("player"); - -- if decoded_player_data['guild'] == guildName then - -- local name_long = sender .. "-" .. GetNormalizedRealmName() - -- for i = 1, GetNumGuildMembers() do - -- local name, _, _, level, class_str, _, _, _, _, _, class = GetGuildRosterInfo(i) - -- if name_long == name and level == decoded_player_data["level"] then - -- deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]["in_guild"] = 1 - -- local delay = math.random(0, 10) - -- C_Timer.After(delay, function() - -- if deathlog.death_ping_lru_cache_tbl[checksum] and deathlog.death_ping_lru_cache_tbl[checksum]["committed"] then return end - -- table.insert(broadcast_death_ping_queue, checksum) -- Must be added to queue to be broadcasted to network - -- end) - -- break - -- end - -- end - -- end + local guildName, _, _ = GetGuildInfo("player"); + if decoded_player_data['guild'] == guildName then + local name_long = sender .. "-" .. GetNormalizedRealmName() + for i = 1, GetNumGuildMembers() do + local name, _, _, level, _, _, _, _, _, _, _ = GetGuildRosterInfo(i) + if name_long == name and level == decoded_player_data["level"] then + deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]["in_guild"] = 1 + local delay = math.random(0, 10) + C_Timer.After(delay, function() + if deathlog.death_ping_lru_cache_tbl[checksum] and deathlog.death_ping_lru_cache_tbl[checksum]["committed"] then return end + table.insert(deathlog.broadcast_death_ping_queue, checksum) -- Must be added to queue to be broadcasted to network + end) + break + end + end + end deathlog.death_ping_lru_cache_tbl[checksum]["self_report"] = 1 if deathlog.shouldCreateEntry(checksum) then @@ -297,6 +315,26 @@ function deathlog.receiveChannelMessageChecksum(sender, checksum) end end +function deathlog.receiveLastWords(sender, data) + if data == nil then return end + local values = {} + for w in data:gmatch("(.-)~") do table.insert(values, w) end + local checksum = values[1] + local msg = values[2] + + if checksum == nil or msg == nil then return end + + if deathlog.death_ping_lru_cache_tbl[checksum] == nil then + deathlog.death_ping_lru_cache_tbl[checksum] = {} + end + if deathlog.death_ping_lru_cache_tbl[checksum]["player_data"] ~= nil then + deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]["last_words"] = msg + deathlog.ui.SetLastWords(sender, msg) + else + deathlog.death_ping_lru_cache_tbl[checksum]["last_words"] = msg + end +end + function deathlog:sendNextInQueue(command, queue) local channel_num = GetChannelName(death_alerts_channel) if channel_num == 0 then @@ -401,7 +439,7 @@ function deathlog:CHAT_MSG_CHANNEL(...) shadowbanned[player_name_short] = 1 end - -- deathlogReceiveLastWords(player_name_short, msg) + deathlog.receiveLastWords(player_name_short, msg) if debug then print("last words", msg) end return end @@ -449,7 +487,7 @@ function deathlog:PLAYER_DEAD() instance_id = _instance_id end - local guildName, guildRankName, guildRankIndex = GetGuildInfo("player"); + local guildName, _, _ = GetGuildInfo("player"); local _, _, race_id = UnitRace("player") local _, _, class_id = UnitClass("player") local death_source = "-1" @@ -457,22 +495,29 @@ function deathlog:PLAYER_DEAD() death_source = npc_to_id[self.last_attack_source] end - msg = deathlog.encodeMessage(UnitName("player"), guildName, death_source, race_id, class_id, UnitLevel("player"), - instance_id, - map, position) - if msg == nil then return end + deathMsg = deathlog.encodeMessage({ + name = UnitName("player"), + guild = guildName, + source_id = death_source, + race_id = race_id, + class_id = class_id, + level = UnitLevel("player"), + instance_id = instance_id, + map_id = map, + map_pos = position, + guid = UnitGUID("player") + }) + if deathMsg == nil then return end - table.insert(deathlog.death_alert_out_queue, msg) + table.insert(deathlog.death_alert_out_queue, deathMsg) if deathlog.last_words == nil then return end if guildName == nil then guildName = "" end - local player_data = deathlog.PlayerData(UnitName("player"), guildName, nil, nil, nil, UnitLevel("player"), nil, nil, - nil, nil, nil) - local checksum = fletcher16(player_data) - local msg = checksum .. COMM_FIELD_DELIM .. deathlog.last_words .. COMM_FIELD_DELIM + local checksum = fletcher16(UnitName("player"), guildName, UnitLevel("player")) + local lastWordsMsg = checksum .. COMM_FIELD_DELIM .. deathlog.last_words .. COMM_FIELD_DELIM - table.insert(deathlog.last_words_queue, msg) + table.insert(deathlog.last_words_queue, lastWordsMsg) end function deathlog:setLastWords(...) diff --git a/Modules/deathlog/deathlog_spec.lua b/Modules/deathlog/deathlog_spec.lua index 8e250a72..9d0d6b09 100644 --- a/Modules/deathlog/deathlog_spec.lua +++ b/Modules/deathlog/deathlog_spec.lua @@ -1,17 +1,24 @@ _G.hardcore_settings = {} +_G.Recorded_Deaths = {} _G.hc_peer_guilds = {} _G.date = os.date _G.bit = require("bit32") -_G.GetChannelName = function(name) return 1 end _G.ChatThrottleLib = {} +_G.C_Map = {} +_G.C_Timer = { + After = function(_, f) f() end +} -local deathlog = require('Modules/deathlog/deathlog') +local deathlog = require('Modules.deathlog.deathlog') local ui_mock = require('Modules.deathlog.ui_mock') +deathlog.ui = ui_mock +require('npc_to_id_classic') +local last_attack_source = "Blackrock Champion" local testPlayer = { name = "Zunter", guild = "Hardcore Academy", - source_id = "435", + source_id = npc_to_id[last_attack_source], race_id = "1", class_id = 9, level = "17", @@ -36,15 +43,48 @@ local testPlayerData = deathlog.PlayerData( testPlayer.map_id, deathlog.mapPosToString(testPlayer.map_pos), nil, - nil) + nil, + testPlayer.guid) -describe('Deathlog', function() - before_each(function() - deathlog.ui = ui_mock - end) +_G.GetChannelName = function(name) return 1 end +_G.GetServerTime = function() return 1681863205 end +_G.C_Map.GetBestMapForUnit = function(_) return testPlayer.map_id end +_G.C_Map.GetPlayerMapPosition = function(_, _) return testPlayer.map_pos end +_G.GetGuildInfo = function(_) return testPlayer.guild end +_G.UnitRace = function(_) return _, _, testPlayer.race_id end +_G.UnitClass = function(_) return _, _, testPlayer.class_id end +_G.UnitName = function(_) return testPlayer.name end +_G.UnitLevel = function(_) return testPlayer.level end +_G.UnitGUID = function(_) return testPlayer.guid end +_G.GetNormalizedRealmName = function() return "BloodsailBuccaneers" end +_G.GetNumGuildMembers = function() return 10 end +_G.GetGuildRosterInfo = function(_) return testPlayer.name .. "-BloodsailBuccaneers", _, _, tonumber(testPlayer.level), _, _, _, _, _, _, _ end +describe('Deathlog', function() after_each(function() hardcore_settings["death_log_entries"] = {} + deathlog.death_ping_lru_cache_tbl = {} + deathlog.last_attack_source = "" + deathlog.last_words = "" + deathlog.broadcast_death_ping_queue = {} + deathlog.last_words_queue = {} + deathlog.death_alert_out_queue = {} + deathlog.death_reports_this_session = {} + end) + + it('should decodeMessage with a previous version data (no guid)', function() + local message = "Zunter~Hardcore Academy~435~1~9~17~~1433~0.3122,0.1521~" + local playerData = deathlog.decodeMessage(message) + + assert.are.equal("Zunter", playerData["name"]) + assert.are.equal("Hardcore Academy", playerData["guild"]) + assert.are.equal("435", tostring(playerData["source_id"])) + assert.are.equal("1", tostring(playerData["race_id"])) + assert.are.equal(9, playerData["class_id"]) + assert.are.equal("17", tostring(playerData["level"])) + assert.are.equal(nil, playerData["instance_id"]) + assert.are.equal(1433, playerData["map_id"]) + assert.are.equal("0.3122,0.1521", playerData["map_pos"]) end) it('should encodeMessage and decodeMessage correct data', function() @@ -54,7 +94,7 @@ describe('Deathlog', function() assert.are.equal(params.name, playerData["name"]) assert.are.equal(params.guild, playerData["guild"]) - assert.are.equal(params.source_id, tostring(playerData["source_id"])) + assert.are.equal(params.source_id, playerData["source_id"]) assert.are.equal(params.race_id, tostring(playerData["race_id"])) assert.are.equal(params.class_id, playerData["class_id"]) assert.are.equal(params.level, tostring(playerData["level"])) @@ -119,4 +159,35 @@ describe('Deathlog', function() assert.stub(_G.ChatThrottleLib.SendChatMessage).was.called(4) assert.are.equal(0, #deathlog.last_words_queue) end) -end) \ No newline at end of file + + it('should store the player guid if a valid death message is received', function() + stub(deathlog, "isValidEntry") + local message = deathlog.encodeMessage(testPlayer) + deathlog.receiveChannelMessage(testPlayer.name, message) + + local expectedRecordedTime = _G.GetServerTime() + local key = "Player-5139-020C481D-Zunter" + assert.are.equal(expectedRecordedTime, Recorded_Deaths[key]) + assert.is_true(deathlog.death_reports_this_session[testPlayer.name]) + + local expectedChecksum = fletcher16(testPlayer.name, testPlayer.guild, testPlayer.level) + assert.are.equal(1, #deathlog.broadcast_death_ping_queue) + assert.are.equal(expectedChecksum, deathlog.broadcast_death_ping_queue[1]) + end) + + it('should emit a death message on PLAYER_DEAD event', function() + local expectedDeathMsg = deathlog.encodeMessage(testPlayer) + local lastWords = "foobar" + local expectedLastWordsMsg = fletcher16(testPlayer.name, testPlayer.guild, testPlayer.level) .. "~" .. lastWords .. "~" + + deathlog.last_attack_source = last_attack_source + deathlog.last_words = lastWords + deathlog:PLAYER_DEAD() + + assert.are.equal(1, #deathlog.death_alert_out_queue) + assert.are.equal(expectedDeathMsg, deathlog.death_alert_out_queue[1]) + + assert.are.equal(1, #deathlog.last_words_queue) + assert.are.equal(expectedLastWordsMsg, deathlog.last_words_queue[1]) + end) +end) diff --git a/Modules/deathlog/ui.lua b/Modules/deathlog/ui.lua index 0611f93d..ac35ca60 100644 --- a/Modules/deathlog/ui.lua +++ b/Modules/deathlog/ui.lua @@ -8,6 +8,15 @@ end local AceGUI = LibStub("AceGUI-3.0") +local environment_damage = { + [-2] = "Drowning", + [-3] = "Falling", + [-4] = "Fatigue", + [-5] = "Fire", + [-6] = "Lava", + [-7] = "Slime", + } + -- Death log icon local death_log_icon_frame = CreateFrame("frame") death_log_icon_frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0) @@ -359,3 +368,14 @@ function ui.InsertEntry(player_data) end setEntry(player_data, row_entry[20]) end + + +function ui.SetLastWords(name, last_words) + for i = 1, 20 do + if row_entry[i].player_data ~= nil then + if row_entry[i].player_data["name"] == name then + row_entry[i].player_data["last_words"] = msg + end + end + end +end \ No newline at end of file From cedd4ef5c77d359627141935115e9d72d9759498 Mon Sep 17 00:00:00 2001 From: Taylor Petrychyn Date: Thu, 20 Apr 2023 12:02:58 -0700 Subject: [PATCH 6/8] add an internal toggle command for recording deaths --- Modules/deathlog/deathlog.lua | 42 ++++++++++++++++++++++-------- Modules/deathlog/deathlog_spec.lua | 26 ++++++++++++++---- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/Modules/deathlog/deathlog.lua b/Modules/deathlog/deathlog.lua index 1fd26177..3e863bfd 100644 --- a/Modules/deathlog/deathlog.lua +++ b/Modules/deathlog/deathlog.lua @@ -35,9 +35,9 @@ deathlog.broadcast_death_ping_queue = {} deathlog.last_words_queue = {} deathlog.death_alert_out_queue = {} --- store the count of times a player has reported their death, --- prevents a malicious actor from filling up the reported_deaths list -deathlog.death_reports_this_session = {} +-- store the count of times a player has reported their death, +-- prevents a malicious actor from filling up the Recorded_Deaths list +deathlog.sender_reported_death_timestamp = {} function fletcher16(player_name, player_guild, player_level) local data = player_name .. player_guild .. player_level @@ -244,15 +244,10 @@ function deathlog.receiveChannelMessage(sender, data) if sender ~= decoded_player_data["name"] then return end if deathlog.isValidEntry(decoded_player_data) == false then return end - if decoded_player_data["guid"] ~= nil then + if hardcore_settings["record_other_deaths"] and decoded_player_data["guid"] ~= nil then local guid = decoded_player_data["guid"] - local name = decoded_player_data["name"] - local key = string.format("%s-%s", guid, name) - if Recorded_Deaths[key] ~= nil or deathlog.death_reports_this_session[name] then - return -- already recorded this death - end - deathlog.death_reports_this_session[name] = true - Recorded_Deaths[key] = GetServerTime() + local source_id = decoded_player_data["source_id"] + deathlog.recordDeath(sender, guid, source_id) end local checksum = fletcher16(decoded_player_data["name"], decoded_player_data["guild"], decoded_player_data["level"]) @@ -290,6 +285,21 @@ function deathlog.receiveChannelMessage(sender, data) end end +function deathlog.recordDeath(sender, guid, source_id) + local time = GetServerTime() + if deathlog.sender_reported_death_timestamp[sender] ~= nil and + (time - deathlog.sender_reported_death_timestamp[sender]) < 300 then + return -- already recorded this death in the last 5 minutes + end + deathlog.sender_reported_death_timestamp[sender] = time + table.insert(Recorded_Deaths, { + sender = sender, + guid = guid, + source_id = source_id, + time = time + }) +end + function deathlog.receiveChannelMessageChecksum(sender, checksum) if checksum == nil then return end if deathlog.death_ping_lru_cache_tbl[checksum] == nil then @@ -554,6 +564,16 @@ function deathlog:PLAYER_LOGIN() self:RegisterEvent("CHAT_MSG_GUILD") end +function DeathlogToggleRecordOtherDeaths() + if hardcore_settings["record_other_deaths"] == nil or hardcore_settings["record_other_deaths"] == false then + print("record other deaths: enabled") + hardcore_settings["record_other_deaths"] = true + else + print("record other deaths: disabled") + hardcore_settings["record_other_deaths"] = false + end +end + function deathlog:startup() if type(addon) ~= "table" then -- only bind event listeners if we are inside wow diff --git a/Modules/deathlog/deathlog_spec.lua b/Modules/deathlog/deathlog_spec.lua index 9d0d6b09..79975684 100644 --- a/Modules/deathlog/deathlog_spec.lua +++ b/Modules/deathlog/deathlog_spec.lua @@ -61,6 +61,9 @@ _G.GetNumGuildMembers = function() return 10 end _G.GetGuildRosterInfo = function(_) return testPlayer.name .. "-BloodsailBuccaneers", _, _, tonumber(testPlayer.level), _, _, _, _, _, _, _ end describe('Deathlog', function() + before_each(function() + stub(deathlog, "isValidEntry") + end) after_each(function() hardcore_settings["death_log_entries"] = {} deathlog.death_ping_lru_cache_tbl = {} @@ -161,15 +164,28 @@ describe('Deathlog', function() end) it('should store the player guid if a valid death message is received', function() - stub(deathlog, "isValidEntry") + _G.hardcore_settings["record_other_deaths"] = true + local message = deathlog.encodeMessage(testPlayer) deathlog.receiveChannelMessage(testPlayer.name, message) - local expectedRecordedTime = _G.GetServerTime() - local key = "Player-5139-020C481D-Zunter" - assert.are.equal(expectedRecordedTime, Recorded_Deaths[key]) - assert.is_true(deathlog.death_reports_this_session[testPlayer.name]) + assert.are.equal(1, #Recorded_Deaths) + local recordedDeath = Recorded_Deaths[1] + assert.are.equal(testPlayer.name, recordedDeath.sender) + assert.are.equal(testPlayer.guid, recordedDeath.guid) + assert.are.equal(testPlayer.source_id, recordedDeath.source_id) + assert.are.equal(_G.GetServerTime(), recordedDeath.time) + assert.are.equal(_G.GetServerTime(), deathlog.sender_reported_death_timestamp[testPlayer.name]) + + -- check that receiving the same message again within 5 mins does not record another entry + deathlog.receiveChannelMessage(testPlayer.name, message) + assert.are.equal(1, #Recorded_Deaths) + end) + it('should broadcast a death ping when receiving a death message', function() + local message = deathlog.encodeMessage(testPlayer) + deathlog.receiveChannelMessage(testPlayer.name, message) + local expectedChecksum = fletcher16(testPlayer.name, testPlayer.guild, testPlayer.level) assert.are.equal(1, #deathlog.broadcast_death_ping_queue) assert.are.equal(expectedChecksum, deathlog.broadcast_death_ping_queue[1]) From 866a80937e5e2431d9618282f4bd06f454ec9ca0 Mon Sep 17 00:00:00 2001 From: Taylor Petrychyn Date: Fri, 21 Apr 2023 12:27:32 -0700 Subject: [PATCH 7/8] use existing death_log_entries but add config --- Hardcore.lua | 37 ++++++++++++++++++++++++- Modules/deathlog/deathlog.lua | 43 +++++------------------------- Modules/deathlog/deathlog_spec.lua | 24 ++++++++++------- 3 files changed, 57 insertions(+), 47 deletions(-) diff --git a/Hardcore.lua b/Hardcore.lua index 7aea705a..67ec18e8 100644 --- a/Hardcore.lua +++ b/Hardcore.lua @@ -75,6 +75,8 @@ Hardcore_Settings = { ignore_xguild_chat = false, ignore_xguild_alerts = false, global_custom_pronoun = false, + deathlog_require_verification = true, + deathlog_log_size = 1000, } WARNING = "" @@ -955,6 +957,8 @@ local settings_saved_variable_meta = { ["use_alternative_menu"] = false, ["ignore_xguild_chat"] = false, ["ignore_xguild_alerts"] = false, + ["deathlog_require_verification"] = true, + ["deathlog_log_size"] = 1000, } --[[ Post-utility functions]] @@ -987,7 +991,9 @@ function Hardcore:InitializeSettingsSavedVariables() end for k, v in pairs(settings_saved_variable_meta) do - Hardcore_Settings[k] = Hardcore_Settings[k] or v + if Hardcore_Settings[k] == nil then + Hardcore_Settings[k] = v + end end if Hardcore_Settings["alert_frame_scale"] <= 0 then @@ -4262,6 +4268,33 @@ local options = { end, order = 13, }, + deathlog_require_verification = { + type = "toggle", + name = "Only verified deaths", + desc = "Require deaths to be verified by guildmates", + get = function() + return Hardcore_Settings.deathlog_require_verification + end, + set = function(info, value) + Hardcore_Settings.deathlog_require_verification = value + end, + order = 14, + }, + deathlog_log_size = { + type = "range", + name = "Log Size", + desc = "How many deaths to store, increasing this limit can increase load time.", + min = 0, + max = 100000, + step = 1, + get = function() + return Hardcore_Settings.deathlog_log_size + end, + set = function(info, value) + Hardcore_Settings.deathlog_log_size = value + end, + order = 15, + }, }, }, cross_guild_header = { @@ -4315,6 +4348,8 @@ local options = { Hardcore_Settings.show_minimap_mailbox_icon = false Hardcore_Settings.ignore_xguild_alerts = false Hardcore_Settings.ignore_xguild_chat = false + Hardcore_Settings.deathlog_require_verification = true + Hardcore_Settings.deathlog_log_size = 1000 Hardcore:ApplyAlertFrameSettings() end, order = 20, diff --git a/Modules/deathlog/deathlog.lua b/Modules/deathlog/deathlog.lua index 3e863bfd..bbc7701e 100644 --- a/Modules/deathlog/deathlog.lua +++ b/Modules/deathlog/deathlog.lua @@ -19,7 +19,7 @@ local COMM_COMMANDS = { local COMM_COMMAND_DELIM = "$" local COMM_FIELD_DELIM = "~" local HC_REQUIRED_ACKS = 3 -local HC_DEATH_LOG_MAX = 1000 +local HC_DEATH_LOG_MAX_DEFAULT = 1000 local death_alerts_channel = "hcdeathalertschannel" local death_alerts_channel_pw = "hcdeathalertschannelpw" @@ -35,10 +35,6 @@ deathlog.broadcast_death_ping_queue = {} deathlog.last_words_queue = {} deathlog.death_alert_out_queue = {} --- store the count of times a player has reported their death, --- prevents a malicious actor from filling up the Recorded_Deaths list -deathlog.sender_reported_death_timestamp = {} - function fletcher16(player_name, player_guild, player_level) local data = player_name .. player_guild .. player_level local sum1 = 0 @@ -64,6 +60,9 @@ function deathlog.shouldCreateEntry(checksum) if deathlog.death_ping_lru_cache_tbl[checksum] == nil then return false end if deathlog.death_ping_lru_cache_tbl[checksum]["player_data"] == nil then return false end if hardcore_settings.death_log_types == nil or hardcore_settings.death_log_types == "faction_wide" and deathlog.isValidEntry(deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]) then + if hardcore_settings.deathlog_require_verification == false then + return true + end if deathlog.death_ping_lru_cache_tbl[checksum]["peer_report"] and deathlog.death_ping_lru_cache_tbl[checksum]["peer_report"] > HC_REQUIRED_ACKS then return true else @@ -215,8 +214,9 @@ function deathlog.createEntry(checksum) end table.insert(hardcore_settings["death_log_entries"], deathlog.death_ping_lru_cache_tbl[checksum]["player_data"]) + local entry_limit = hardcore_settings["deathlog_log_size"] or HC_DEATH_LOG_MAX_DEFAULT -- Cap list size, otherwise loading time will increase - if hardcore_settings["death_log_entries"] and #hardcore_settings["death_log_entries"] > HC_DEATH_LOG_MAX then -- TODO parameterize + if hardcore_settings["death_log_entries"] and #hardcore_settings["death_log_entries"] > entry_limit then table.remove(hardcore_settings["death_log_entries"], 1) end @@ -244,12 +244,6 @@ function deathlog.receiveChannelMessage(sender, data) if sender ~= decoded_player_data["name"] then return end if deathlog.isValidEntry(decoded_player_data) == false then return end - if hardcore_settings["record_other_deaths"] and decoded_player_data["guid"] ~= nil then - local guid = decoded_player_data["guid"] - local source_id = decoded_player_data["source_id"] - deathlog.recordDeath(sender, guid, source_id) - end - local checksum = fletcher16(decoded_player_data["name"], decoded_player_data["guild"], decoded_player_data["level"]) if deathlog.death_ping_lru_cache_tbl[checksum] == nil then @@ -285,21 +279,6 @@ function deathlog.receiveChannelMessage(sender, data) end end -function deathlog.recordDeath(sender, guid, source_id) - local time = GetServerTime() - if deathlog.sender_reported_death_timestamp[sender] ~= nil and - (time - deathlog.sender_reported_death_timestamp[sender]) < 300 then - return -- already recorded this death in the last 5 minutes - end - deathlog.sender_reported_death_timestamp[sender] = time - table.insert(Recorded_Deaths, { - sender = sender, - guid = guid, - source_id = source_id, - time = time - }) -end - function deathlog.receiveChannelMessageChecksum(sender, checksum) if checksum == nil then return end if deathlog.death_ping_lru_cache_tbl[checksum] == nil then @@ -564,16 +543,6 @@ function deathlog:PLAYER_LOGIN() self:RegisterEvent("CHAT_MSG_GUILD") end -function DeathlogToggleRecordOtherDeaths() - if hardcore_settings["record_other_deaths"] == nil or hardcore_settings["record_other_deaths"] == false then - print("record other deaths: enabled") - hardcore_settings["record_other_deaths"] = true - else - print("record other deaths: disabled") - hardcore_settings["record_other_deaths"] = false - end -end - function deathlog:startup() if type(addon) ~= "table" then -- only bind event listeners if we are inside wow diff --git a/Modules/deathlog/deathlog_spec.lua b/Modules/deathlog/deathlog_spec.lua index 79975684..24ac8f63 100644 --- a/Modules/deathlog/deathlog_spec.lua +++ b/Modules/deathlog/deathlog_spec.lua @@ -47,7 +47,7 @@ local testPlayerData = deathlog.PlayerData( testPlayer.guid) _G.GetChannelName = function(name) return 1 end -_G.GetServerTime = function() return 1681863205 end +_G.date = function() return "Sat Apr 15 09:12:04 2023" end _G.C_Map.GetBestMapForUnit = function(_) return testPlayer.map_id end _G.C_Map.GetPlayerMapPosition = function(_, _) return testPlayer.map_pos end _G.GetGuildInfo = function(_) return testPlayer.guild end @@ -164,22 +164,28 @@ describe('Deathlog', function() end) it('should store the player guid if a valid death message is received', function() - _G.hardcore_settings["record_other_deaths"] = true + _G.hardcore_settings["deathlog_require_verification"] = false local message = deathlog.encodeMessage(testPlayer) + print(message) deathlog.receiveChannelMessage(testPlayer.name, message) - assert.are.equal(1, #Recorded_Deaths) - local recordedDeath = Recorded_Deaths[1] - assert.are.equal(testPlayer.name, recordedDeath.sender) + assert.are.equal(1, #hardcore_settings["death_log_entries"]) + local recordedDeath = hardcore_settings["death_log_entries"][1] + assert.are.equal(testPlayer.name, recordedDeath.name) assert.are.equal(testPlayer.guid, recordedDeath.guid) + assert.are.equal(testPlayer.guild, recordedDeath.guild) + assert.are.equal(testPlayer.race_id, tostring(recordedDeath.race_id)) + assert.are.equal(testPlayer.class_id, recordedDeath.class_id) + assert.are.equal(testPlayer.level, tostring(recordedDeath.level)) assert.are.equal(testPlayer.source_id, recordedDeath.source_id) - assert.are.equal(_G.GetServerTime(), recordedDeath.time) - assert.are.equal(_G.GetServerTime(), deathlog.sender_reported_death_timestamp[testPlayer.name]) + assert.are.equal(testPlayer.instance_id, recordedDeath.instance_id) + assert.are.equal(deathlog.mapPosToString(testPlayer.map_pos), recordedDeath.map_pos) + assert.are.equal(date(), recordedDeath.date) - -- check that receiving the same message again within 5 mins does not record another entry + -- check that receiving the same message again does not record another entry deathlog.receiveChannelMessage(testPlayer.name, message) - assert.are.equal(1, #Recorded_Deaths) + assert.are.equal(1, #hardcore_settings["death_log_entries"]) end) it('should broadcast a death ping when receiving a death message', function() From 2ff4349270ef062202cdc4b27f324bb0f3213cc8 Mon Sep 17 00:00:00 2001 From: Taylor Petrychyn Date: Fri, 21 Apr 2023 12:47:38 -0700 Subject: [PATCH 8/8] better toggle description --- Hardcore.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Hardcore.lua b/Hardcore.lua index 67ec18e8..712526ec 100644 --- a/Hardcore.lua +++ b/Hardcore.lua @@ -4271,7 +4271,7 @@ local options = { deathlog_require_verification = { type = "toggle", name = "Only verified deaths", - desc = "Require deaths to be verified by guildmates", + desc = "Only show deaths that have been verified by the player's guildmates", get = function() return Hardcore_Settings.deathlog_require_verification end,