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.lua b/Hardcore.lua index 8c91778a..712526ec 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 @@ -73,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 = "" @@ -212,7 +216,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 @@ -954,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]] @@ -986,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 @@ -1694,11 +1701,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 +1824,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 +2579,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 +3912,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 +4017,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, }, @@ -4282,6 +4268,33 @@ local options = { end, order = 13, }, + deathlog_require_verification = { + type = "toggle", + name = "Only verified deaths", + desc = "Only show deaths that have been verified by the player's 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 = { @@ -4335,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/Hardcore_Classic.toc b/Hardcore_Classic.toc index 96fbfab0..3d15af75 100644 --- a/Hardcore_Classic.toc +++ b/Hardcore_Classic.toc @@ -161,7 +161,8 @@ TextureInfo.lua AchievementAlertFrame.lua id_to_npc_classic.lua npc_to_id_classic.lua -DeathLog.lua +Modules/deathlog/deathlog.lua +Modules/deathlog/ui.lua Hardcore.lua Libs/GreenWall/Constants.lua diff --git a/Modules/deathlog/deathlog.lua b/Modules/deathlog/deathlog.lua new file mode 100644 index 00000000..bbc7701e --- /dev/null +++ b/Modules/deathlog/deathlog.lua @@ -0,0 +1,562 @@ +local deathlog = {} + +local _, addon = ... +-- check if we are running in wow +if type(addon) == "table" then + 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", + ["LAST_WORDS"] = "3", +} +local COMM_COMMAND_DELIM = "$" +local COMM_FIELD_DELIM = "~" +local HC_REQUIRED_ACKS = 3 +local HC_DEATH_LOG_MAX_DEFAULT = 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 = {} + +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 + +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 + 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 + +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 + 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: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, guid) + 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, + ["guid"] = guid + } +end + +local encodeMessageParams = { + name = "", + guild = "", + source_id = "", + race_id = "", + class_id = nil, + level = "", + instance_id = nil, + map_id = nil, + 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 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, guid) + 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"]) + + 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"] > entry_limit then + 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 deathlog.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, _, _ = 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 + 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 deathlog.shouldCreateEntry(checksum) then + deathlog.createEntry(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 + 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 + +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 + self:sendNextInQueue(COMM_COMMANDS["BROADCAST_DEATH_PING"], self.death_alert_out_queue) + return + end + + if #self.last_words_queue > 0 then + 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(...) + 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 + + deathlog.receiveLastWords(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") + else + local _, _, _, _, _, _, _, _instance_id, _, _ = GetInstanceInfo() + instance_id = _instance_id + end + + local guildName, _, _ = 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 + + 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, deathMsg) + + if deathlog.last_words == nil then return end + if guildName == nil then guildName = "" end + + 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, lastWordsMsg) +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 +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: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 + return + end + + -- event handling helper + self:SetScript("OnEvent", function(self, event, ...) + self[event](self, ...) + end) + + self:RegisterEvent("PLAYER_LOGIN") +end + +deathlog:startup() + +return deathlog diff --git a/Modules/deathlog/deathlog_spec.lua b/Modules/deathlog/deathlog_spec.lua new file mode 100644 index 00000000..24ac8f63 --- /dev/null +++ b/Modules/deathlog/deathlog_spec.lua @@ -0,0 +1,215 @@ +_G.hardcore_settings = {} +_G.Recorded_Deaths = {} +_G.hc_peer_guilds = {} +_G.date = os.date +_G.bit = require("bit32") +_G.ChatThrottleLib = {} +_G.C_Map = {} +_G.C_Timer = { + After = function(_, f) f() end +} + +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 = npc_to_id[last_attack_source], + 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, + testPlayer.guid) + +_G.GetChannelName = function(name) return 1 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 +_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() + before_each(function() + stub(deathlog, "isValidEntry") + end) + 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() + 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, 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 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) + + it('should store the player guid if a valid death message is received', function() + _G.hardcore_settings["deathlog_require_verification"] = false + + local message = deathlog.encodeMessage(testPlayer) + print(message) + deathlog.receiveChannelMessage(testPlayer.name, message) + + 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(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 does not record another entry + deathlog.receiveChannelMessage(testPlayer.name, message) + assert.are.equal(1, #hardcore_settings["death_log_entries"]) + 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]) + 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 new file mode 100644 index 00000000..ac35ca60 --- /dev/null +++ b/Modules/deathlog/ui.lua @@ -0,0 +1,381 @@ +local ui = {} + +local _, addon = ... +-- check if we are running in wow +if type(addon) == "table" then + addon.deathlog.ui = ui +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) +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 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 + + +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 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