diff --git a/OverwriteFTC.lua b/OverwriteFTC.lua deleted file mode 100644 index e75e1c4..0000000 --- a/OverwriteFTC.lua +++ /dev/null @@ -1,181 +0,0 @@ - --- grab ftc from global -FTC = _G['FTC'] - ---[[ - * Handles Combat Events - * -------------------------------- - * Called by EVENT_COMBAT_EVENT - * -------------------------------- - * - * Update for PanicModeCombatAnalyzer: Only added abilityId to function call - * - ]]-- -function FTC.OnCombatEvent( eventCode , result , isError , abilityName , abilityGraphic , abilityActionSlotType , sourceName , sourceType , targetName , targetType , hitValue , powerType , damageType , log , sourceUnitId , targetUnitId , abilityId ) - - -- Ignore errors - if ( isError ) then return end - - -- Pass damage event to handler - FTC.Damage:New( result , abilityName , abilityGraphic , abilityActionSlotType , sourceName , sourceType , targetName , targetType , hitValue , powerType , damageType, abilityId ) -end - - - ---[[ - * Validate and Process New Damages - * -------------------------------- - * Called by FTC:OnCombatEvent() - * -------------------------------- - * - * Update for PanicModeCombatAnalyzer: Only added abilityId to damage object - * - ]]-- -function FTC.Damage:New( result , abilityName , abilityGraphic , abilityActionSlotType , sourceName , sourceType , targetName , targetType , hitValue , powerType , damageType, abilityId ) - - -- Determine context - local target = zo_strformat("<<!aC:1>>",targetName) - local player = zo_strformat("<<!aC:1>>",FTC.Player.name) - local damageOut = false - if ( sourceType == COMBAT_UNIT_TYPE_PLAYER or sourceType == COMBAT_UNIT_TYPE_PLAYER_PET ) then damageOut = true - elseif ( target == player ) then damageOut = false - else return end - - -- Debugging - --d( result .. " || " .. sourceType .. " || " .. sourceName .. " || " .. targetName .. " || " .. abilityName .. " || " .. hitValue ) - - -- Reflag self-targetted as incoming - if ( damageOut and ( target == player ) ) then damageOut = false end - - -- Ignore certain results - if ( FTC.Damage:Filter( result , abilityName ) ) then return end - - -- Compute some flags - local isCrit = result == ACTION_RESULT_CRITICAL_DAMAGE or result == ACTION_RESULT_CRITICAL_HEAL or result == ACTION_RESULT_DOT_TICK_CRITICAL or result == ACTION_RESULT_HOT_TICK_CRITICAL - local isHeal = result == ACTION_RESULT_HEAL or result == ACTION_RESULT_CRITICAL_HEAL or result == ACTION_RESULT_HOT_TICK or result == ACTION_RESULT_HOT_TICK_CRITICAL - - -- Get the icon - local icon = FTC.UI.Textures[abilityName] or '/esoui/art/icons/death_recap_ranged_basic.dds' - if ( abilityName == "" and ( not damageOut ) and isHeal ) then icon = '/esoui/art/icons/ability_healer_017.dds' - elseif ( abilityName == "" and not damageOut ) then icon = '/esoui/art/icons/death_recap_ranged_heavy.dds' end - - -- Setup the damage object - local damage = { - ["out"] = damageOut, - ["result"] = result, - ["target"] = targetName, - ["source"] = sourceName, - ["ability"] = abilityName, - ["type"] = damageType, - ["value"] = hitValue, - ["power"] = powerType, - ["ms"] = GetGameTimeMilliseconds(), - ["crit"] = isCrit, - ["heal"] = isHeal, - ["icon"] = icon, - ["mult"] = 1, - ["weapon"] = FTC.Damage:IsWeaponAttack(abilityName), - -- added by PanicModeCombatAnalyzer for better usage - ["abilityId"] = abilityId - } - - -- ACTION_RESULT_IMMUNE - -- ACTION_RESULT_BLOCKED - -- ACTION_RESULT_POWER_DRAIN - -- ACTION_RESULT_POWER_ENERGIZE - - -- Damage Dealt - if ( hitValue > 0 and ( result == ACTION_RESULT_DAMAGE or result == ACTION_RESULT_CRITICAL_DAMAGE or result == ACTION_RESULT_BLOCKED_DAMAGE or result == ACTION_RESULT_DOT_TICK or result == ACTION_RESULT_DOT_TICK_CRITICAL ) ) then - - -- Flag timestamps - if ( damageOut ) then FTC.Damage.lastOut = GetGameTimeMilliseconds() - else FTC.Damage.lastIn = GetGameTimeMilliseconds() end - - -- Log and SCT - if ( FTC.init.Log ) then FTC.Log:CombatEvent(damage) end - if ( FTC.init.SCT ) then FTC.SCT:Damage(damage) end - - -- Statistics - if ( FTC.init.Stats and damageOut ) then FTC.Stats:RegisterDamage(damage) end - - -- Trigger Ulti Buff - if ( FTC.init.Hotbar ) then FTC.Hotbar:UltimateBuff(damage) end - - -- Falling damage - elseif ( result == ACTION_RESULT_FALL_DAMAGE ) then - damage.ability = GetString(FTC_Falling) - damage.icon = '/esoui/art/icons/death_recap_fall_damage.dds' - - -- Log and SCT - if ( FTC.init.Log ) then FTC.Log:CombatEvent(damage) end - if ( FTC.init.SCT ) then FTC.SCT:Damage(damage) end - - -- Shielded Damage - elseif ( result == ACTION_RESULT_DAMAGE_SHIELDED ) then - - -- Log and SCT - if ( FTC.init.Log ) then FTC.Log:CombatEvent(damage) end - if ( FTC.init.SCT ) then FTC.SCT:Damage(damage) end - - -- Statistics - if ( FTC.init.Stats and damageOut ) then FTC.Stats:RegisterDamage(damage) end - - -- Misses and Dodges - elseif ( result == ACTION_RESULT_DODGED or result == ACTION_RESULT_MISS ) then - - -- Log and SCT - if ( FTC.init.Log ) then FTC.Log:CombatEvent(damage) end - if ( FTC.init.SCT ) then FTC.SCT:Damage(damage) end - - -- Crowd Controls - elseif ( result == ACTION_RESULT_INTERRUPT or result == ACTION_RESULT_STUNNED or result == ACTION_RESULT_OFFBALANCE or result == ACTION_RESULT_DISORIENTED or result == ACTION_RESULT_STAGGERED or result == ACTION_RESULT_FEARED or result == ACTION_RESULT_SILENCED or result == ACTION_RESULT_ROOTED ) then - - -- Trigger Break Free buff - if ( FTC.init.Buffs and damage.ability == GetAbilityName(16565) ) then - local ability = { - ["owner"] = FTC.Player.name, - ["id"] = 16565, - ["name"] = GetString(FTC_BreakFree), - ["dur"] = 8000, - ["icon"] = FTC.UI.Textures[GetAbilityName(16565)], - ["ground"] = false, - ["area"] = false, - ["debuff"] = false, - ["toggle"] = nil, - } - FTC.Buffs:NewEffect( ability , "Player" ) - end - - -- Fire SCT Alert - if ( FTC.init.SCT ) then FTC.SCT:NewCC( result , abilityName , damageOut ) end - - -- Healing Dealt - elseif ( hitValue > 0 and ( result == ACTION_RESULT_HEAL or result == ACTION_RESULT_CRITICAL_HEAL or result == ACTION_RESULT_HOT_TICK or result == ACTION_RESULT_HOT_TICK_CRITICAL ) ) then - - -- Log and SCT - if ( FTC.init.Log ) then FTC.Log:CombatEvent(damage) end - if ( FTC.init.SCT ) then FTC.SCT:Damage(damage) end - - -- Statistics - if ( FTC.init.Stats and sourceType == COMBAT_UNIT_TYPE_PLAYER ) then FTC.Stats:RegisterDamage(damage) end - - -- Target Death - elseif ( result == ACTION_RESULD_DIED or result == ACTION_RESULT_DIED_XP ) then - - -- Wipe Buffs - if ( FTC.init.Buffs ) then FTC.Buffs:WipeBuffs(targetName) end - - -- Log and SCT - if ( FTC.init.Log ) then FTC.Log:CombatEvent(damage) end - - -- DEBUG NEW EVENTS - elseif ( hitValue > 0 ) then - - -- Prompt other unrecognized - --local direction = damageIn and "Incoming" or "Outgoing" - -- FTC.Log:Print( direction .. " result " .. result .. " not recognized! Target: " .. targetName .. " Value: " .. hitValue , {1,1,0} ) - end - - -- Fire a callback for extensions to use - CALLBACK_MANAGER:FireCallbacks( "FTC_NewDamage" , damage ) -end \ No newline at end of file diff --git a/PanicModeCombatAnalyzer.txt b/PanicModeCombatAnalyzer.txt index dba2f5c..c83f3d9 100644 --- a/PanicModeCombatAnalyzer.txt +++ b/PanicModeCombatAnalyzer.txt @@ -1,10 +1,10 @@ ## Title: |cEFEBBEPanic Mode Combat Analyzer|r -## Description: Saves your dps during fight, based on FTC DPS Meter. +## Description: Saves your dps during fight. ## Author: |c009ad6silentgecko|r, deevilius -## Version: 1.1.1 +## Version: 1.2.0 ## APIVersion: 100014 ## SavedVariables: PMCAVars -## DependsOn: FoundryTacticalCombat + lib/LibStub/LibStub.lua lib/LibMapPing/LibMapPing.lua @@ -13,5 +13,5 @@ lib/LibGroupSocket/LibGroupSocket.lua lib/LibGroupSocket/handlers/SyncTimeStampHandler.lua ext/base64.lua -OverwriteFTC.lua -PmCa.lua \ No newline at end of file +PmCa.lua +damage.lua \ No newline at end of file diff --git a/PmCa.lua b/PmCa.lua index 4698276..cbbe9e5 100644 --- a/PmCa.lua +++ b/PmCa.lua @@ -2,8 +2,8 @@ PmCa = PmCa or {} base64 = base64 or {} -PmCa.name = 'Panic Moce Combat Analyzer' -PmCa.version = '1.1.1' +PmCa.name = 'PanicModeCombatAnalyzer' +PmCa.version = '1.2.0' PmCa.versionDB = 4 PmCa.loaded = false PmCa.author = 'silentgecko, deevilius' @@ -45,6 +45,8 @@ function PmCa.Initialize(_, addonName) self.cleanUp() + self.Damage:Initialize() + -- death/alive/rezz EVENT_MANAGER:RegisterForEvent(self.name, EVENT_PLAYER_DEAD, self.onDeath) EVENT_MANAGER:RegisterForEvent(self.name, EVENT_PLAYER_ALIVE, self.onAlive) @@ -53,8 +55,16 @@ function PmCa.Initialize(_, addonName) --combat state EVENT_MANAGER:RegisterForEvent(self.name, EVENT_PLAYER_COMBAT_STATE, self.OnPlayerCombatState) - CALLBACK_MANAGER:RegisterCallback("FTC_NewDamage", self.addDamage) + -- damage event + EVENT_MANAGER:RegisterForEvent(self.name, EVENT_COMBAT_EVENT, self.OnCombatEvent) +end + +function PmCa.OnCombatEvent(_, result, isError, abilityName, _, _, _, sourceType, targetName, _, hitValue, _, _, _, _, _, abilityId) + -- Ignore errors + if (isError) then return end + -- Pass damage event to handler + PmCa.Damage:New(result, abilityName, sourceType, targetName, hitValue, abilityId) end function PmCa:getOffset(offset, finished) @@ -100,66 +110,6 @@ function PmCa.getTrial() return currentRaid end --- get dmg from FTC -function PmCa.addDamage(damage) - local self = PmCa - - --only log outgoing stuff and greater than zero - if damage['out'] and damage['value'] > 0 and damage['target'] ~= '' then - local data = self.savedVariables.data or {} - local lastSave = self.tempVars.lastSave - local lastSaveTS = self.tempVars.lastSaveTimeStamp - local currentTime = GetGameTimeMilliseconds() - local timeStamp = GetTimeStamp() + self.tempVars.timeStampOffsetToLead - local lastSaveDiff = currentTime - lastSave - - -- if the last saving data is 1 sek ago, make a new table - if lastSaveDiff >= 1000 then - lastSaveTS = timeStamp - self.tempVars.lastSaveTimeStamp = timeStamp - self.tempVars.lastSave = currentTime - end - data[lastSaveTS] = data[lastSaveTS] or {} - data[lastSaveTS]['damage'] = data[lastSaveTS]['damage'] or {} - data[lastSaveTS]['healing'] = data[lastSaveTS]['healing'] or {} - - local damageData = { - abilityId = damage['abilityId'], - value = damage['value'], - crit = damage['crit'], - ms = damage['ms'], - } - - if damage['heal'] then - --filter unwanted stuff - if damage['result'] == ACTION_RESULT_HEAL or damage['result'] == ACTION_RESULT_CRITICAL_HEAL or damage['result'] == ACTION_RESULT_HOT_TICK or damage['result'] == ACTION_RESULT_HOT_TICK_CRITICAL then - local currentData = data[lastSaveTS]['healing'][damage['target']] or {} - table.insert(currentData, damageData) - if #currentData ~= 0 then - data[lastSaveTS]['healing'][damage['target']] = currentData - end - end - else - --filter unwanted stuff - if damage['result'] == ACTION_RESULT_DAMAGE or damage['result'] == ACTION_RESULT_CRITICAL_DAMAGE or damage['result'] == ACTION_RESULT_DOT_TICK or damage['result'] == ACTION_RESULT_DOT_TICK_CRITICAL then - local currentData = data[lastSaveTS]['damage'][damage['target']] or {} - table.insert(currentData, damageData) - if #currentData ~= 0 then - data[lastSaveTS]['damage'][damage['target']] = currentData - end - end - end - - -- only store, when we have damage or healing - if #data[lastSaveTS]['damage'] ~= 0 or #data[lastSaveTS]['healing'] ~= 0 then - data[lastSaveTS]['trial'] = self.getTrial() - data[lastSaveTS]['group'] = self.getGroupEnc() - - self.savedVariables.data[lastSaveTS] = data[lastSaveTS] - end - end -end - -- player death / revive function PmCa.onDeath(event) local self = PmCa @@ -210,8 +160,12 @@ function PmCa.onRezz(event, requesterCharacterName, timeLeftToAccept) table.insert(data[timeStamp]['event'], {rezz = zo_strformat("<<!aC:1>>", requesterCharacterName)}) -- add group and trial to event - data[timeStamp]['trial'] = self.getTrial() - data[timeStamp]['group'] = self.getGroupEnc() + if self.getTrial() > 0 then + data[timeStamp]['trial'] = self.getTrial() + end + if GetGroupSize() > 0 then + data[timeStamp]['group'] = self.getGroupEnc() + end self.savedVariables.data[timeStamp] = data[timeStamp] end diff --git a/damage.lua b/damage.lua new file mode 100644 index 0000000..603c3c0 --- /dev/null +++ b/damage.lua @@ -0,0 +1,149 @@ + +PmCa = PmCa or {} +PmCa.Damage = {} + +--Initialize Damage Management +function PmCa.Damage:Initialize() + + -- Set up initial timestamps + PmCa.Damage.lastIn = 0 + PmCa.Damage.lastOut = 0 +end + +--Validate and Process New Damages +function PmCa.Damage:New(result, abilityName, sourceType, targetName, hitValue, abilityId) + local self = PmCa + + -- if we have no target, then return + local target = zo_strformat("<<!aC:1>>", targetName) + if target == '' then return end + + local player = zo_strformat("<<!aC:1>>", GetUnitName('player')) + local damageOut = false + if (sourceType == COMBAT_UNIT_TYPE_PLAYER or sourceType == COMBAT_UNIT_TYPE_PLAYER_PET) then + damageOut = true + elseif (target == player) then + damageOut = false + else + return + end + + -- Reflag self-targetted as incoming and return + if (damageOut and (target == player)) then return end + + -- Ignore certain results + if (self.Damage:Filter(result, abilityName)) then return end + + -- we only want outgoing stuff and values > 0 + if damageOut == false or hitValue <= 0 then return end + + -- Compute some flags + local isCrit = result == ACTION_RESULT_CRITICAL_DAMAGE or result == ACTION_RESULT_CRITICAL_HEAL or result == ACTION_RESULT_DOT_TICK_CRITICAL or result == ACTION_RESULT_HOT_TICK_CRITICAL + local isHeal = result == ACTION_RESULT_HEAL or result == ACTION_RESULT_CRITICAL_HEAL or result == ACTION_RESULT_HOT_TICK or result == ACTION_RESULT_HOT_TICK_CRITICAL + local isDamage = result == ACTION_RESULT_DAMAGE or result == ACTION_RESULT_CRITICAL_DAMAGE or result == ACTION_RESULT_DOT_TICK or result == ACTION_RESULT_DOT_TICK_CRITICAL + + --only log outgoing stuff and greater than zero + + local data = self.savedVariables.data or {} + local lastSave = self.tempVars.lastSave + local lastSaveTS = self.tempVars.lastSaveTimeStamp + local currentTime = GetGameTimeMilliseconds() + local timeStamp = GetTimeStamp() + self.tempVars.timeStampOffsetToLead + local lastSaveDiff = currentTime - lastSave + + -- if the last saving data is 1 sek ago, make a new table + if lastSaveDiff >= 1000 then + lastSaveTS = timeStamp + self.tempVars.lastSaveTimeStamp = timeStamp + self.tempVars.lastSave = currentTime + end + + data[lastSaveTS] = data[lastSaveTS] or {} + + local savingData = { + abilityId = abilityId, + value = hitValue, + crit = isCrit, +-- ms = GetGameTimeMilliseconds(), + } + + local arrayString = 'damage' + if isHeal then + arrayString = 'healing' + end + data[lastSaveTS][arrayString] = data[lastSaveTS][arrayString] or {} + data[lastSaveTS][arrayString][target] = data[lastSaveTS][arrayString][target] or {} + table.insert(data[lastSaveTS][arrayString][target], savingData) + + -- only store, when we have damage or healing + if #data[lastSaveTS][arrayString][target] > 1 then + if self.getTrial() > 0 then + data[lastSaveTS]['trial'] = self.getTrial() + end + if GetGroupSize() > 0 then + data[lastSaveTS]['group'] = self.getGroupEnc() + end + + self.savedVariables.data[lastSaveTS] = data[lastSaveTS] + end +end + + +--[[---------------------------------------------------------- + HELPER FUNCTIONS + ]] ----------------------------------------------------------- + +--[[ + * Filter Out Unwanted Combat Events + * -------------------------------- + * Called by PmCa.Damage:New() + * -------------------------------- + ]] -- +function PmCa.Damage:Filter(result, abilityName) + + -- Keep a list of ignored actions + local results = { + ACTION_RESULT_QUEUED, + } + + -- Check actions + for i = 1, #results do + if (result == results[i]) then return true end + end + + -- Keep a list of ignored abilities + local abilities = { + 31221, -- Skyshard Collect + 36010, -- Mount Up + 41467, -- Regeneration Dummy + 57466, -- Rapid Regeneration Dummy + 57468, -- Mutagen Dummy + } + for i = 1, #abilities do + if (abilityName == GetAbilityName(abilities[i])) then return true end + end +end + +--[[ + * Track Whether a Damage Source is a Weapon ATtack + * -------------------------------- + * Called by PmCa.Damage:New() + * -------------------------------- + ]] -- +function PmCa.Damage:IsWeaponAttack(abilityName) + + local attacks = { + 4858, -- Bash + 7880, -- Light Attack + 7095, -- Heavy Attack + 16420, -- Heavy Attack (Dual Wield) + 16691, -- Heavy Attack (Bow) + 32480, -- Heavy Attack Werewolf + } + + -- Compare each ability with the damage name + for i = 1, #attacks do + local name = GetAbilityName(attacks[i]) + if (abilityName == name) then return true end + end +end diff --git a/lib/LibGroupSocket/LibGroupSocket.lua b/lib/LibGroupSocket/LibGroupSocket.lua index ae20e7c..4d99903 100644 --- a/lib/LibGroupSocket/LibGroupSocket.lua +++ b/lib/LibGroupSocket/LibGroupSocket.lua @@ -338,5 +338,5 @@ function lib:GetHandler(handlerType) end lib.MESSAGE_TYPE_RESOURCES = 1 -lib.MESSAGE_TYPE_FTC_DPS = 2 +lib.MESSAGE_TYPE_DPS = 2 lib.MESSAGE_TYPE_SYNC_TIMESTAMP = 3 diff --git a/lib/LibGroupSocket/handlers/DpsHandler.lua b/lib/LibGroupSocket/handlers/DpsHandler.lua new file mode 100644 index 0000000..b48b764 --- /dev/null +++ b/lib/LibGroupSocket/handlers/DpsHandler.lua @@ -0,0 +1,106 @@ +local LGS = LibStub("LibGroupSocket") +local type, version = LGS.MESSAGE_TYPE_DPS, 1 +local handler = LGS:RegisterHandler(type, version) -- TODO return saveData +if(not handler) then return end +local SKIP_CREATE = true +local Log = LGS.Log +local ON_RECEIVE_DPS = "OnReceiveDps" + +local lastSendTime = 0 + +local function OnData(unitTag, data, isSelf) + local index = 1 + local dps, index = LGS:ReadUint16(data, index) + local randomOne, index = LGS:ReadUint8(data, index) + local time, index = LGS:ReadUint16(data, index) + local randomTwo, index = LGS:ReadUint16(data, index) +-- Log("OnData %s (%d byte): dps: %s, time: %s, randomOne: %s, randomTwo: %s", GetUnitName(unitTag), #data, tostring(dps), tostring(time), tostring(randomOne), tostring(randomTwo)) + + local expectedLength = 7 + if(#data < expectedLength) then Log("DpsHandler received only %d of %d byte", #data, expectedLength) return end + + LGS.cm:FireCallbacks(ON_RECEIVE_DPS, GetUnitName(unitTag), dps, time, isSelf) +end + +local saveData = { + enabled = true, +} -- TODO + +local MIN_SEND_TIMEOUT = 2 +local MIN_COMBAT_SEND_TIMEOUT = 1 +function handler:Send(dps, time) + if(not saveData.enabled or not IsUnitGrouped("player")) then return end + local now = GetTimeStamp() + local timeout = IsUnitInCombat("player") and MIN_COMBAT_SEND_TIMEOUT or MIN_SEND_TIMEOUT + if(now - lastSendTime < timeout) then return end + --only ping when in combat + if not IsUnitInCombat("player") then return end + + if dps == nil or time == nil then return end + + local data = {} + local index = 1 + index = LGS:WriteUint16(data, index, dps) + index = LGS:WriteUint8(data, index, 123) + index = LGS:WriteUint16(data, index, time) + index = LGS:WriteUint16(data, index, 54321) + index = index + 1 + + lastSendTime = now +-- Log("Send %d byte: dps: %d / time: %d", #data, AdvancedDpsSharing.tempVars.dps, AdvancedDpsSharing.tempVars.time) + LGS:Send(type, data) +end + +function handler:RegisterForReceiveDps(callback) + LGS.cm:RegisterCallback(ON_RECEIVE_DPS, callback) +end + +function handler:UnregisterForReceiveDps(callback) + LGS.cm:UnregisterCallback(ON_RECEIVE_DPS, callback) +end + +local function OnUpdate() + handler:Send() +end + +local isActive = false + +local function StartSending() + if(not isActive and saveData.enabled and IsUnitGrouped("player")) then + EVENT_MANAGER:RegisterForUpdate("LibGroupSocketDpsHandler", 1000, OnUpdate) + isActive = true + end +end + +local function StopSending() + if(isActive) then + EVENT_MANAGER:UnregisterForUpdate("LibGroupSocketDpsHandler") + isActive = false + end +end + +local function OnCombat(inCombat) + if inCombat and IsUnitGrouped("player") then + StartSending() + else + StopSending() + end +end + +local function Unload() + LGS.cm:UnregisterCallback(type, handler.dataHandler) +-- EVENT_MANAGER:UnregisterForEvent("LibGroupSocketDpsHandler", EVENT_PLAYER_COMBAT_STATE) + StopSending() +end + +local function Load() + handler.dataHandler = OnData + LGS.cm:RegisterCallback(type, OnData) +-- EVENT_MANAGER:RegisterForEvent("LibGroupSocketDpsHandler", EVENT_PLAYER_COMBAT_STATE, OnCombat) + handler.Unload = Unload + + StartSending() +end + +if(handler.Unload) then handler.Unload() end +Load() \ No newline at end of file diff --git a/lib/LibGroupSocket/handlers/ResourceHandler.lua b/lib/LibGroupSocket/handlers/ResourceHandler.lua new file mode 100644 index 0000000..6ca3686 --- /dev/null +++ b/lib/LibGroupSocket/handlers/ResourceHandler.lua @@ -0,0 +1,215 @@ +-- The Group Resource Protocol +-- *bitArray* flags, *uint8* magicka percentage, *uint8* stamina percentage[, *uint16* magicka maximum, *uint16* stamina maximum] +-- flags: +-- 1: isFullUpdate - the user is sending max values in addition to percentages in this packet +-- 2: requestsFullUpdate - the user does not have all the necessary data and wants to have a full update from everyone (e.g. after reloading the ui) +-- 3: sharesPercentagesOnly - the user does not want to share maximum values + +local LGS = LibStub("LibGroupSocket") +local type, version = LGS.MESSAGE_TYPE_RESOURCES, 1 +local handler = LGS:RegisterHandler(type, version) -- TODO return saveData +if(not handler) then return end +local SKIP_CREATE = true +local ON_RESOURCES_CHANGED = "OnResourcesChanged" +local Log = LGS.Log + +handler.resources = {} +local resources = handler.resources +local sendFullUpdate = true +local needFullUpdate = true +local lastSendTime = 0 + +local function GetCachedUnitResources(unitTag, skipCreate) + local unitName = GetUnitName(unitTag) + local unitResources = resources[unitName] + if(not unitResources and not skipCreate) then + resources[unitName] = { + [POWERTYPE_MAGICKA] = { current = 1000, maximum = 1000, percent = 255 }, + [POWERTYPE_STAMINA] = { current = 1000, maximum = 1000, percent = 255 }, + percentageOnly = true, + hasFullData = false, + lastUpdate = 0, + } + unitResources = resources[unitName] + end + return unitResources +end + +function handler:GetLastUpdateTime(unitTag) + local unitResources = GetCachedUnitResources(unitTag, SKIP_CREATE) + if(unitResources) then return unitResources.lastUpdate end + return -1 +end + +local function OnData(unitTag, data, isSelf) + local index, bitIndex = 1, 1 + local isFullUpdate, index, bitIndex = LGS:ReadBit(data, index, bitIndex) + local requestsFullUpdate, index, bitIndex = LGS:ReadBit(data, index, bitIndex) + local sharesPercentagesOnly, index, bitIndex = LGS:ReadBit(data, index, bitIndex) +-- Log("OnData %s (%d byte): is full: %s, needs full: %s, percent only: %s", GetUnitName(unitTag), #data, tostring(isFullUpdate), tostring(requestsFullUpdate), tostring(sharesPercentagesOnly)) + index = index + 1 + if(not isSelf and requestsFullUpdate) then + sendFullUpdate = true + end + + local expectedLength = isFullUpdate and 7 or 3 + if(#data < expectedLength) then Log("ResourceHandler received only %d of %d byte", #data, expectedLength) return end + + local unitResources = GetCachedUnitResources(unitTag) + local magicka = unitResources[POWERTYPE_MAGICKA] + local stamina = unitResources[POWERTYPE_STAMINA] + + unitResources.percentageOnly = sharesPercentagesOnly + + magicka.percent, index = LGS:ReadUint8(data, index) + stamina.percent, index = LGS:ReadUint8(data, index) + + if(sharesPercentagesOnly) then + magicka.maximum = 1000 + stamina.maximum = 1000 + unitResources.hasFullData = false + elseif(isFullUpdate) then + magicka.maximum, index = LGS:ReadUint16(data, index) + stamina.maximum, index = LGS:ReadUint16(data, index) + unitResources.hasFullData = true + elseif(not unitResources.hasFullData and not isSelf) then + needFullUpdate = true + end + + magicka.current = math.floor((magicka.percent / 255) * magicka.maximum) + stamina.current = math.floor((stamina.percent / 255) * stamina.maximum) + + unitResources.lastUpdate = GetTimeStamp() + +-- Log("magicka: %d/%d stamina: %d/%d", magicka.current, magicka.maximum, stamina.current, stamina.maximum) + LGS.cm:FireCallbacks(ON_RESOURCES_CHANGED, unitTag, magicka.current, magicka.maximum, stamina.current, stamina.maximum, isSelf) +end + +function handler:RegisterForResourcesChanges(callback) + LGS.cm:RegisterCallback(ON_RESOURCES_CHANGED, callback) +end + +function handler:UnregisterForResourcesChanges(callback) + LGS.cm:UnregisterCallback(ON_RESOURCES_CHANGED, callback) +end + +local saveData = { + enabled = true, + percentOnly = true, +} -- TODO + +local MIN_SEND_TIMEOUT = 2 +local MIN_COMBAT_SEND_TIMEOUT = 1 +function handler:Send() + if(not saveData.enabled or not IsUnitGrouped("player")) then return end + local now = GetTimeStamp() + local timeout = IsUnitInCombat("player") and MIN_COMBAT_SEND_TIMEOUT or MIN_SEND_TIMEOUT + if(now - lastSendTime < timeout) then return end + + local unitResources = GetCachedUnitResources("player") + local magicka = unitResources[POWERTYPE_MAGICKA] + local stamina = unitResources[POWERTYPE_STAMINA] + + local magickaCurrent, magickaMaximum = GetUnitPower("player", POWERTYPE_MAGICKA) + local magickaPercent = math.floor(magickaCurrent / magickaMaximum * 255) + local staminaCurrent, staminaMaximum = GetUnitPower("player", POWERTYPE_STAMINA) + local staminaPercent = math.floor(staminaCurrent / staminaMaximum * 255) + + local percentOnly = saveData.percentOnly + sendFullUpdate = sendFullUpdate or (not percentOnly and (magicka.maximum ~= magickaMaximum or stamina.maximum ~= staminaMaximum)) + if(magicka.percent ~= magickaPercent or stamina.percent ~= staminaPercent or sendFullUpdate or needFullUpdate) then + magicka.percent = magickaPercent + stamina.percent = staminaPercent + + local data = {} + local index, bitIndex = 1, 1 + index, bitIndex = LGS:WriteBit(data, index, bitIndex, sendFullUpdate) + index, bitIndex = LGS:WriteBit(data, index, bitIndex, needFullUpdate) + index, bitIndex = LGS:WriteBit(data, index, bitIndex, percentOnly) + index = index + 1 + index = LGS:WriteUint8(data, index, magickaPercent) + index = LGS:WriteUint8(data, index, staminaPercent) + if(sendFullUpdate and not percentOnly) then + index = LGS:WriteUint16(data, index, magickaMaximum) + index = LGS:WriteUint16(data, index, staminaMaximum) + magicka.maximum = magickaMaximum + stamina.maximum = staminaMaximum + end + lastSendTime = now +-- Log("Send %d byte: is full: %s, needs full: %s, percent only: %s", #data, tostring(sendFullUpdate), tostring(needFullUpdate), tostring(percentOnly)) + LGS:Send(type, data) + + sendFullUpdate = false + needFullUpdate = false + end +end + +local function OnUpdate() + handler:Send() +end + +local isActive = false + +local function StartSending() + if(not isActive and saveData.enabled and IsUnitGrouped("player")) then + EVENT_MANAGER:RegisterForUpdate("LibGroupSocketResourceHandler", 1000, OnUpdate) + isActive = true + end +end + +local function StopSending() + if(isActive) then + EVENT_MANAGER:UnregisterForUpdate("LibGroupSocketResourceHandler") + isActive = false + end +end + +local function OnUnitCreated(_, unitTag) + sendFullUpdate = true + StartSending() +end + +local function OnUnitDestroyed(_, unitTag) + resources[GetUnitName(unitTag)] = nil + if(isActive and not IsUnitGrouped("player")) then + StopSending() + end +end + +local function Unload() + LGS.cm:UnregisterCallback(type, handler.dataHandler) + EVENT_MANAGER:UnregisterForEvent("LibGroupSocketResourceHandler", EVENT_UNIT_CREATED) + EVENT_MANAGER:UnregisterForEvent("LibGroupSocketResourceHandler", EVENT_UNIT_DESTROYED) + StopSending() +end + +local function Load() + handler.dataHandler = OnData + LGS.cm:RegisterCallback(type, OnData) + EVENT_MANAGER:RegisterForEvent("LibGroupSocketResourceHandler", EVENT_UNIT_CREATED, OnUnitCreated) + EVENT_MANAGER:RegisterForEvent("LibGroupSocketResourceHandler", EVENT_UNIT_DESTROYED, OnUnitDestroyed) + handler.Unload = Unload + + StartSending() + SLASH_COMMANDS["/lgrs"] = function(c) -- TODO + c = tonumber(c) + if(c == 1) then + Log("resourcehandler enabled") + saveData.enabled = true + StartSending() + elseif(c == 2) then + Log("resourcehandler disabled") + saveData.enabled = false + StopSending() + elseif(c == 3) then + Log("percentonly enabled") + saveData.percentOnly = true + elseif(c == 4) then + Log("percentonly disabled") + saveData.percentOnly = false + end + end +end + +if(handler.Unload) then handler.Unload() end +Load()