diff --git a/Details.toc b/Details.toc index ac0f77be6..5727930dc 100644 --- a/Details.toc +++ b/Details.toc @@ -28,6 +28,7 @@ API.lua functions\profiles.lua functions\hooks.lua functions\bossmods.lua +functions\coach.lua functions\skins.lua functions\boss.lua functions\spells.lua diff --git a/Libs/DF/fw.lua b/Libs/DF/fw.lua index 158e16d9e..e03d3f19f 100644 --- a/Libs/DF/fw.lua +++ b/Libs/DF/fw.lua @@ -1,6 +1,6 @@ -local dversion = 214 +local dversion = 215 local major, minor = "DetailsFramework-1.0", dversion local DF, oldminor = LibStub:NewLibrary (major, minor) @@ -609,7 +609,26 @@ function DF:TruncateText (fontString, maxWidth) if (string.len (text) <= 1) then break end - end + end + + DF:CleanTruncateUTF8String(text) + fontString:SetText (text) +end + +function DF:CleanTruncateUTF8String(text) + if type(text) == "string" and text ~= "" then + local b1 = (#text > 0) and strbyte(strsub(text, #text, #text)) or nil + local b2 = (#text > 1) and strbyte(strsub(text, #text-1, #text)) or nil + local b3 = (#text > 2) and strbyte(strsub(text, #text-2, #text)) or nil + if b1 and b1 >= 194 and b1 <= 244 then + text = strsub (text, 1, #text - 1) + elseif b2 and b2 >= 224 and b2 <= 244 then + text = strsub (text, 1, #text - 2) + elseif b3 and b3 >= 240 and b3 <= 244 then + text = strsub (text, 1, #text - 3) + end + end + return text end function DF:Msg (msg, ...) diff --git a/boot.lua b/boot.lua index beaa89fc4..33392e7d6 100644 --- a/boot.lua +++ b/boot.lua @@ -4,11 +4,11 @@ _ = nil _detalhes = LibStub("AceAddon-3.0"):NewAddon("_detalhes", "AceTimer-3.0", "AceComm-3.0", "AceSerializer-3.0", "NickTag-1.0") - _detalhes.build_counter = 7938 - _detalhes.alpha_build_counter = 7938 --if this is higher than the regular counter, use it instead + _detalhes.build_counter = 7950 + _detalhes.alpha_build_counter = 7950 --if this is higher than the regular counter, use it instead _detalhes.game_version = "v9.0.1" _detalhes.userversion = "v9.0.1." .. _detalhes.build_counter - _detalhes.realversion = 143 --core version, this is used to check API version for scripts and plugins (see alias below) + _detalhes.realversion = 144 --core version, this is used to check API version for scripts and plugins (see alias below) _detalhes.APIVersion = _detalhes.realversion --core version _detalhes.version = _detalhes.userversion .. " (core " .. _detalhes.realversion .. ")" --simple stirng to show to players @@ -28,8 +28,16 @@ do local Loc = _G.LibStub("AceLocale-3.0"):GetLocale( "Details" ) local news = { + {"v9.0.1.7950.144", "November 3rd, 2020"}, + "Added the baseline for the Coach feature, for testing use '/details coach', all users in the raid must have details! up to date.", + "Added container_spells:GetOrCreateSpell(id, shouldCreate, token).", + "Added Details:GetRaidLeader(), return the RL name.", + "Fixed Tiny Threat not showing threat.", + "Fixed annoucement interrupt enable toggle checkbox was reseting on logon.", + {"v9.0.1.7938.142", "October 29th, 2020"}, "Added option to select the icon buttons in the title bar.", + {"v9.0.1.7739.142", "August 18th, 2020"}, "More development on the new plugin Cast Timeline.", "More development on Details! Scroll Damage.", @@ -333,7 +341,7 @@ do --> player detail skin _detalhes.playerdetailwindow_skins = {} - _detalhes.BitfieldSwapDebuffsIDs = {265646, 272407, 269691, 273401, 269131, 260900, 260926, 284995, 292826, 311367, 310567, 308996, 307832, 327414} + _detalhes.BitfieldSwapDebuffsIDs = {265646, 272407, 269691, 273401, 269131, 260900, 260926, 284995, 292826, 311367, 310567, 308996, 307832, 327414, 337253} --> auto run code _detalhes.RunCodeTypes = { diff --git a/classes/class_instance.lua b/classes/class_instance.lua index 62eb35d78..08f3cca20 100644 --- a/classes/class_instance.lua +++ b/classes/class_instance.lua @@ -608,13 +608,13 @@ end if (not self.iniciada) then self:RestauraJanela (self.meu_id, nil, true) --parece que esta chamando o ativar instance denovo... passei true no load_only vamos ver o resultado + --tiny threat parou de funcionar depois de /reload depois dessa mudança, talvez tenha algo para carregar ainda self.iniciada = true else _detalhes.opened_windows = _detalhes.opened_windows+1 end self:ChangeSkin() --carrega a skin aqui que era antes feito dentro do restaura janela - _detalhes:TrocaTabela (self, nil, nil, nil, true) if (self.hide_icon) then @@ -634,7 +634,7 @@ end if (not temp) then if (self.modo == modo_raid) then - _detalhes.RaidTables:EnableRaidMode (self) + _detalhes.RaidTables:EnableRaidMode(self) elseif (self.modo == modo_alone) then self:SoloMode (true) @@ -656,6 +656,14 @@ end if (not temp and not _detalhes.initializing) then _detalhes:SendEvent ("DETAILS_INSTANCE_OPEN", nil, self) end + + if (self.modo == modo_raid) then + _detalhes.RaidTables:EnableRaidMode(self) + + elseif (self.modo == modo_alone) then + self:SoloMode (true) + end + end ------------------------------------------------------------------------------------------------------------------------ diff --git a/classes/container_actors.lua b/classes/container_actors.lua index 0f59a82a6..d4fa262a1 100644 --- a/classes/container_actors.lua +++ b/classes/container_actors.lua @@ -27,8 +27,7 @@ local GetNumDeclensionSets = _G.GetNumDeclensionSets local DeclineName = _G.DeclineName - - local GetLocale = _G.GetLocale + ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --> constants @@ -438,7 +437,7 @@ --if the user client is in russian language --make an attempt to remove declensions from the character's name --this is equivalent to remove 's from the owner on enUS - if (GetLocale() == "ruRU") then + if (CONST_CLIENT_LANGUAGE == "ruRU") then if (find_name_declension (text1, playerName)) then return find_pet_found_owner (pName, serial, nome, flag, self) else @@ -462,7 +461,7 @@ local pName = playerName playerName = playerName:gsub ("%-.*", "") --remove realm name - if (GetLocale() == "ruRU") then + if (CONST_CLIENT_LANGUAGE == "ruRU") then if (find_name_declension (text2, playerName)) then return find_pet_found_owner (pName, serial, nome, flag, self) else diff --git a/classes/container_spells.lua b/classes/container_spells.lua index f0caa256c..0322d3cd0 100644 --- a/classes/container_spells.lua +++ b/classes/container_spells.lua @@ -59,7 +59,11 @@ local _ return pairs (self._ActorTable) end - function container_habilidades:PegaHabilidade (id, criar, token, cria_shadow) + function container_habilidades:GetOrCreateSpell(id, shouldCreate, token) + return self:PegaHabilidade (id, shouldCreate, token) + end + + function container_habilidades:PegaHabilidade (id, criar, token) local esta_habilidade = self._ActorTable [id] @@ -68,7 +72,7 @@ local _ else if (criar) then - local novo_objeto = self.funcao_de_criacao (nil, id, shadow_objeto, token) + local novo_objeto = self.funcao_de_criacao (nil, id, nil, token) self._ActorTable [id] = novo_objeto diff --git a/core/gears.lua b/core/gears.lua index a2f8505b8..ba2bfa17f 100644 --- a/core/gears.lua +++ b/core/gears.lua @@ -1758,6 +1758,8 @@ function Details.Database.StoreEncounter(combat) local mybest2 = mybest[1] or 0 local myBestDps = mybest2 / onencounter.elapsed + --[12:18:37] Details!: error occurred on Details.Database.StoreEncounter(): Interface\AddOns\Details\core\gears.lua:1758: attempt to index local 'mybest' (a nil value) + if (mybest) then local d_one = 0 if (myrole == "DAMAGER" or myrole == "TANK") then diff --git a/core/network.lua b/core/network.lua index efd9fffb8..a78dfd072 100644 --- a/core/network.lua +++ b/core/network.lua @@ -22,9 +22,7 @@ ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --> constants - local CONST_REALM_SYNC_ENABLED = false - - local CONST_DETAILS_PREFIX = "DTLS" + DETAILS_PREFIX_NETWORK = "DTLS" local CONST_HIGHFIVE_REQUEST = "HI" local CONST_HIGHFIVE_DATA = "HF" @@ -48,6 +46,8 @@ local CONST_PVP_ENEMY = "PP" local CONST_ROGUE_SR = "SR" --soul rip from akaari's soul (LEGION ONLY) + + DETAILS_PREFIX_COACH = "CO" --coach feature _detalhes.network.ids = { ["HIGHFIVE_REQUEST"] = CONST_HIGHFIVE_REQUEST, @@ -69,6 +69,8 @@ ["MISSDATA_ROGUE_SOULRIP"] = CONST_ROGUE_SR, --soul rip from akaari's soul (LEGION ONLY) ["CLOUD_SHAREDATA"] = CONST_CLOUD_SHAREDATA, + + ["COACH_FEATURE"] = DETAILS_PREFIX_COACH, --ask the raid leader is the coach is enbaled } local plugins_registred = {} @@ -204,7 +206,7 @@ if (_detalhes.debug) then _detalhes:Msg ("(debug) sent 'okey' answer for a cloud parser request.") end - _detalhes:SendCommMessage (CONST_DETAILS_PREFIX, _detalhes:Serialize (_detalhes.network.ids.CLOUD_FOUND, _UnitName ("player"), _GetRealmName(), _detalhes.realversion), "WHISPER", player) + _detalhes:SendCommMessage (DETAILS_PREFIX_NETWORK, _detalhes:Serialize (_detalhes.network.ids.CLOUD_FOUND, _UnitName ("player"), _GetRealmName(), _detalhes.realversion), "WHISPER", player) end end end @@ -263,7 +265,7 @@ _detalhes:Msg ("(debug) requesting data from the cloud.") end - _detalhes:SendCommMessage (CONST_DETAILS_PREFIX, _detalhes:Serialize (CONST_CLOUD_DATARC, atributo, atributo_name, export), "WHISPER", _detalhes.host_of) + _detalhes:SendCommMessage (DETAILS_PREFIX_NETWORK, _detalhes:Serialize (CONST_CLOUD_DATARC, atributo, atributo_name, export), "WHISPER", _detalhes.host_of) _table_wipe (temp) end end @@ -366,6 +368,62 @@ end + --"CIEA" Coach Is Enabled Ask (client > server) + --"CIER" Coach Is Enabled Response (server > client) + --"CCS" Coach Combat Start (client > server) + function _detalhes.network.Coach(player, realm, core_version, msgType, data) + if (not IsInRaid()) then + return + end + + local sourcePlayer = Ambiguate(player .. "-" .. realm, "none") + local playerName = UnitName("player") + if (playerName == sourcePlayer) then + return + end + + if (core_version ~= _detalhes.realversion) then + if (core_version > _detalhes.realversion) then + Details:Msg ("your Details! is out dated and cannot use Coach feature.") + end + return false + end + + if (msgType == "CIEA") then --Coach Is Enabled Ask (regular player asked to raid leader) + --check if the player that received the msg is the raid leader + if (UnitIsGroupLeader("player")) then + return + end + + --send the answer + Details:SendCommMessage(DETAILS_PREFIX_NETWORK, Details:Serialize(DETAILS_PREFIX_COACH, playerName, GetRealmName(), Details.realversion, "CIER", Details.Coach.Server.IsEnabled()), "WHISPER", sourcePlayer) + + elseif (msgType == "CIER") then --Coach Is Enabled Response (regular player received a raid leader response) + local isEnabled = data + Details.Coach.Client.CoachIsEnabled_Response(isEnabled, sourcePlayer) + + elseif (msgType == "CCS") then --Coach Combat Start (raid assistant told the raid leader a combat started) + Details.Coach.Server.CombatStarted() + + elseif (msgType == "CCE") then --Coach Combat End (raid assistant told the raid leader a combat ended) + Details.Coach.Server.CombatEnded() + + elseif (msgType == "CS") then --Coach Start (raid leader notifying other members of the group) + Details.Coach.Client.EnableCoach(sourcePlayer) + + elseif (msgType == "CE") then --Coach End (raid leader notifying other members of the group) + Details.Coach.Client.CoachEnd() + + elseif (msgType == "CDT") then --Coach Data (a player in the raid sent to raid leader combat data) + if (UnitIsGroupLeader("player")) then + if (Details.Coach.Server.IsEnabled()) then + --update the current combat with new information + + end + end + end + end + --guild sync R = someone pressed the sync button --guild sync L = list of fights IDs --guild sync G = requested a list of encounters @@ -397,7 +455,7 @@ if (IDs and IDs [1]) then local from = UnitName ("player") local realm = GetRealmName() - _detalhes:SendCommMessage (CONST_DETAILS_PREFIX, _detalhes:Serialize (CONST_GUILD_SYNC, from, realm, _detalhes.realversion, "L", IDs), "WHISPER", chr_name) + _detalhes:SendCommMessage (DETAILS_PREFIX_NETWORK, _detalhes:Serialize (CONST_GUILD_SYNC, from, realm, _detalhes.realversion, "L", IDs), "WHISPER", chr_name) end _detalhes.LastGuildSyncDataTime1 = GetTime() + 60 @@ -409,7 +467,7 @@ if (MissingIDs and MissingIDs [1]) then local from = UnitName ("player") local realm = GetRealmName() - _detalhes:SendCommMessage (CONST_DETAILS_PREFIX, _detalhes:Serialize (CONST_GUILD_SYNC, from, realm, _detalhes.realversion, "G", MissingIDs), "WHISPER", chr_name) + _detalhes:SendCommMessage (DETAILS_PREFIX_NETWORK, _detalhes:Serialize (CONST_GUILD_SYNC, from, realm, _detalhes.realversion, "G", MissingIDs), "WHISPER", chr_name) end return true @@ -429,7 +487,7 @@ local from = UnitName ("player") local realm = GetRealmName() --todo: need to check if the target is still online - _detalhes:SendCommMessage (CONST_DETAILS_PREFIX, _detalhes:Serialize (CONST_GUILD_SYNC, from, realm, _detalhes.realversion, "A", data), "WHISPER", task.Target) + _detalhes:SendCommMessage (DETAILS_PREFIX_NETWORK, _detalhes:Serialize (CONST_GUILD_SYNC, from, realm, _detalhes.realversion, "A", data), "WHISPER", task.Target) if (_detalhes.debug) then _detalhes:Msg ("(debug) [RoS-EncounterSync] send-task sending data #" .. task.TickID .. ".") @@ -497,6 +555,8 @@ [CONST_ROGUE_SR] = _detalhes.network.HandleMissData, --soul rip from akaari's soul (LEGION ONLY) [CONST_PVP_ENEMY] = _detalhes.network.ReceivedEnemyPlayer, + + [DETAILS_PREFIX_COACH] = _detalhes.network.Coach, --coach feature } ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -630,12 +690,12 @@ --> doesn't have realm at all, so we assume the actor is in same realm as player realm = _GetRealmName() end - _detalhes:SendCommMessage(CONST_DETAILS_PREFIX, _detalhes:Serialize (type, player, realm, _detalhes.realversion, ...), "RAID") + _detalhes:SendCommMessage(DETAILS_PREFIX_NETWORK, _detalhes:Serialize (type, player, realm, _detalhes.realversion, ...), "RAID") end function _detalhes:SendHomeRaidData(type, ...) if (IsInRaid(LE_PARTY_CATEGORY_HOME) and IsInInstance()) then - _detalhes:SendCommMessage(CONST_DETAILS_PREFIX, _detalhes:Serialize (type, _UnitName("player"), _GetRealmName(), _detalhes.realversion, ...), "RAID") + _detalhes:SendCommMessage(DETAILS_PREFIX_NETWORK, _detalhes:Serialize (type, _UnitName("player"), _GetRealmName(), _detalhes.realversion, ...), "RAID") end end @@ -644,12 +704,12 @@ local isInInstanceGroup = IsInRaid (LE_PARTY_CATEGORY_INSTANCE) if (isInInstanceGroup) then - _detalhes:SendCommMessage (CONST_DETAILS_PREFIX, _detalhes:Serialize (type, _UnitName("player"), _GetRealmName(), _detalhes.realversion, ...), "INSTANCE_CHAT") + _detalhes:SendCommMessage (DETAILS_PREFIX_NETWORK, _detalhes:Serialize (type, _UnitName("player"), _GetRealmName(), _detalhes.realversion, ...), "INSTANCE_CHAT") if (_detalhes.debug) then _detalhes:Msg ("(debug) sent comm to INSTANCE raid group") end else - _detalhes:SendCommMessage (CONST_DETAILS_PREFIX, _detalhes:Serialize (type, _UnitName("player"), _GetRealmName(), _detalhes.realversion, ...), "RAID") + _detalhes:SendCommMessage (DETAILS_PREFIX_NETWORK, _detalhes:Serialize (type, _UnitName("player"), _GetRealmName(), _detalhes.realversion, ...), "RAID") if (_detalhes.debug) then _detalhes:Msg ("(debug) sent comm to LOCAL raid group") end @@ -661,12 +721,12 @@ local isInInstanceGroup = IsInGroup (LE_PARTY_CATEGORY_INSTANCE) if (isInInstanceGroup) then - _detalhes:SendCommMessage (CONST_DETAILS_PREFIX, _detalhes:Serialize (type, _UnitName ("player"), _GetRealmName(), _detalhes.realversion, ...), "INSTANCE_CHAT") + _detalhes:SendCommMessage (DETAILS_PREFIX_NETWORK, _detalhes:Serialize (type, _UnitName ("player"), _GetRealmName(), _detalhes.realversion, ...), "INSTANCE_CHAT") if (_detalhes.debug) then _detalhes:Msg ("(debug) sent comm to INSTANCE party group") end else - _detalhes:SendCommMessage (CONST_DETAILS_PREFIX, _detalhes:Serialize (type, _UnitName ("player"), _GetRealmName(), _detalhes.realversion, ...), "PARTY") + _detalhes:SendCommMessage (DETAILS_PREFIX_NETWORK, _detalhes:Serialize (type, _UnitName ("player"), _GetRealmName(), _detalhes.realversion, ...), "PARTY") if (_detalhes.debug) then _detalhes:Msg ("(debug) sent comm to LOCAL party group") end @@ -684,7 +744,7 @@ function _detalhes:SendGuildData (type, ...) if not IsInGuild() then return end --> fix from Tim@WoWInterface - _detalhes:SendCommMessage (CONST_DETAILS_PREFIX, _detalhes:Serialize (type, _UnitName ("player"), _GetRealmName(), _detalhes.realversion, ...), "GUILD") + _detalhes:SendCommMessage (DETAILS_PREFIX_NETWORK, _detalhes:Serialize (type, _UnitName ("player"), _GetRealmName(), _detalhes.realversion, ...), "GUILD") end @@ -712,16 +772,16 @@ if (instancia.ativa) then local atributo = instancia.atributo if (atributo == 1 and not _detalhes:CaptureGet ("damage")) then - _detalhes:SendCommMessage (CONST_DETAILS_PREFIX, _detalhes:Serialize (CONST_CLOUD_DATARQ, atributo, instancia.sub_atributo), "WHISPER", _detalhes.host_by) + _detalhes:SendCommMessage (DETAILS_PREFIX_NETWORK, _detalhes:Serialize (CONST_CLOUD_DATARQ, atributo, instancia.sub_atributo), "WHISPER", _detalhes.host_by) break elseif (atributo == 2 and (not _detalhes:CaptureGet ("heal") or _detalhes:CaptureGet ("aura"))) then - _detalhes:SendCommMessage (CONST_DETAILS_PREFIX, _detalhes:Serialize (CONST_CLOUD_DATARQ, atributo, instancia.sub_atributo), "WHISPER", _detalhes.host_by) + _detalhes:SendCommMessage (DETAILS_PREFIX_NETWORK, _detalhes:Serialize (CONST_CLOUD_DATARQ, atributo, instancia.sub_atributo), "WHISPER", _detalhes.host_by) break elseif (atributo == 3 and not _detalhes:CaptureGet ("energy")) then - _detalhes:SendCommMessage (CONST_DETAILS_PREFIX, _detalhes:Serialize (CONST_CLOUD_DATARQ, atributo, instancia.sub_atributo), "WHISPER", _detalhes.host_by) + _detalhes:SendCommMessage (DETAILS_PREFIX_NETWORK, _detalhes:Serialize (CONST_CLOUD_DATARQ, atributo, instancia.sub_atributo), "WHISPER", _detalhes.host_by) break elseif (atributo == 4 and not _detalhes:CaptureGet ("miscdata")) then - _detalhes:SendCommMessage (CONST_DETAILS_PREFIX, _detalhes:Serialize (CONST_CLOUD_DATARQ, atributo, instancia.sub_atributo), "WHISPER", _detalhes.host_by) + _detalhes:SendCommMessage (DETAILS_PREFIX_NETWORK, _detalhes:Serialize (CONST_CLOUD_DATARQ, atributo, instancia.sub_atributo), "WHISPER", _detalhes.host_by) break end end diff --git a/core/util.lua b/core/util.lua index e74da46e5..150746b8c 100644 --- a/core/util.lua +++ b/core/util.lua @@ -63,6 +63,18 @@ end end end + + function Details:GetRaidLeader() + if (IsInRaid()) then + for i = 1, GetNumGroupMembers() do + local name, rank = GetRaidRosterInfo(i) + if (rank == 2) then + return name, "raid" .. i + end + end + end + return + end function _detalhes:UnpackDeathTable (t) local deathevents = t[1] @@ -860,6 +872,9 @@ end return true end end + + elseif (Details.Coach.Server.IsEnabled()) then + return true end --> don't leave the combat if is in the argus encounter ~REMOVE on 8.0 diff --git a/frames/window_options2_sections.lua b/frames/window_options2_sections.lua index c59d2b768..5e7822ddb 100644 --- a/frames/window_options2_sections.lua +++ b/frames/window_options2_sections.lua @@ -5132,7 +5132,7 @@ do --raid tools {--auto current segment type = "toggle", - get = function() return currentInstance.auto_current end, + get = function() return Details.announce_interrupts.enabled end, set = function (self, fixedparam, value) if (value) then _detalhes:EnableInterruptAnnouncer() diff --git a/functions/coach.lua b/functions/coach.lua new file mode 100644 index 000000000..015650667 --- /dev/null +++ b/functions/coach.lua @@ -0,0 +1,386 @@ + +local Details = _G.Details + +--stop yellow warning on my editor +local IsInRaid = _G.IsInRaid +local UnitIsGroupLeader = _G.UnitIsGroupLeader +local UnitIsGroupAssistant = _G.UnitIsGroupAssistant +local UnitName = _G.UnitName +local GetRealmName = _G.GetRealmName +local GetTime = _G.GetTime +local GetNumGroupMembers = _G.GetNumGroupMembers + +--return if the player is inside a raid zone +local isInRaidZone = function() + return Details.zone_type == "raid" +end + +--create a namespace using capital letter 'C' for coach feature, the profile entry is lower character .coach +Details.Coach = { + Client = { --regular player + enabled = false, + coachName = "", + }, + + Server = { --the raid leader + enabled = false, + lastCombatStartTime = 0, + lastCombatEndTime = 0, + }, + + isInRaidGroup = false, + isInRaidZone = false, +} + +function Details.Coach.AskRLForCoachStatus(raidLeaderName) + Details:SendCommMessage(_G.DETAILS_PREFIX_NETWORK, Details:Serialize(_G.DETAILS_PREFIX_COACH, UnitName("player"), GetRealmName(), Details.realversion, "CIEA"), "WHISPER", raidLeaderName) + Details:Msg("asked the raid leader the coach status.") +end + +function Details.Coach.SendRLCombatStartNotify(raidLeaderName) + Details:SendCommMessage(_G.DETAILS_PREFIX_NETWORK, Details:Serialize(_G.DETAILS_PREFIX_COACH, UnitName("player"), GetRealmName(), Details.realversion, "CCS"), "WHISPER", raidLeaderName) + Details:Msg("sent to raid leader a combat start notification.") +end + +function Details.Coach.SendRLCombatEndNotify(raidLeaderName) + Details:SendCommMessage(_G.DETAILS_PREFIX_NETWORK, Details:Serialize(_G.DETAILS_PREFIX_COACH, UnitName("player"), GetRealmName(), Details.realversion, "CCE"), "WHISPER", raidLeaderName) + Details:Msg("sent to raid leader a combat end notification.") +end + +--the coach is no more a coach +function Details.Coach.SendRaidCoachEndNotify() + Details:SendCommMessage(_G.DETAILS_PREFIX_NETWORK, Details:Serialize(_G.DETAILS_PREFIX_COACH, UnitName("player"), GetRealmName(), Details.realversion, "CE"), "RAID") + Details:Msg("sent to raid a coach end notification.") +end + +--there's a new coach, notify players +function Details.Coach.SendRaidCoachStartNotify() + Details:SendCommMessage(_G.DETAILS_PREFIX_NETWORK, Details:Serialize(_G.DETAILS_PREFIX_COACH, UnitName("player"), GetRealmName(), Details.realversion, "CS"), "RAID") + Details:Msg("sent to raid a coach start notification.") +end + +--send data to raid leader +function Details.Coach.Client.SendDataToRL() + local data = Details.packFunctions.GetAllData() + if (data and Details.Coach.Client.coachName) then + Details:SendCommMessage(_G.DETAILS_PREFIX_NETWORK, Details:Serialize(_G.DETAILS_PREFIX_COACH, UnitName("player"), GetRealmName(), Details.realversion, "CDT", data), "WHISPER", Details.Coach.Client.coachName) + end +end + +--on details startup +function Details.Coach.StartUp() + + Details.Coach.isInRaidGroup = IsInRaid() + Details.Coach.isInRaidZone = select(2, _G.GetInstanceInfo()) + + --server + if (Details.coach.enabled) then --profile + Details.Coach.Server.EnableCoach(true) + + elseif (not Details.coach.enabled) then --profile + if (IsInRaid()) then + if (isInRaidZone()) then + local raidLeaderName = Details:GetRaidLeader() + if (raidLeaderName) then + --client ask for the raid leader if the Coach is enabled, GetRaidLeader returns nil is the user isn't in raid + Details:Msg("sent ask to raid leader, is coach?") + Details.Coach.AskRLForCoachStatus(raidLeaderName) + end + end + end + end + + local eventListener = Details:CreateEventListener() + Details.Coach.Listener = eventListener + + function eventListener.OnEnterGroup() --client + --when entering a group, check if the player isn't the raid leader + if (not UnitIsGroupLeader("player")) then + if (IsInRaid()) then + if (isInRaidZone()) then + local raidLeaderName = Details:GetRaidLeader() + if (raidLeaderName) then + Details:Msg("sent ask to raid leader, is coach?") + Details.Coach.AskRLForCoachStatus(raidLeaderName) + end + end + end + end + + Details.Coach.isInRaidGroup = true + end + + function eventListener.OnLeaveGroup() + --disable coach feature on server and client if the player leaves the group + Details.Coach.Disable() + Details.Coach.isInRaidGroup = false + end + + function eventListener.OnEnterCombat() + --send a notify to raid leader telling a new combat has started + if (Details.Coach.Client.IsEnabled()) then + if (IsInRaid() and isInRaidZone()) then + if (UnitIsGroupAssistant("player")) then + local raidLeaderName = Details.Coach.Client.GetLeaderName() + if (raidLeaderName) then + Details:Msg("i'm a raid assistant, sent combat start notification to raid leader.") + Details.Coach.SendRLCombatStartNotify(raidLeaderName) + end + end + + --start a timer to send data to the raid leader + if (Details.Coach.Client.UpdateTicker) then + Details.Coach.Client.UpdateTicker:Cancel() + end + Details.Coach.Client.UpdateTicker = Details.Schedules.NewTicker(1.5, Details.Coach.Client.SendDataToRL) + end + end + end + + function eventListener.OnLeaveCombat() + --send a notify to raid leader telling the combat has finished + if (Details.Coach.Client.IsEnabled()) then + if (IsInRaid() and isInRaidZone()) then + if (UnitIsGroupAssistant("player")) then + local raidLeaderName = Details.Coach.Client.GetLeaderName() + if (raidLeaderName) then + Details:Msg("i'm a raid assistant, sent combat end notification to raid leader.") + Details.Coach.SendRLCombatEndNotify(raidLeaderName) + end + end + end + + Details.Schedules.Cancel(Details.Coach.Client.UpdateTicker) + end + end + + function eventListener.OnZoneChanged() + --if the raid leader entered in a raid, disable the coach + if (Details.Coach.Server.IsEnabled()) then + if (isInRaidZone()) then + --the raid leader entered a raid instance + Details.Coach.Disable() + Details:Msg("Coach feature stopped: you entered in a raid instance.") + end + return + else + --check if the raid leader just left the raid to be a coach + if (Details.Coach.IsEnabled()) then --profile coach feature is enabled + if (UnitIsGroupLeader("player")) then --player is the raid leader + if (not Details.Coach.Server.IsEnabled()) then --the coach feature isn't running + Details.Coach.Server.EnableCoach() + Details:Msg("Coach feature is now running, if this come as surprise, use '/details coach' to disable.") + end + end + return + end + end + + --when entering a new zone, check if there's a coach + if (not Details.Coach.isInRaidZone and isInRaidZone()) then + if (not UnitIsGroupLeader("player")) then + if (IsInRaid()) then + if (not Details.Coach.Client.IsEnabled()) then + local raidLeaderName = Details:GetRaidLeader() + if (raidLeaderName) then + Details:Msg("sent ask to raid leader, is coach?") + Details.Coach.AskRLForCoachStatus(raidLeaderName) + return + end + end + end + end + end + + --check if the player has left the raid zone + if (Details.Coach.isInRaidZone and Details.Coach.Client.IsEnabled()) then + if (not isInRaidZone()) then + --player left the raid zone + Details.Schedules.Cancel(Details.Coach.Client.UpdateTicker) + Details.Coach.Disable() + end + end + + Details.Coach.isInRaidZone = isInRaidZone() + end + + eventListener:RegisterEvent("GROUP_ONENTER", "OnEnterGroup") + eventListener:RegisterEvent("GROUP_ONLEAVE", "OnLeaveGroup") + eventListener:RegisterEvent("COMBAT_PLAYER_ENTER", "OnEnterCombat") + eventListener:RegisterEvent("COMBAT_PLAYER_LEAVE", "OnLeaveCombat") + eventListener:RegisterEvent("ZONE_TYPE_CHANGED", "OnZoneChanged") +end + +--received an answer from server telling if the raidleader has the coach feature enabled +--the request is made when the player enters a new group or reconnects +function Details.Coach.Client.CoachIsEnabled_Response(isCoachEnabled, raidLeaderName) + if (isCoachEnabled) then + --raid leader confirmed the coach feature is enabled and running + Details.Coach.Client.EnableCoach(raidLeaderName) + end +end + +function Details.Coach.Disable() + Details.coach.enabled = false --profile + + --if the player is the raid leader and the coach feature is enabled + if (Details.Coach.Server.IsEnabled()) then + Details.Coach.SendRaidCoachEndNotify() + end + + Details.Coach.Server.enabled = false + Details.Coach.Client.enabled = false + Details.Coach.Client.coachName = nil + + Details.Coach.EventFrame:UnregisterEvent("GROUP_ROSTER_UPDATE") +end + +--the player used '/details coach' or it's Details! initialization +function Details.Coach.Server.EnableCoach(fromStartup) + if (not IsInRaid()) then + Details:Msg("cannot enabled coach: not in raid.") + Details.coach.enabled = false + Details.Coach.Server.enabled = false + return + + elseif (not UnitIsGroupLeader("player")) then + Details:Msg("cannot enabled coach: you aren't the raid leader.") + Details.coach.enabled = false + Details.Coach.Server.enabled = false + return + + elseif (isInRaidZone()) then + Details:Msg("cannot enabled coach: you are inside a raid zone.") + Details.coach.enabled = false + Details.Coach.Server.enabled = false + return + end + + Details.coach.enabled = true + Details.Coach.Server.enabled = true + + --notify players about the new coach + Details.Coach.SendRaidCoachStartNotify() + + --enable group roster to know if the server isn't raid leader any more + Details.Coach.EventFrame:RegisterEvent("GROUP_ROSTER_UPDATE") + + if (fromStartup) then + Details:Msg("coach feature enabled, welcome back captain!") + end +end + +--the raid leader sent a coach end notify +function Details.Coach.Client.CoachEnd() + Details.Coach.Client.enabled = false + Details.Coach.Client.coachName = nil + Details.Coach.EventFrame:UnregisterEvent("GROUP_ROSTER_UPDATE") +end + +--a player in the raid asked to be the coach of the group +function Details.Coach.Client.EnableCoach(raidLeaderName) + if (not IsInRaid()) then + return + + elseif (not UnitIsGroupLeader(raidLeaderName)) then + return + end + + Details.Coach.Client.enabled = true + Details.Coach.Client.coachName = raidLeaderName + + --enable group roster to know if the raid leader has changed + Details.Coach.EventFrame:RegisterEvent("GROUP_ROSTER_UPDATE") + + Details:Msg("there's a new coach: ", raidLeaderName) +end + +--raid leader received a notification that a new combat has started +function Details.Coach.Server.CombatStarted() + if (Details.Coach.Server.lastCombatStartTime > GetTime()) then + return + else + Details.Coach.Server.lastCombatStartTime = GetTime() + 10 + end + + --stop the combat if already in one + if (Details.in_combat) then + Details:EndCombat() + end + + --start a new combat + Details:StartCombat() +end + +--raid leader received a notification that the current combat ended +function Details.Coach.Server.CombatEnded() + if (Details.Coach.Server.lastCombatEndTime > GetTime()) then + return + else + Details.Coach.Server.lastCombatEndTime = GetTime() + 10 + end + + Details:EndCombat() +end + +--profile +function Details.Coach.IsEnabled() + return Details.coach.enabled +end +--server +function Details.Coach.Server.IsEnabled() + return Details.Coach.Server.enabled +end +--client +function Details.Coach.Client.IsEnabled() + return Details.Coach.Client.enabled +end +function Details.Coach.Client.GetLeaderName() + return Details.Coach.Client.coachName +end + +Details.Coach.EventFrame = _G.CreateFrame("frame") +Details.Coach.EventFrame:RegisterEvent("GROUP_ROSTER_UPDATE") +Details.Coach.EventFrame:SetScript("OnEvent", function(event, ...) + if (event == "GROUP_ROSTER_UPDATE") then + --check who is raid leader to know if the leader is still the same + if (Details.Coach.Client.IsEnabled()) then + if (IsInRaid()) then + for i = 1, GetNumGroupMembers() do + if (UnitIsGroupLeader("raid" .. i)) then + local unitName = UnitName("raid" .. i) + if (_G.Ambiguate(unitName .. "-" .. GetRealmName(), "none") ~= Details.Coach.Client.coachName) then + --the raid leader has changed, finish the coach feature on the client + Details:Msg("raid leader has changed, coach feature has been disabled.") + Details.Coach.Client.CoachEnd() + end + break + end + end + end + end + + --check if the player is the new raid leader + if (UnitIsGroupLeader("player")) then + if (Details.Coach.IsEnabled()) then + if (not Details.Coach.Server.IsEnabled()) then + if (IsInRaid()) then + if (not isInRaidZone()) then + Details:Msg("you're now the coach of the group.") + --delay to set the new leader to give time for SendRaidCoachEndNotify() + _G.C_Timer.After(3, Details.Coach.Server.EnableCoach) + end + end + end + end + else + --player isn't the raid leader, check if the player is the coach and disable the feature + if (Details.Coach.IsEnabled()) then + if (Details.Coach.Server.IsEnabled()) then + Details:Msg("you're not the raid leader, disabling the coach feature.") + Details.Coach.Disable() + end + end + end + end +end) \ No newline at end of file diff --git a/functions/pack.lua b/functions/pack.lua index e3f13b511..3b012f752 100644 --- a/functions/pack.lua +++ b/functions/pack.lua @@ -1 +1,1025 @@ -local Details = Details + +if (true) then + --return +end + +local Details = _G.Details +local bit = _G.bit +local DETAILS_ATTRIBUTE_DAMAGE = _G.DETAILS_ATTRIBUTE_DAMAGE +local DF = _G.DetailsFramework +local tonumber = _G.tonumber +local select = _G.select +local strsplit = _G.strsplit +local floor = _G.floor +local tremove = _G.tremove +local UnitName = _G.UnitName +local tinsert = _G.tinsert +local IsInRaid = _G.IsInRaid +local GetNumGroupMembers = _G.GetNumGroupMembers +local GetRaidRosterInfo = _G.GetRaidRosterInfo +local unpack = _G.unpack + +Details.packFunctions = {} + +--lookup actor information tables +local actorInformation = {} +local actorInformationIndexes = {} +local actorDamageInfo = {} +local actorHealInfo = {} + +--flags +local REACTION_HOSTILE = 0x00000040 +local IS_GROUP_OBJECT = 0x00000007 +local REACTION_FRIENDLY = 0x00000010 +local OBJECT_TYPE_MASK = 0x0000FC00 +local OBJECT_TYPE_OBJECT = 0x00004000 +local OBJECT_TYPE_PETGUARDIAN = 0x00003000 +local OBJECT_TYPE_GUARDIAN = 0x00002000 +local OBJECT_TYPE_PET = 0x00001000 +local OBJECT_TYPE_NPC = 0x00000800 +local OBJECT_TYPE_PLAYER = 0x00000400 + +local INDEX_EXPORT_FLAG = 1 +local INDEX_COMBAT_START_TIME = 2 +local INDEX_COMBAT_END_TIME = 3 +local INDEX_COMBAT_START_DATE = 4 +local INDEX_COMBAT_END_DATE = 5 +local INDEX_COMBAT_NAME = 6 + +local TOTAL_INDEXES_FOR_COMBAT_INFORMATION = 6 + +local entitySerialCounter = 0 + +function Dexport() --test case + local combat = Details:GetCurrentCombat() + local readyToSendData = Details.packFunctions.PackCombatData(combat, 0x1) + local newCombatWithData = Details.packFunctions.UnPackCombatData(readyToSendData) +end + +function Details.packFunctions.GetAllData() + local combat = Details:GetCurrentCombat() + local packedData = Details.packFunctions.PackCombatData(combat, 0x13) + return packedData +end + +--pack the combat +function Details.packFunctions.PackCombatData(combatObject, flags) + + --0x1 damage + --0x2 healing + --0x4 energy + --0x8 misc + --0x10 no combat header + + table.wipe(actorInformation) + table.wipe(actorInformationIndexes) + table.wipe(actorDamageInfo) + table.wipe(actorHealInfo) + + --reset the serial counter + entitySerialCounter = 0 + + local isBossEncouter = combatObject.is_boss + local startDate, endDate = combatObject:GetDate() + + local startCombatTime = combatObject:GetStartTime() or 0 + local endCombatTime = combatObject:GetEndTime() or 0 + local combatInfo = { + floor(startCombatTime), --1 + floor(endCombatTime), --2 + startDate, --3 + endDate, --4 + isBossEncouter and isBossEncouter.encounter or "Unknown Enemy" --5 + } + + if (bit.band(flags, 0x1) ~= 0) then + Details.packFunctions.PackDamage(combatObject) + end + + if (bit.band(flags, 0x2) ~= 0) then + Details.packFunctions.PackHeal(combatObject) + end + + --> prepare data to send over network + local exportedString = flags .. "," + + --add the combat info + if (bit.band(flags, 0x10) == 0) then + for index, data in ipairs(combatInfo) do + exportedString = exportedString .. data .. "," + end + end + + --add the actor references table + for index, data in ipairs(actorInformation) do + exportedString = exportedString .. data .. "," + end + + --add the damage actors data + if (bit.band(flags, 0x1) ~= 0) then + exportedString = exportedString .. "!D" .. "," + for index, data in ipairs(actorDamageInfo) do + exportedString = exportedString .. data .. "," + end + end + + --add the heal actors data + if (bit.band(flags, 0x2) ~= 0) then + exportedString = exportedString .. "!H" .. "," + for index, data in ipairs(actorHealInfo) do + exportedString = exportedString .. data .. "," + end + end + + --main stuff + --print("finished export (debug):", exportedString) --debug + --print("uncompressed (debug):", format("%.2f", #exportedString/1024), "KBytes") + + --compress + local LibDeflate = _G.LibStub:GetLibrary("LibDeflate") + local dataCompressed = LibDeflate:CompressDeflate(exportedString, {level = 9}) + local dataEncoded = LibDeflate:EncodeForWoWAddonChannel(dataCompressed) + + --print("encoded for WowAddonChannel (debug):", format("%.2f", #dataEncoded/1024), "KBytes") + + return dataEncoded +end + +function Details.packFunctions.GenerateSerialNumber() + local serialNumber = entitySerialCounter + entitySerialCounter = entitySerialCounter + 1 + return serialNumber +end + +--[[ + actor flag IDs + 1: player friendly + 2: player enemy + 3: enemy npc pet + 4: enemy npc non-pet + 5: friendly npc pet + 6: friendly non-npc + 7: unknown entity type +]] + +local packActorFlag = function(actor) + if (actor.grupo) then + --it's a player in the group + return 1 + end + + local flag = actor.flag_original or 0 + + --check hostility + if (bit.band(flag, REACTION_HOSTILE) ~= 0) then + --is hostile + if (bit.band(flag, OBJECT_TYPE_PLAYER) == 0) then + --isn't a player + if (bit.band(flag, OBJECT_TYPE_PETGUARDIAN) ~= 0) then + --is pet + return 3 + else + --is enemy npc + return 4 + end + else + --is enemy player + return 2 + end + else + --is friendly + if (bit.band(flag, OBJECT_TYPE_PLAYER) == 0) then + --is player + return 1 + else + --isn't a player + if (bit.band(flag, OBJECT_TYPE_PETGUARDIAN) ~= 0) then + --is friendly pet + return 5 + else + --is a friendly entity, most likely a npc + return 6 + end + end + end + + return 7 +end + +local unpackActorFlag = function(flag) + --convert to integer + flag = tonumber(flag) + + if (flag == 1) then --player + return 0x511 + + elseif (flag == 2) then --enemy player + return 0x548 + + elseif (flag == 3) then --enemy pet with player or AI controller + return 0x1A48 + + elseif (flag == 4) then --enemy npc + return 0xA48 + + elseif (flag == 5) then --friendly pet + return 0x1914 + + elseif (flag == 6) then --friendly npc + return 0xA14 + + elseif (flag == 7) then --unknown entity + return 0x4A28 + end +end + +local isActorInGroup = function(class, flag) + if (bit.band (flag, IS_GROUP_OBJECT) ~= 0 and class ~= "UNKNOW" and class ~= "UNGROUPPLAYER" and class ~= "PET") then + return true + end + return false +end + +--[[ + actor class IDs + 1-12: player class Id + 20: "PET" + 21: "UNKNOW" + 22: "UNGROUPPLAYER" + 23: "ENEMY" + 24: "MONSTER" +]] + +local detailsClassIndexToFileName = { + [20] = "PET", + [21] = "UNKNOW", + [22] = "UNGROUPPLAYER", + [23] = "ENEMY", + [24] = "MONSTER", +} + +local packActorClass = function(actor) + local classId = DF.ClassFileNameToIndex[actor.classe] + if (classId) then + return classId + elseif (classId == "PET") then + return 20 + elseif (classId == "UNKNOW") then + return 21 + elseif (classId == "UNGROUPPLAYER") then + return 22 + elseif (classId == "ENEMY") then + return 23 + elseif (classId == "MONSTER") then + return 24 + end + + return 21 +end + +local unpackActorClass = function(classId) + --convert to integer + classId = tonumber(classId) + + local classFileName = DF.ClassIndexToFileName[classId] + if (not classFileName) then + classFileName = detailsClassIndexToFileName[classId] + end + + return classFileName +end + +--[[ + actor serial + creature: C12345 (numbers are the npcId) + player: P +--]] + +local packActorSerial = function(actor) + local serial = actor.serial + if (serial:match("^C") == "C") then + local npcId = tonumber(select(6, strsplit("-", serial)) or 0) + return "C" .. npcId + + elseif (serial:match("^P") == "P") then + return "P" + + elseif (serial == "") then + return "C12345" + end +end + +local unpackActorSerial = function(serialNumber) + --player serial + if (serialNumber:match("^P")) then + return "Player-1-" .. Details.packFunctions.GenerateSerialNumber() + + elseif (serialNumber:match("^C")) then + return "Creature-0-0-0-0-" .. serialNumber:gsub("C", "") .."-" .. Details.packFunctions.GenerateSerialNumber() + end +end + +function Details.packFunctions.AddActorInformation(actor) + --the next index to use on the actor info table + local currentIndex = #actorInformation + 1 + + --calculate where this actor will be placed on the combatData table + local indexOnCombatDataTable = TOTAL_INDEXES_FOR_COMBAT_INFORMATION + currentIndex + + --add the actor start information index + actorInformationIndexes[actor.nome] = indexOnCombatDataTable + + --index 1: actor name + actorInformation[currentIndex] = actor.nome or "unnamed" --[1] + + --index 2: actor flag + actorInformation[currentIndex + 1] = packActorFlag(actor) --[2] + + --index 3: actor serial + actorInformation[currentIndex + 2] = packActorSerial(actor) or "" --[3] + + --index 4: actor class + actorInformation[currentIndex + 3] = packActorClass(actor) --[4] + + --index 5: actor spec + actorInformation[currentIndex + 4] = actor.spec or 0 --[5] + + return indexOnCombatDataTable +end + +function Details.packFunctions.RetriveActorInformation(combatData, index) + --name [1] + local actorName = combatData[index] + if (not actorName) then + return + end + + --flag [2] + local actorFlag = combatData[index + 1] + actorFlag = unpackActorFlag(actorFlag) + + --serial [3] + local serialNumber = combatData[index + 2] + serialNumber = unpackActorSerial(serialNumber) + + --class [4] + local class = combatData[index + 3] + class = unpackActorClass(class) + + --spec [5] + local spec = tonumber(combatData[index + 4]) + + --return the values + return actorName, actorFlag, serialNumber, class, spec +end + + + +--pack damage passes the player damage info + pets the player own +--each player will also send an enemy, the enemy will be in order of raidIndex of the player +function Details.packFunctions.PackDamage(combatObject) + + --store actorObjects to pack + local actorsToPack = {} + + --get the player object from the combat > damage container + local playerName = UnitName("player") + local playerOBject = combatObject:GetActor(DETAILS_ATTRIBUTE_DAMAGE, playerName) + tinsert(actorsToPack, playerOBject) + + --get the list of pets the player own + local playerPets = playerOBject.pets + for _, petName in ipairs(playerPets) do + local petObject = combatObject:GetActor(DETAILS_ATTRIBUTE_DAMAGE, petName) + if (petObject) then + tinsert(actorsToPack, petObject) + end + end + + local playerIndex + --check if this player has to send information about an enemy npc + if (IsInRaid()) then + for i = 1, GetNumGroupMembers() do + local unitId = "raid" .. i + local name, rank, subgroup, level, class, fileName, zone, online, isDead, role, isML = GetRaidRosterInfo(i) + if (name == playerName) then + playerIndex = i + break + end + end + end + + if (not playerIndex) then + return + end + + --get all npc enemies and sort them by their respaw id + local allActors = combatObject[DETAILS_ATTRIBUTE_DAMAGE]._ActorTable + local allEnemies = {} --this have subtables, format: {actorObject, spawnId} + + for i = 1, #allActors do + --get the actor object + local actor = allActors[i] + --check if is an enemy or neutral + if (actor:IsNeutralOrEnemy()) then + --get the spawnId + local spawnId = select(7, strsplit( "-", actor.serial)) + if (spawnId) then + spawnId = tonumber(spawnId) + if (spawnId) then + --first index is the actorObject, the second index is the spawnId to sort enemies + tinsert(allEnemies, {actor, spawnId}) + end + end + end + end + + --sort enemies by their spawnId + table.sort(allEnemies, Details.Sort2) + + --this is the enemy that this player has to send + local enemyObjectToSend = allEnemies[playerIndex] + if (enemyObjectToSend) then + tinsert(actorsToPack, enemyObjectToSend) + end + + --add the actors to actor information table + for _, actorObject in ipairs(actorsToPack) do + --check if already has the actor information + local indexToActorInfo = actorInformationIndexes[actorObject.nome] --actor name + if (not indexToActorInfo) then + --need to add the actor general information into the actor information table + indexToActorInfo = Details.packFunctions.AddActorInformation(actorObject) + end + end + + local spellSize = 0 + + for i = 1, #actorsToPack do + --get the actor object + local actor = actorsToPack[i] + local indexToActorInfo = actorInformationIndexes[actor.nome] + + --where the information of this actor starts + local currentIndex = #actorDamageInfo + 1 + + --[1] index where is stored the this actor info like name, class, spec, etc + actorDamageInfo[currentIndex] = indexToActorInfo --[1] + + --[2 - 6] + actorDamageInfo [currentIndex + 1] = floor(actor.total) --[2] + actorDamageInfo [currentIndex + 2] = floor(actor.totalabsorbed) --[3] + actorDamageInfo [currentIndex + 3] = floor(actor.damage_taken) --[4] + actorDamageInfo [currentIndex + 4] = floor(actor.friendlyfire_total) --[5] + actorDamageInfo [currentIndex + 5] = floor(actor.total_without_pet) --[6] + + local spellContainer = actor.spells._ActorTable + + --reserve an index to tell the length of spells + actorDamageInfo [#actorDamageInfo + 1] = 0 + local reservedSpellSizeIndex = #actorDamageInfo + local totalSpellIndexes = 0 + + for spellId, spellInfo in pairs(spellContainer) do + local spellDamage = spellInfo.total + local spellHits = spellInfo.counter + local spellTargets = spellInfo.targets + + actorDamageInfo [#actorDamageInfo + 1] = floor(spellId) + actorDamageInfo [#actorDamageInfo + 1] = floor(spellDamage) + actorDamageInfo [#actorDamageInfo + 1] = floor(spellHits) + + --build targets + local targetsSize = Details.packFunctions.CountTableEntriesValid(spellTargets) * 2 + actorDamageInfo [#actorDamageInfo + 1] = targetsSize + + for actorName, damageDone in pairs(spellTargets) do + local actorInfoIndex = actorInformationIndexes[actorName] + if (actorInfoIndex) then + actorDamageInfo [#actorDamageInfo + 1] = actorInfoIndex + actorDamageInfo [#actorDamageInfo + 1] = floor(damageDone) + spellSize = spellSize + 2 + end + end + + --+3: spellId, damage, spellHits + --+1: the index that tell the size of targets + totalSpellIndexes = totalSpellIndexes + 3 + targetsSize + 1 + spellSize = spellSize + 1 --debug + end + + --amount of indexes spells are using + actorDamageInfo[reservedSpellSizeIndex] = totalSpellIndexes + end +end + +-------------------------------------------------------------------------------------------------------------------------------- +--> unpack + +--@currentCombat: details! combat object +--@combatData: array with strings with combat information +--@tablePosition: first index of the first damage actor +function Details.packFunctions.UnPackDamage(currentCombat, combatData, tablePosition) + + --get the damage container + local damageContainer = currentCombat[DETAILS_ATTRIBUTE_DAMAGE] + + --loop from 1 to 199, the amount of actors store is unknown + --todo: it's only unpacking the first actor from the table, e.g. theres izimode and eye of corruption, after export it only shows the eye of corruption + --table position does not move forward + for i = 1, 199 do + --actor information index in the combatData table + --this index gives the position where the actor name, class, spec are stored + local actorReference = tonumber(combatData[tablePosition]) --[1] + local actorName, actorFlag, serialNumber, class, spec = Details.packFunctions.RetriveActorInformation(combatData, actorReference) + + --check if all damage actors has been processed + --if there's no actor name it means it reached the end + if (not actorName) then + print("damage END index (debug):", i, actorReference, "tablePosition:", tablePosition, "value:", combatData[tablePosition]) + break + end + + --get or create the actor object + local actorObject = damageContainer:GetOrCreateActor(serialNumber, actorName, actorFlag, true) + --set the actor class, spec and group + actorObject.classe = class + actorObject.spec = spec + actorObject.grupo = isActorInGroup(class, actorFlag) + + --> copy back the base damage + actorObject.total = tonumber(combatData[tablePosition+1]) --[2] + actorObject.totalabsorbed = tonumber(combatData[tablePosition+2]) --[3] + actorObject.damage_taken = tonumber(combatData[tablePosition+3]) --[4] + actorObject.friendlyfire_total = tonumber(combatData[tablePosition+4]) --[5] + actorObject.total_without_pet = tonumber(combatData[tablePosition+5]) --[6] + + tablePosition = tablePosition + 6 --increase table position + + --> copy back the actor spells + --amount of indexes used to store spells for this actor + local spellsSize = tonumber(combatData [tablePosition]) --[7] + tablePosition = tablePosition + 1 + + local spellIndex = tablePosition + while(spellIndex < tablePosition + spellsSize) do + local spellId = tonumber(combatData [spellIndex]) --[1] + local spellDamage = tonumber(combatData [spellIndex+1]) --[2] + local spellHits = tonumber(combatData [spellIndex+2]) --[3] + + local targetsSize = combatData [spellIndex+3] --[4] + local targetTable = Details.packFunctions.UnpackTable(combatData, spellIndex+3, true) + local spellObject = actorObject.spells:GetOrCreateSpell(spellId, true) --this one need some translation + spellObject.total = spellDamage + spellObject.counter = spellHits + spellObject.targets = targetTable + + spellIndex = spellIndex + targetsSize + 4 + end + + tablePosition = tablePosition + spellsSize --increase table position + end + + return tablePosition +end + +function Details.packFunctions.PackHeal(combatObject) + + --store actorObjects to pack + local actorsToPack = {} + + --get the player object from the combat > damage container + local playerName = UnitName("player") + local playerOBject = combatObject:GetActor(DETAILS_ATTRIBUTE_HEAL, playerName) + tinsert(actorsToPack, playerOBject) + + --get the list of pets the player own + local playerPets = playerOBject.pets + for _, petName in ipairs(playerPets) do + local petObject = combatObject:GetActor(DETAILS_ATTRIBUTE_HEAL, petName) + if (petObject) then + tinsert(actorsToPack, petObject) + end + end + + local playerIndex + --check if this player has to send information about an enemy npc + if (IsInRaid()) then + for i = 1, GetNumGroupMembers() do + local unitId = "raid" .. i + local name, rank, subgroup, level, class, fileName, zone, online, isDead, role, isML = GetRaidRosterInfo(i) + if (name == playerName) then + playerIndex = i + break + end + end + end + + if (not playerIndex) then + return + end + + --get all npc enemies and sort them by their respaw id + local allActors = combatObject[DETAILS_ATTRIBUTE_HEAL]._ActorTable + local allEnemies = {} --this have subtables, format: {actorObject, spawnId} + + for i = 1, #allActors do + --get the actor object + local actor = allActors[i] + --check if is an enemy or neutral + if (actor:IsNeutralOrEnemy()) then + --get the spawnId + local spawnId = select(7, strsplit( "-", actor.serial)) + if (spawnId) then + spawnId = tonumber(spawnId) + if (spawnId) then + --first index is the actorObject, the second index is the spawnId to sort enemies + tinsert(allEnemies, {actor, spawnId}) + end + end + end + end + + --sort enemies by their spawnId + table.sort(allEnemies, Details.Sort2) + + --this is the enemy that this player has to send + local enemyObjectToSend = allEnemies[playerIndex] + if (enemyObjectToSend) then + tinsert(actorsToPack, enemyObjectToSend) + end + + --add the actors to actor information table + for _, actorObject in ipairs(actorsToPack) do + --check if already has the actor information + local indexToActorInfo = actorInformationIndexes[actorObject.nome] --actor name + if (not indexToActorInfo) then + --need to add the actor general information into the actor information table + indexToActorInfo = Details.packFunctions.AddActorInformation(actorObject) + end + end + + local spellSize = 0 + + for i = 1, #actorsToPack do + --get the actor object + local actor = actorsToPack[i] + local indexToActorInfo = actorInformationIndexes[actor.nome] + + --where the information of this actor starts + local currentIndex = #actorHealInfo + 1 + + --[1] index where is stored the this actor general information + actorHealInfo[currentIndex] = indexToActorInfo --[1] + + --[2 - 6] + actorHealInfo [currentIndex + 1] = floor(actor.total) --[2] + actorHealInfo [currentIndex + 2] = floor(actor.totalabsorb) --[3] + actorHealInfo [currentIndex + 3] = floor(actor.totalover) --[4] + actorHealInfo [currentIndex + 4] = floor(actor.healing_taken) --[5] + actorHealInfo [currentIndex + 5] = floor(actor.totalover_without_pet) --[6] + + local spellContainer = actor.spells._ActorTable + + --reserve an index to tell the length of spells + actorHealInfo [#actorHealInfo + 1] = 0 + local reservedSpellSizeIndex = #actorHealInfo + local totalSpellIndexes = 0 + + for spellId, spellInfo in pairs(spellContainer) do + local spellHealingDone = spellInfo.total + local spellHits = spellInfo.counter + local spellTargets = spellInfo.targets + + actorHealInfo [#actorHealInfo + 1] = floor(spellId) + actorHealInfo [#actorHealInfo + 1] = floor(spellHealingDone) + actorHealInfo [#actorHealInfo + 1] = floor(spellHits) + + --build targets + local targetsSize = Details.packFunctions.CountTableEntriesValid(spellTargets) * 2 + actorHealInfo [#actorHealInfo + 1] = targetsSize + + for actorName, damageDone in pairs(spellTargets) do + local actorInfoIndex = actorInformationIndexes[actorName] + if (actorInfoIndex) then + actorHealInfo [#actorHealInfo + 1] = actorInfoIndex + actorHealInfo [#actorHealInfo + 1] = floor(damageDone) + spellSize = spellSize + 2 + end + end + + --+3: spellId, damage, spellHits + --+1: the index that tell the size of targets + totalSpellIndexes = totalSpellIndexes + 3 + targetsSize + 1 + spellSize = spellSize + 1 --debug + end + + --amount of indexes spells are using + actorDamageInfo[reservedSpellSizeIndex] = totalSpellIndexes + + end +end + +-------------------------------------------------------------------------------------------------------------------------------- +--> unpack + +function Details.packFunctions.UnPackHeal(currentCombat, combatData, tablePosition) + --get the damage container + local healContainer = currentCombat[DETAILS_ATTRIBUTE_HEAL] + + for i = 1, 199 do + --this is the same as damage, all comments for the code are there + local actorInfoIndex = tonumber(combatData[tablePosition]) --[1] + local actorName, actorFlag, serialNumber, class, spec = Details.packFunctions.RetriveActorInformation(combatData, actorInfoIndex) + + --if there's no actor name it means it reached the end + if (not actorName) then + print("Heal loop has been stopped", "index:", i, "tablePosition:", tablePosition, "value:", combatData[tablePosition]) + break + end + + --creata the actor object + local actorObject = healContainer:GetOrCreateActor(serialNumber, actorName, actorFlag, true) + --set the actor class, spec and group + actorObject.classe = class + actorObject.spec = spec + actorObject.grupo = isActorInGroup(class, actorFlag) + + --> copy the base healing + actorObject.total = tonumber(combatData[tablePosition+1]) --[2] + actorObject.totalabsorb = tonumber(combatData[tablePosition+2]) --[3] + actorObject.totalover = tonumber(combatData[tablePosition+3]) --[4] + actorObject.healing_taken = tonumber(combatData[tablePosition+4]) --[5] + actorObject.totalover_without_pet = tonumber(combatData[tablePosition+5]) --[6] + + tablePosition = tablePosition + 6 + + --> copy back the actor spells + --amount of indexes used to store spells for this actor + local spellsSize = tonumber(combatData [tablePosition]) --[7] + tablePosition = tablePosition + 1 + + local spellIndex = tablePosition + while(spellIndex < tablePosition + spellsSize) do + local spellId = tonumber(combatData [spellIndex]) --[1] + local spellDamage = tonumber(combatData [spellIndex+1]) --[2] + local spellHits = tonumber(combatData [spellIndex+2]) --[3] + + local targetsSize = combatData [spellIndex+3] --[4] + local targetTable = Details.packFunctions.UnpackTable(combatData, spellIndex+3, true) + local spellObject = actorObject.spells:GetOrCreateSpell(spellId, true) --this one need some translation + spellObject.total = spellDamage + spellObject.counter = spellHits + spellObject.targets = targetTable + + spellIndex = spellIndex + targetsSize + 4 + end + + tablePosition = tablePosition + spellsSize --increase table position + end + + return tablePosition +end + +--what this function receives? +--@packedCombatData: packed combat, ready to be unpacked +function Details.packFunctions.UnPackCombatData(packedCombatData) + local LibDeflate = _G.LibStub:GetLibrary("LibDeflate") + local dataCompressed = LibDeflate:DecodeForWoWAddonChannel(packedCombatData) + local combatDataString = LibDeflate:DecompressDeflate(dataCompressed) + + --[= + local function count(text, pattern) + return select(2, text:gsub(pattern, "")) + end + --]=] + + local combatData = {} + local amountOfIndexes = count(combatDataString, ",") + 1 + print ("amountOfIndexes (debug):", amountOfIndexes) + + while (amountOfIndexes > 0) do + + local splitPart = {strsplit(",", combatDataString, 4000)} --strsplit(): Stack overflow, max allowed: 4000 + + if (#splitPart == 4000 and amountOfIndexes > 4000) then + + print ("#combatDataString (debug) must be > 4000:", amountOfIndexes) + for i = 1, 3999 do + combatData[#combatData+1] = splitPart[i] + end + + --get get part that couldn't be read this loop + combatDataString = splitPart[4000] + amountOfIndexes = amountOfIndexes - 3999 + + print ("#combatDataString (debug) left over:", amountOfIndexes) + else + for i = 1, #splitPart do + combatData[#combatData+1] = splitPart[i] + end + + amountOfIndexes = amountOfIndexes - #splitPart + end + end + + print("total indexes (debug):", #combatData) + + --if true then return end + + local flags = tonumber(combatData[INDEX_EXPORT_FLAG]) + + local tablePosition = TOTAL_INDEXES_FOR_COMBAT_INFORMATION + 1 --[[ +1 to jump to damage ]] + --tablePosition now have the first index of the actorInfoTable + + --stop the combat if already in one + if (Details.in_combat) then + Details:EndCombat() + end + + --start a new combat + Details:StartCombat() + --get the current combat + local currentCombat = Details:GetCurrentCombat() + + --check if this export has include damage info + if (bit.band(flags, 0x1) ~= 0) then + --find the index where the damage information start + for i = tablePosition, #combatData do + if (combatData[i] == "!D") then + tablePosition = i + 1; + break + end + end + + --unpack damage + tablePosition = Details.packFunctions.UnPackDamage(currentCombat, combatData, tablePosition) + end + + if (bit.band(flags, 0x2) ~= 0) then + --find the index where the heal information start + for i = tablePosition, #combatData do + if (combatData[i] == "!H") then + tablePosition = i + 1; + break + end + end + + --unpack heal + Details.packFunctions.UnPackHeal(currentCombat, combatData, tablePosition) + end + + --all done, end combat + Details:EndCombat() + + --set the start and end of combat time and date + currentCombat:SetStartTime(combatData[INDEX_COMBAT_START_TIME]) + currentCombat:SetEndTime(combatData[INDEX_COMBAT_END_TIME]) + currentCombat:SetDate(combatData[INDEX_COMBAT_START_DATE], combatData[INDEX_COMBAT_END_DATE]) + currentCombat.enemy = combatData[INDEX_COMBAT_NAME] + + --debug: delete the segment just created (debug) + --[[ + local combat2 = _detalhes.tabela_historico.tabelas[2] + if (combat2) then + tremove (_detalhes.tabela_historico.tabelas, 1) + _detalhes.tabela_historico.tabelas[1] = combat2 + _detalhes.tabela_vigente = combat2 + end + --]] +end + +--this function does the same as the function above but does not create a new combat, it just add new information +function Details.packFunctions.DeployUnpackedCombatData(packedCombatData) + + local LibDeflate = _G.LibStub:GetLibrary("LibDeflate") + local dataCompressed = LibDeflate:DecodeForWoWAddonChannel(packedCombatData) + local combatDataString = LibDeflate:DecompressDeflate(dataCompressed) + + local function count(text, pattern) + return select(2, text:gsub(pattern, "")) + end + + local combatData = {} + local amountOfIndexes = count(combatDataString, ",") + 1 + + while (amountOfIndexes > 0) do + local splitPart = {strsplit(",", combatDataString, 4000)} + if (#splitPart == 4000 and amountOfIndexes > 4000) then + for i = 1, 3999 do + combatData[#combatData+1] = splitPart[i] + end + --get get part that couldn't be read this loop + combatDataString = splitPart[4000] + amountOfIndexes = amountOfIndexes - 3999 + + else + for i = 1, #splitPart do + combatData[#combatData+1] = splitPart[i] + end + amountOfIndexes = amountOfIndexes - #splitPart + end + end + + local flags = tonumber(combatData[INDEX_EXPORT_FLAG]) + local tablePosition + + if (bit.band(flags, 0x10) ~= 0) then + tablePosition = 2 + else + tablePosition = TOTAL_INDEXES_FOR_COMBAT_INFORMATION + 1 + end + + --get the current combat + local currentCombat = Details:GetCurrentCombat() + + --check if this export has included damage info + if (bit.band(flags, 0x1) ~= 0) then + --find the index where the damage information start + for i = tablePosition, #combatData do + if (combatData[i] == "!D") then + tablePosition = i + 1 + break + end + end + --unpack damage + tablePosition = Details.packFunctions.UnPackDamage(currentCombat, combatData, tablePosition) + end + + if (bit.band(flags, 0x2) ~= 0) then + --find the index where the heal information start + for i = tablePosition, #combatData do + if (combatData[i] == "!H") then + tablePosition = i + 1 + break + end + end + --unpack heal + Details.packFunctions.UnPackHeal(currentCombat, combatData, tablePosition) + end + + if (bit.band(flags, 0x10) == 0) then + --set the start and end of combat time and date + currentCombat:SetStartTime(combatData[INDEX_COMBAT_START_TIME]) + currentCombat:SetEndTime(combatData[INDEX_COMBAT_END_TIME]) + currentCombat:SetDate(combatData[INDEX_COMBAT_START_DATE], combatData[INDEX_COMBAT_END_DATE]) + currentCombat.enemy = combatData[INDEX_COMBAT_NAME] + end +end + +--get the amount of entries of a hash table +function Details.packFunctions.CountTableEntries(hasTable) + local amount = 0 + for _ in pairs(hasTable) do + amount = amount + 1 + end + return amount +end + +--get the amount of entries and check for validation +function Details.packFunctions.CountTableEntriesValid(hasTable) + local amount = 0 + for actorName, _ in pairs(hasTable) do + if (actorInformationIndexes[actorName]) then + amount = amount + 1 + end + end + return amount +end + +--stract some indexes of a table +local selectIndexes = function(table, startIndex, amountIndexes) + local values = {} + for i = startIndex, amountIndexes do + values[#values+1] = tonumber(table[i]) or 0 + end + return unpack(values) +end + +function Details.packFunctions.UnpackTable(table, index, isPair, valueAsTable, amountOfValues) + local result = {} + local reservedIndexes = table[index] + local indexStart = index+1 + local indexEnd = reservedIndexes+index + + if (isPair) then + amountOfValues = amountOfValues or 2 + for i = indexStart, indexEnd, amountOfValues do + if (valueAsTable) then + local key = tonumber(table[i]) + result[key] = {selectIndexes(table, i+1, amountOfValues-1)} + else + local key = tonumber(table[i]) + local value = tonumber(table[i+1]) + result[key] = value + end + end + else + for i = indexStart, indexEnd do + local value = tonumber(table[i]) + result[#result+1] = value + end + end + + return result +end diff --git a/functions/profiles.lua b/functions/profiles.lua index 041be5131..88c93b9de 100644 --- a/functions/profiles.lua +++ b/functions/profiles.lua @@ -1100,6 +1100,9 @@ _detalhes.default_profile = default_profile -- aqui fica as propriedades do jogador que n�o ser�o armazenadas no profile local default_player_data = { + coach = { + enabled = false, + }, --> force all fonts to have this outline force_font_outline = "", diff --git a/functions/slash.lua b/functions/slash.lua index 7ba2e3837..91136ec0d 100644 --- a/functions/slash.lua +++ b/functions/slash.lua @@ -1585,6 +1585,26 @@ Damage Update Status: @INSTANCEDAMAGESTATUS Details:Dump(exportedValues) end + elseif (msg == "coach") then + if (not UnitIsGroupLeader("player")) then + Details:Msg("you aren't the raid leader.") + return + end + + Details.coach.enabled = not Details.coach.enabled + + if (Details.coach.enabled) then + Details:Msg("coach enabled, good luck!") + Details:Msg("[raid leader] stay outside the raid.") + Details:Msg("[assistants] at least one player inside the raid need to have assistant.") + Details:Msg("[players] have an updated version of Details!.") + Details.Coach.Server.EnableCoach() + + else + Details:Msg("coach disabled.") + Details.DisableCoach() + end + elseif (msg == "9") then print ("skin:", Details.skin) print ("current profile:", _detalhes:GetCurrentProfileName()) diff --git a/startup.lua b/startup.lua index 1ff10cc6f..9eb4d837c 100644 --- a/startup.lua +++ b/startup.lua @@ -458,6 +458,9 @@ function Details:StartMeUp() --I'll never stop! C_Timer.After(2, reset_player_detail_window) end + --coach feature startup + Details.Coach.StartUp() + --enforce to show 6 abilities on the tooltip --_detalhes.tooltip.tooltip_max_abilities = 6 freeeeeedooommmmm