refactored to use own damage handling instead of ftc

René Welbers [02-14-16 - 23:23]
refactored to use own damage handling instead of ftc
updated lib
Filename
OverwriteFTC.lua
PanicModeCombatAnalyzer.txt
PmCa.lua
damage.lua
lib/LibGroupSocket/LibGroupSocket.lua
lib/LibGroupSocket/handlers/DpsHandler.lua
lib/LibGroupSocket/handlers/ResourceHandler.lua
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()