ESOUI SVN TaosGroupTools

[/] [trunk/] [TaosGroupTools/] [logic/] [PlayerHandler.lua] - Rev 136

Compare with Previous | Blame | View Log

--[[
        Addon: Taos Group Tools
        Author: TProg Taonnor
        Created by @Taonnor
]]--

--[[
        Global callbacks
]]--
TGT_PLAYER_ACTIVATED = "TGT-PlayerActivated"
TGT_PLAYER_DATA_OFFLINE_CHANGED = "TGT-PlayerDataOfflineChanged"
TGT_PLAYER_DATA_BUFFS_CHANGED = "TGT-PlayerDataBuffsChanged"
TGT_PLAYER_DATA_REMOTE_CHANGED = "TGT-PlayerDataRemoteChanged"
TGT_PLAYER_DATA_REFRESH = "TGT-PlayerHandlerRefresh"
TGT_PLAYER_DATA_CLEAR = "TGT-PlayerDataClear"
TGT_PLAYER_DATA_SUB_GROUP_CHANGED = "TGT-PlayerDataSubGroupChanged"
TGT_PLAYER_DATA_PURGE_CHANGED = "TGT-PlayerDataPurgeChanged"

--[[
        Global values
]]--
PLAYERTIMEOUT = 4 -- s; GetTimeStamp() is in seconds
COMBATTIMEOUT = 30000 -- ms

--[[
        Local variables
]]--
-- Set to local for faster access
local MESSAGE_PLAYER_RESOURCES = MESSAGE_PLAYER_RESOURCES
local MESSAGE_PLAYER_DPS = MESSAGE_PLAYER_DPS
local MESSAGE_PLAYER_HPS = MESSAGE_PLAYER_HPS

local REFRESHRATE = 1000 -- ms; RegisterForUpdate is in miliseconds
local TIMEOUT = PLAYERTIMEOUT
local EARTHGORE_ID = 97855
local EARTHGORE_CD = 35000 -- ms
local EARTHGORE = EARTHGORE_ICON_ID

local _logger = nil
local _settingsHandler = TGT_SettingsHandler

local _name = "TGT-PlayerHandler"
local _playerMyself = nil
local _groupPlayers = {}
local _lastCombatTimestamp = 0
local _lastGarbageCollectionTimestamp = 0

local _foodBuffs = FOOD_BUFFS
local _trackedBuffs = TRACKED_BUFFS
local _trackedSpecificBuffs = {
    [EARTHGORE_ICON_ID] = {},
    [DETONATON_ICON_ID] = {},
    [SPEEDBUFF_ICON_ID] = {},
}

--[[
        Table TGT_PlayerHandler
]]--
TGT_PlayerHandler = {}
TGT_PlayerHandler.__index = TGT_PlayerHandler

--[[
        ==============
    PRIVATE METHODS
    ==============
]]--

--[[
        GetAllReceivedHealing gets healing from all players
]]--
local function GetAllReceivedHealing()
    local healingSum = 0

    for i, player in pairs(_groupPlayers) do
        healingSum = healingSum + player.HealingReceived
    end

    return healingSum
end

--[[
        UpdateAllRelativeHeal updates all relative heal
]]--
local function UpdateAllRelativeHeal()
    local allReceivedHealing = GetAllReceivedHealing()

    for i, player in pairs(_groupPlayers) do
        player.HealingReceivedRelative = player.HealingReceived / allReceivedHealing * 100
    end
end

--[[
        ResetAllPlayerHeal resets all player heal
]]--
local function ResetAllPlayerHeal()
    for i, player in pairs(_groupPlayers) do
        player.HealingReceived = 0
        player.HealingReceivedRelative = 0
    end
end

--[[
        UpdatePlayerHps updates player hps from pingPlayer
]]--
local function UpdatePlayerHps(player, pingPlayer)
    if (pingPlayer.HealingReceived > 0) then
        player.HealingReceived = player.HealingReceived + pingPlayer.HealingReceived
        UpdateAllRelativeHeal()
    elseif (pingPlayer.HealingReceived == 0) then
        ResetAllPlayerHeal()
    else
        _logger:logError("PlayerHandler -> UpdatePlayerHps; HealingReceived unexpected value", pingPlayer.HealingReceived)
    end
end

--[[
        GetAllReceivedDamage gets damage from all players
]]--
local function GetAllReceivedDamage()
    local damageSum = 0

    for i, player in pairs(_groupPlayers) do
        damageSum = damageSum + player.DamageReceived
    end
        
    return damageSum
end

--[[
        UpdateAllRelativeDmg updates all relative dmg
]]--
local function UpdateAllRelativeDmg()
    local allReceivedDmg = GetAllReceivedDamage()

    for i, player in pairs(_groupPlayers) do
        player.DamageReceivedRelative = player.DamageReceived / allReceivedDmg * 100
    end
end

--[[
        ResetAllPlayerDmg resets all player dmg
]]--
local function ResetAllPlayerDmg()
    for i, player in pairs(_groupPlayers) do
        player.DamageReceived = 0
        player.DamageReceivedRelative = 0
    end
end

--[[
        UpdatePlayerDps updates player dps from pingPlayer
]]--
local function UpdatePlayerDps(player, pingPlayer)
    if (pingPlayer.DamageReceived > 0) then
        player.DamageReceived = player.DamageReceived + pingPlayer.DamageReceived
        UpdateAllRelativeDmg()
    elseif (pingPlayer.DamageReceived == 0) then
        ResetAllPlayerDmg()
    else
        _logger:logError("PlayerHandler -> UpdatePlayerDps; DamageReceived unexpected value", pingPlayer.DamageReceived)
    end
end

--[[
        UpdatePlayerResources updates player resources from pingPlayer
]]--
local function UpdatePlayerResources(player, pingPlayer)
    -- Update UltimateGroup or Earthgore, if procced
    if (pingPlayer.IsEarthgoreProcced) then
        -- -1000, because of lag
        player.IsEarthgoreProccedTimestamp = GetGameTimeMilliseconds() - 1000
    else
        if (player.UltimateGroup == nil or
            player.UltimateGroup.GroupAbilityPing ~= pingPlayer.GroupAbilityPing) then

            local ultimateGroup = TGT_UltimateGroupHandler.GetUltimateGroupByAbilityPing(pingPlayer.GroupAbilityPing)
            local ultimateName = GetAbilityName(ultimateGroup.GroupAbilityId)
            local ultimateIcon = GetAbilityIcon(ultimateGroup.GroupAbilityId)

            player.UltimateGroup = ultimateGroup
            player.UltimateName = ultimateName
            player.UltimateIcon = ultimateIcon
        end
    end
    
    -- Update relative ultimate
    if (player.IsPlayerDead) then
        player.RelativeUltimate = 0
    else
        if (player.RelativeUltimate ~= pingPlayer.RelativeUltimate) then
            -- play sound if ultimate ready
                    if (player.RelativeUltimate < 100 and pingPlayer.RelativeUltimate >= 100) then
                            local sound = TGT_SettingsHandler.SavedVariables.SoundOnReady
                            if (sound[1] > 1) then PlaySound(SOUNDS[sound[2]]) end
                    end

            -- play sound if ultimate raised
                    if (player.RelativeUltimate >= 100 and pingPlayer.RelativeUltimate < 100) then
                            local sound = TGT_SettingsHandler.SavedVariables.SoundOnThrown
                            if (sound[1] > 1) then PlaySound(SOUNDS[sound[2]]) end
                    end

            player.RelativeUltimate = pingPlayer.RelativeUltimate
        end
    end

    -- Update relative magicka
    if (player.RelativeMagicka ~= pingPlayer.RelativeMagicka) then
        player.RelativeMagicka = pingPlayer.RelativeMagicka
    end

    -- Update relative stamina
    if (player.RelativeStamina ~= pingPlayer.RelativeStamina) then
        player.RelativeStamina = pingPlayer.RelativeStamina
    end
end

--[[
        UpdateFoodBuff on player
]]--
local function UpdateFoodBuff(player)
    -- Reset first
    player.FoodBuffActive = false
    player.FoodBuffIcon = nil

    -- Iterate buffs and set new
    for i = 1, GetNumBuffs(player.PingTag) do
        local _, _, _, _, _, iconFile, _, _, _, _, abilityId = GetUnitBuffInfo(player.PingTag, i)

        if (_foodBuffs[abilityId] ~= nil) then
            player.FoodBuffActive = true
            player.FoodBuffIcon = iconFile
            break
        end
    end
end

--[[
        GetNewPlayer Gets new empty player
]]--
local function GetNewPlayer(pingTag)
    local current, max, effectiveMax = GetUnitPower(pingTag, POWERTYPE_HEALTH)
    local relativeHealth = math.floor((current / max * 100))

    if (relativeHealth > 100) then
        relativeHealth = 100
    end
    
    local playerName = GetUnitName(pingTag)

    local newPlayer = {
        PingTag = pingTag,
        PlayerPosition = tonumber(pingTag:match('(%d+)$')),
        GroupIdentifer = _settingsHandler.SavedVariables.PlayerFrameGroups[playerName],
        PlayerName = playerName,
        IsPlayerDead = IsUnitDead(pingTag),
        IsPlayerInCombat = IsUnitInCombat(pingTag),
        IsPlayerTimedOut = true,
        IsPlayerOnline = true,
        LastMapPingTimestamp = 0,
        UltimateName = nil,
        UltimateIcon = nil,
        UltimateGroup = nil,
        CurentHealth = current,
        CurrentHealthPool = max,
        RelativeHealth = (current / max * 100),
        RelativeUltimate = 0,
        RelativeMagicka = 0,
        RelativeStamina = 0,
        CurrentShield = 0,
        RelativeShield = 0,
                DamageDone = 0,
                HealingDone = 0,
        DamageToSend = 0,
        HealingToSend = 0,
        DamageReceived = 0,
        DamageReceivedRelative = 0,
        HealingReceived = 0,
        HealingReceivedRelative = 0,
        IsEarthgoreProcced = false,
        IsEarthgoreProccedTimestamp = 0,
        Buffs = {},
        FoodBuffActive = false,
        FoodBuffIcon = nil,
        PurgableDebuffs = 0,
        IsPurgable = false
    }

    -- Updates Buffs
    UpdateFoodBuff(newPlayer)

    return newPlayer
end

--[[
        SetUltimate Sets PlayerMyself ultimate; called on TGT_STATIC_ULTIMATE_ID_CHANGED
]]--
local function SetUltimate(staticUltimateID)
    _playerMyself.UltimateGroup = TGT_UltimateGroupHandler.GetUltimateGroupByAbilityId(staticUltimateID)
end

--[[
        UpdatePlayers Removes player if left; called on TAO_GROUP_CHANGED
]]--
local function UpdatePlayers(playerName, isJoined)
    if (playerName ~= "" or playerName ~= nil) then
        -- Player joined
        if (isJoined) then
            if (_groupPlayers[playerName] == nil) then
                local isGroupMember, unitTag = IsTargetGroupMember(playerName)
                                
                                if (isGroupMember) then
                                        local player = GetNewPlayer(unitTag)

                                        _groupPlayers[playerName] = player

                                        FireCallbacksAsync(TGT_PLAYER_DATA_OFFLINE_CHANGED, player)
                                else
                                        _logger:logDebug("isGroupMember == false", unitTag, playerName)
                                end
            end
        -- Player left
        else
            if (_groupPlayers[playerName] ~= nil) then
                local player = _groupPlayers[playerName]

                FireCallbacksAsync(TGT_PLAYER_DATA_CLEAR, player)

                _groupPlayers[playerName] = nil
            end
        end
    else
        _logger:logError("PlayerHandler -> UpdatePlayers; playerName invalid", playerName)
    end

    -- Prevent spam
    if (GetGameTimeMilliseconds() - _lastGarbageCollectionTimestamp > 10000) then
        _lastGarbageCollectionTimestamp = GetGameTimeMilliseconds()
        collectgarbage()
    end
end

--[[
        UpdateGroup Add or removes complete groupPlayers list; called on TAO_UNIT_GROUPED_CHANGED
]]--
local function UpdateGroup(isGrouped)
    for i,player in pairs(_groupPlayers) do
        if (isGrouped) then
            local isGroupMember, unitTag = IsTargetGroupMember(player.PlayerName)

            if (not isGroupMember) then
                FireCallbacksAsync(TGT_PLAYER_DATA_CLEAR, player)
                _groupPlayers[player.PlayerName] = nil
            end
        else
            FireCallbacksAsync(TGT_PLAYER_DATA_CLEAR, player)
            _groupPlayers[player.PlayerName] = nil
        end
    end

    -- Add all group players, which not already in group list
    if (isGrouped) then
        for i = 1, GetGroupSize() do
            local unitTag = GetGroupUnitTagByIndex(i)
            if (unitTag) then
                local playerName = GetUnitName(unitTag)

                if (_groupPlayers[playerName] == nil) then
                    local player = GetNewPlayer(unitTag)

                    _groupPlayers[playerName] = player

                    FireCallbacksAsync(TGT_PLAYER_DATA_OFFLINE_CHANGED, player)
                else
                    -- Refresh tag / position
                    local player = _groupPlayers[playerName]
                    player.PingTag = unitTag
                    player.PlayerPosition = tonumber(unitTag:match('(%d+)$'))
                end
            end
        end
    end
    
    -- Prevent spam
    if (GetGameTimeMilliseconds() - _lastGarbageCollectionTimestamp > 10000) then
        _lastGarbageCollectionTimestamp = GetGameTimeMilliseconds()
        collectgarbage()
    end
end

--[[
        OnCombatEvent Handles several combat events; called on EVENT_COMBAT_EVENT
]]--
local function OnCombatEvent(eventCode, result, isError, abilityName, graphic, actionSlotType, sourceName, sourceType, targetName, targetType, hitValue, powerType, damageType, log, sourceUnitID, targetUnitID, abilityID)
    -- Actions
    local isMyAction = (sourceType == COMBAT_UNIT_TYPE_PLAYER) or (sourceType == COMBAT_UNIT_TYPE_PLAYER_PET)
    local isDmgAction = (result == ACTION_RESULT_DAMAGE) or (result == ACTION_RESULT_CRITICAL_DAMAGE) or (result == ACTION_RESULT_DOT_TICK) or (result == ACTION_RESULT_DOT_TICK_CRITICAL)
    local isHealAction = (result == ACTION_RESULT_HEAL) or (result == ACTION_RESULT_CRITICAL_HEAL) or (result == ACTION_RESULT_HOT_TICK) or (result == ACTION_RESULT_HOT_TICK_CRITICAL)

    -- DMG
    if (isMyAction and isDmgAction and (sourceName ~= targetName) and (hitValue > 0)) then
                _playerMyself.DamageDone = _playerMyself.DamageDone + hitValue
    end

    -- HEAL
        local isTargetGroupMember, unitTag = IsTargetGroupMember(targetName)
    if (isMyAction and isHealAction and (sourceName ~= targetName) and (hitValue > 0) and isTargetGroupMember) then
                local currentHealth, maxHealth, effectiveMaxHealth = GetUnitPower(unitTag, POWERTYPE_HEALTH)
                -- Realy healed?
                if (currentHealth ~= maxHealth) then
                        _playerMyself.HealingDone = _playerMyself.HealingDone + hitValue
                end
    end
end

--[[
        IsTrackedBuff Checks abilityId or IconName, because ZOS buff/debuff tracking sends inconsistend values for abilityId's
]]--
local function IsTrackedBuff(abilityId, iconName)
    -- Check specific buffs like speedbuff, deto, earthgore
    if (_trackedSpecificBuffs[abilityId] ~= nil) then
        return true, abilityId
    end

    -- Check buffs via icons
    for i, buff in ipairs(_trackedBuffs) do
        if (string.match(iconName, buff.IconName)) then
            return true, buff.IconId -- found
        end
    end

    -- Nothing found
    return false, nil
end

--[[
        IsPurgableEffect Checks several values, because ZOS buff/debuff tracking sends inconsistend values for abilities
]]--
local function IsPurgableEffect(abilityId, abilityType, statusEffectType)
    local purgableAbilities = {
        [15775] = {},  -- Öl
        [15776] = {},  -- Öl
        [28480] = {},  -- Feuerballiste
        [66243] = {},  -- Kalthafen Feuerballiste
        [25869] = {},  -- Feuertopf-Tribok
        [66247] = {},  -- Feuertopf-Tribok
        [20528] = {},  -- DK Krallen
        [31898] = {},  -- DK Krallen
        [104825] = {}, -- Warden Eis
        [44549] = {},  -- Bogen Gift
        [89491] = {},  -- Heimsuchender Fluch
        [28452] = {},  -- Daedrisches Grabmal
    }

    local isPurgableAbility = purgableAbilities[abilityId] ~= nil
    local isPurgableAbilityType = 
        abilityType == ABILITY_TYPE_STUN or
        abilityType == ABILITY_TYPE_DISORIENT or
        abilityType == ABILITY_TYPE_FEAR
    local isPurgableStatusEffect = 
        statusEffectType == STATUS_EFFECT_TYPE_DISEASE or 
        statusEffectType == STATUS_EFFECT_TYPE_MESMERIZE or
        statusEffectType == STATUS_EFFECT_TYPE_ROOT or
        statusEffectType == STATUS_EFFECT_TYPE_STUN

    return isPurgableAbility or isPurgableAbilityType or isPurgableStatusEffect
end

--[[
        OnEffectChangedEvent Handles several effect events; called on EVENT_EFFECT_CHANGED
]]--
local function OnEffectChangedEvent(eventCode, changeType, effectSlot, effectName, unitTag, beginTime, endTime, stackCount, iconName, buffType, effectType, abilityType, statusEffectType, unitName, unitId, abilityId, sourceType)
    local isMyself = sourceType == COMBAT_UNIT_TYPE_PLAYER
    local isGroupPlayer = string.match(unitTag, "group")
    local isGained = changeType == EFFECT_RESULT_GAINED
    local isFaded = changeType == EFFECT_RESULT_FADED

    -- Earthgore
    if (abilityId == EARTHGORE_ID and isGained and isMyself) then
        _playerMyself.IsEarthgoreProcced = true
    end
    
    -- TODO: To find further purgable skills
    if (GetUnitName(unitTag) == GetUnitName("player")) then
        _logger:logDebug(effectName, unitTag, unitName, abilityId, changeType)
        --_logger:logDebug(iconName, buffType, effectType, abilityType, statusEffectType)
    end

    if (isGroupPlayer) then
        -- Purge
        if (IsPurgableEffect(abilityId, abilityType, statusEffectType)) then
            local playerName = GetUnitName(unitTag)

            if (_groupPlayers[playerName] ~= nil) then
                local player = _groupPlayers[playerName]
            
                if (changeType == EFFECT_RESULT_GAINED) then
                    if (player.PurgableDebuffs < 0) then
                        -- to prevent purgable debuffs are smaller than zero, because ZOS api sends sometimes updates after death or not
                        player.PurgableDebuffs = 0
                    end

                    player.PurgableDebuffs = player.PurgableDebuffs + 1
                    player.IsPurgable = true

                    --_logger:logDebug(playerName, "Purge add", effectName, abilityId, player.PurgableDebuffs)
                elseif (changeType == EFFECT_RESULT_FADED) then
                    player.PurgableDebuffs = player.PurgableDebuffs - 1

                    if (player.PurgableDebuffs <= 0) then
                        player.PurgableDebuffs = 0
                        player.IsPurgable = false

                        --_logger:logDebug(playerName, "Purge remove", effectName, abilityId, player.PurgableDebuffs)
                    else
                        --_logger:logDebug(playerName, "Purge remove and update", effectName, abilityId, player.PurgableDebuffs)
                    end
                else
                    player.IsPurgable = player.PurgableDebuffs > 0
                    --_logger:logDebug(playerName, "Purge update", effectName, abilityId, player.PurgableDebuffs)
                end

                FireCallbacksAsync(TGT_PLAYER_DATA_PURGE_CHANGED, player)
            end
        end

        -- TrackedBuffs
        local isTrackedBuff, iconId = IsTrackedBuff(abilityId, iconName)

        if (isTrackedBuff) then
            local playerName = GetUnitName(unitTag)

            if (_groupPlayers[playerName] ~= nil) then
                local player = _groupPlayers[playerName]
            
                if (changeType == EFFECT_RESULT_GAINED) then
                    player.Buffs[iconId] = { isActive = true, startTime = beginTime, finishTime = endTime }
                elseif (changeType == EFFECT_RESULT_FADED) then
                    player.Buffs[iconId] = { isActive = false, startTime = beginTime, finishTime = endTime }
                else
                    player.Buffs[iconId] = { isActive = true, startTime = beginTime, finishTime = endTime }
                end

                FireCallbacksAsync(TGT_PLAYER_DATA_BUFFS_CHANGED, player, iconId)
            end
        end
    end
end

--[[
        UpdatePlayerHealth updates player health and fires TGT_PLAYER_DATA_OFFLINE_CHANGED
]]--
local function UpdatePlayerHealth(player, powerPool, powerPoolMax)
    local isPlayerDead = IsUnitDead(player.PingTag)
    
    if (isPlayerDead or powerPool == 0) then 
        player.CurentHealth = 0
        player.IsPlayerDead = true

        -- to prevent purgable debuffs will not disappear, because ZOS api sends sometimes updates after death or not
        player.PurgableDebuffs = 0
        player.IsPurgable = false

        FireCallbacksAsync(TGT_PLAYER_DATA_PURGE_CHANGED, player)
    else
        player.CurentHealth = powerPool
        player.IsPlayerDead = false
    end
    
    player.CurrentHealthPool = powerPoolMax
    
    local relativeHealth = math.floor((powerPool / powerPoolMax * 100))

    if (relativeHealth > 100) then
        relativeHealth = 100
    end

    player.RelativeHealth = relativeHealth

    FireCallbacksAsync(TGT_PLAYER_DATA_OFFLINE_CHANGED, player)
end

--[[
        OnPowerUpdate Handles several player pool events; called on EVENT_POWER_UPDATE
]]--
local function OnPowerUpdate(evt, unitTag, powerPoolIndex, powerType, powerPool, powerPoolMax)
    if (powerType == POWERTYPE_HEALTH) then
        local playerName = GetUnitName(unitTag)
        if (TGT_MOCKED) then
            local isNotMyself = string.find(playerName, "reticleover")
            if (isNotMyself == nil) then
                for i,player in pairs(_groupPlayers) do
                    UpdatePlayerHealth(player, powerPool, powerPoolMax)
                end
            end
        else
            if (_groupPlayers[playerName] ~= nil) then
                local player = _groupPlayers[playerName]
                UpdatePlayerHealth(player, powerPool, powerPoolMax)
            end
        end
    end
end

--[[
        UpdatePlayerShield updates player shield and fires TGT_PLAYER_DATA_OFFLINE_CHANGED
]]--
local function UpdatePlayerShield(player, value)
    local isPlayerDead = IsUnitDead(player.PingTag)
    player.IsPlayerDead = isPlayerDead

    if (isPlayerDead) then 
        player.CurrentShield = 0
    else
        player.CurrentShield = value
        local relativeShield = math.floor((value / player.CurrentHealthPool * 100))

        if (relativeShield > 100) then
            relativeShield = 100
        end

        player.RelativeShield = relativeShield
    end
    
    FireCallbacksAsync(TGT_PLAYER_DATA_OFFLINE_CHANGED, player)
end

--[[
        HandleVisualEvents Handles visual events from different base handlers
]]--
local function HandleVisualEvents(unitTag, powerType, visualType, value)
    if (powerType == POWERTYPE_HEALTH) then
        -- Shield
        if (visualType == ATTRIBUTE_VISUAL_POWER_SHIELDING) then
            local playerName = GetUnitName(unitTag)

            if (TGT_MOCKED) then
                local isNotMyself = string.find(playerName, "reticleover")
                if (isNotMyself == nil) then
                    for i,player in pairs(_groupPlayers) do
                        UpdatePlayerShield(player, value)
                    end
                end
            else
                if (_groupPlayers[playerName] ~= nil) then
                    local player = _groupPlayers[playerName]
                    UpdatePlayerShield(player, value)
                end
            end
        end
    end
end

--[[
        OnVisualAdded Handles EVENT_UNIT_ATTRIBUTE_VISUAL_ADDED events
]]--
local function OnVisualAdded(evt, unitTag, visualType, stat, attribute, powerType, value, maxValue)
    HandleVisualEvents(unitTag, powerType, visualType, value)
end

--[[
        OnVisualUpdated Handles EVENT_UNIT_ATTRIBUTE_VISUAL_UPDATED events
]]--
local function OnVisualUpdated(evt, unitTag, visualType, stat, attribute, powerType, oldValue, newValue, oldMaxValue, newMaxValue)
    HandleVisualEvents(unitTag, powerType, visualType, newValue)
end

--[[
        OnVisualRemoved Handles EVENT_UNIT_ATTRIBUTE_VISUAL_REMOVED events
]]--
local function OnVisualRemoved(evt, unitTag, visualType, stat, attribute, powerType, value, maxValue)
    HandleVisualEvents(unitTag, powerType, visualType, 0)
end

--[[
        OnPlayerActivated fires TGT_PLAYER_ACTIVATED callbacks
]]--
local function OnPlayerActivated(eventCode)
    FireCallbacksAsync(TGT_PLAYER_ACTIVATED)

    for i,player in pairs(_groupPlayers) do
        UpdateFoodBuff(player)
        local health, maxHealth = GetUnitPower(player.PingTag, POWERTYPE_HEALTH)
        UpdatePlayerHealth(player, health, maxHealth) -- Calls update offline player
        FireCallbacksAsync(TGT_PLAYER_DATA_REMOTE_CHANGED, player)
    end

    FireCallbacksAsync(TGT_PLAYER_DATA_REFRESH)
end

--[[
        UpdatePlayerData Updates group player data
]]--
local function UpdatePlayerData(pingPlayer)
    local playerName = GetUnitName(pingPlayer.PingTag)
    if (string.len(playerName) > 0) then
        local player = nil

        if (_groupPlayers[playerName] == nil) then
            -- Should not happens, especially in MOCKED mode
            _logger:logError("PlayerHandler -> UpdatePlayerData; Added player via UpdatePlayerData", playerName)

            player = GetNewPlayer(pingPlayer.PingTag)
            _groupPlayers[playerName] = player
        else
            player = _groupPlayers[playerName]
            
            -- Refresh tag / position
            player.PingTag = pingPlayer.PingTag
            player.PlayerPosition = tonumber(pingPlayer.PingTag:match('(%d+)$'))
        end

        -- Update timestamp
        player.LastMapPingTimestamp = GetTimeStamp()
        if (pingPlayer.MessageType == MESSAGE_PLAYER_RESOURCES) then
            UpdatePlayerResources(player, pingPlayer)
        elseif (pingPlayer.MessageType == MESSAGE_PLAYER_DPS) then
            UpdatePlayerDps(player, pingPlayer)
        elseif (pingPlayer.MessageType == MESSAGE_PLAYER_HPS) then
            UpdatePlayerHps(player, pingPlayer)
        else
            -- should not happen
            _logger:logError(zo_strformat("PlayerHandler -> UpdatePlayerData; Message Type not valid (<<1>>)", messageType))
        end
    
        -- Update Earthgore procc; If last earthgore timestamp < EARTHGORE_CD, procc is on CD (true); otherwhise false
        local lastProcDifference = GetGameTimeMilliseconds() - player.IsEarthgoreProccedTimestamp
        player.Buffs[EARTHGORE] = { isActive = lastProcDifference < EARTHGORE_CD, startTime = 0, finishTime = 0 }

        -- update the player
        FireCallbacksAsync(TGT_PLAYER_DATA_REMOTE_CHANGED, player)

    else
        _logger:logError("PlayerHandler -> UpdatePlayerData; Ping Player name empty", pingPlayer.PingTag)
    end

    -- Clear ping player
    pingPlayer = nil
end

--[[
        GetMyselfData Gets PlayerMyself and updates values before
]]--
local function GetMyselfData(messageType)
    -- Update combat timestamp
    if (IsGroupInCombat()) then
        _lastCombatTimestamp = GetGameTimeMilliseconds()
    end
    -- get combatPause
    local combatPause = math.abs(_lastCombatTimestamp - GetGameTimeMilliseconds())

    -- update needed
    local updateNeeded = true

    -- Get current image of player resources
    if (messageType == MESSAGE_PLAYER_RESOURCES) then
        local currentUltimate, maxUltimate, effectiveMaxUltimate = GetUnitPower("player", POWERTYPE_ULTIMATE)
        local ultimateCost = math.max(1, GetAbilityCost(_playerMyself.UltimateGroup.GroupAbilityId))
        local relativeUltimate = math.floor((currentUltimate / ultimateCost) * 100)

            if (relativeUltimate > 100) then
                    relativeUltimate = 100
            end

        local currentMagicka, maxMagicka, effectiveMaxMagicka = GetUnitPower("player", POWERTYPE_MAGICKA)
        local relativeMagicka = math.floor((currentMagicka / maxMagicka) * 100)
        
            if (relativeMagicka > 100) then
                    relativeMagicka = 100
            end

        local currentStamina, maxStamina, effectiveMaxStamina = GetUnitPower("player", POWERTYPE_STAMINA)
        local relativeStamina = math.floor((currentStamina / maxStamina) * 100)
        
            if (relativeStamina > 100) then
                    relativeStamina = 100
            end

        _playerMyself.RelativeUltimate = relativeUltimate
        _playerMyself.RelativeMagicka = relativeMagicka
        _playerMyself.RelativeStamina = relativeStamina
    elseif (messageType == MESSAGE_PLAYER_DPS) then
        if (_playerMyself.DamageDone > 0) then
            _playerMyself.DamageToSend = _playerMyself.DamageDone
            _playerMyself.DamageDone = _playerMyself.DamageDone - _playerMyself.DamageToSend
        else
            if (combatPause >= COMBATTIMEOUT) then
                _playerMyself.DamageToSend = 0
            else
                -- Damage not changed
                updateNeeded = false
            end
        end
    elseif (messageType == MESSAGE_PLAYER_HPS) then
        if (_playerMyself.HealingDone > 0) then
            _playerMyself.HealingToSend = _playerMyself.HealingDone
            _playerMyself.HealingDone = _playerMyself.HealingDone - _playerMyself.HealingToSend
        else
            if (combatPause >= COMBATTIMEOUT) then
                _playerMyself.HealingToSend = 0
            else
                -- Healing not changed
                updateNeeded = false
            end
        end
    end

    return _playerMyself, updateNeeded
end

--[[
        UpdatePlayerStatus updates offline, online timedout, combat values and fires TGT_PLAYER_DATA_REFRESH callbacks, if needed
]]--
local function UpdatePlayerStatus()
    for i,player in pairs(_groupPlayers) do
        -- Update timeout
        local isPlayerTimedOut = (GetTimeStamp() - player.LastMapPingTimestamp) >= PLAYERTIMEOUT

        if (player.IsPlayerTimedOut ~= isPlayerTimedOut) then
            player.IsPlayerTimedOut = isPlayerTimedOut
            FireCallbacksAsync(TGT_PLAYER_DATA_REMOTE_CHANGED, player)
        end

        -- Update Offline/Online
        local isOnline = IsUnitOnline(player.PingTag)

        if (player.IsPlayerOnline ~= isOnline) then
            player.IsPlayerOnline = isOnline
        end

        -- Update Combat
        local isInCombat = IsUnitInCombat(player.PingTag)
        
        if (player.IsPlayerInCombat ~= isInCombat) then
            player.IsPlayerInCombat = isInCombat
        end

        UpdateFoodBuff(player)
        local health, maxHealth = GetUnitPower(player.PingTag, POWERTYPE_HEALTH)
        UpdatePlayerHealth(player, health, maxHealth) -- Calls update offline player
    end
end

--[[
        OnTimedUpdate fires TGT_PLAYER_DATA_REFRESH callbacks, if needed
]]--
local function OnTimedUpdate()
    -- Only if player is in group
    if (GetIsUnitGrouped()) then
        local functionTimestamp = GetGameTimeMilliseconds()

        UpdatePlayerStatus()
            FireCallbacksAsync(TGT_PLAYER_DATA_REFRESH)

        -- Send data
        if (_settingsHandler.SavedVariables.IsSendingDataActive) then
            if (_lastMessageType == nil or _lastMessageType == MESSAGE_PLAYER_HPS) then
                TGT_Communicator.SendData(GetMyselfData(MESSAGE_PLAYER_RESOURCES), MESSAGE_PLAYER_RESOURCES)
                _lastMessageType = MESSAGE_PLAYER_RESOURCES
            elseif (_lastMessageType == nil or _lastMessageType == MESSAGE_PLAYER_RESOURCES) then
                local player, updateNeeded = GetMyselfData(MESSAGE_PLAYER_DPS)

                if (updateNeeded) then
                    TGT_Communicator.SendData(player, MESSAGE_PLAYER_DPS)
                else
                    TGT_Communicator.SendData(GetMyselfData(MESSAGE_PLAYER_RESOURCES), MESSAGE_PLAYER_RESOURCES)
                end

                _lastMessageType = MESSAGE_PLAYER_DPS
            elseif (_lastMessageType == nil or _lastMessageType == MESSAGE_PLAYER_DPS) then
                local player, updateNeeded = GetMyselfData(MESSAGE_PLAYER_HPS)

                if (updateNeeded) then
                    TGT_Communicator.SendData(player, MESSAGE_PLAYER_HPS)
                else
                    TGT_Communicator.SendData(GetMyselfData(MESSAGE_PLAYER_RESOURCES), MESSAGE_PLAYER_RESOURCES)
                end

                _lastMessageType = MESSAGE_PLAYER_HPS
            end
        end

        _logger:logTrace("TGT_PlayerHandler -> OnTimedUpdate", GetGameTimeMilliseconds() - functionTimestamp)
    end
end

--[[
        Called on new data from Communication
]]--
local function MockPlayerData(pingPlayer)
    local pingTag = pingPlayer.PingTag

    for i = 1, GetGroupSize() do
        pingPlayer.PingTag = pingTag .. tostring(i)
        if (i <= 4) then
            pingPlayer.GroupAbilityPing = 1
        elseif (i <= 8) then
            pingPlayer.GroupAbilityPing = 6
        elseif (i <= 12) then
            pingPlayer.GroupAbilityPing = 13
        elseif (i <= 16) then
            pingPlayer.GroupAbilityPing = 15
        elseif (i <= 20) then
            pingPlayer.GroupAbilityPing = 25
        else
            pingPlayer.GroupAbilityPing = 27
        end
        UpdatePlayerData(pingPlayer)
    end
end

--[[
        Called on new data from Communication
]]--
local function OnData(pingPlayer)
    if (pingPlayer ~= nil) then
        local functionTimestamp = GetGameTimeMilliseconds()
        if (TGT_MOCKED) then
            MockPlayerData(pingPlayer)
        else
            UpdatePlayerData(pingPlayer)
        end
        _logger:logTrace("PlayerHandler -> OnData", GetGameTimeMilliseconds() - functionTimestamp)
    else
        _logger:logError("PlayerHandler -> OnMapPing; Ping invalid ultimateGroup: " .. tostring(ultimateGroup) .. "; relativeUltimate: " .. tostring(relativeUltimate))
    end
end

--[[
        ==============
    PUBLIC METHODS
    ==============
]]--

--[[
        GetRemoteGroupPlayers returns internal group players, filtered by remote attribute
]]--
function TGT_PlayerHandler.GetRemoteGroupPlayers()
    local remotePlayers = {}

    for i,player in pairs(_groupPlayers) do
        if (player.LastMapPingTimestamp ~= 0) then
            remotePlayers[player.PlayerName] = player
        end
    end

    return remotePlayers
end

--[[
        GetGroupPlayers returns internal group players
]]--
function TGT_PlayerHandler.GetGroupPlayers()
    return _groupPlayers
end

--[[
        SetPlayerSubGroup sets sub group for player and sets in SettingsHandler
]]--
function TGT_PlayerHandler.SetPlayerSubGroup(groupIdentifer, player)
    if (_groupPlayers[player.PlayerName] ~= nil) then
        _settingsHandler.SavedVariables.PlayerFrameGroups[player.PlayerName] = groupIdentifer
        player.GroupIdentifer = groupIdentifer

        FireCallbacksAsync(TGT_PLAYER_DATA_SUB_GROUP_CHANGED, player)
    else
        _logger:logError("PlayerHandler -> SetPlayerSubGroup; Player not in group list", player.PlayerName)
    end
end

--[[
        Initialize initializes TGT_PlayerHandler
]]--
function TGT_PlayerHandler.Initialize()
    _logger = TGT_LOGGER

    _playerMyself = GetNewPlayer("player")
    _playerMyself.UltimateGroup = 
                TGT_UltimateGroupHandler.GetUltimateGroupByAbilityId(TGT_SettingsHandler.GetStaticUltimateIDSettings())
        
    -- Register events
    EVENT_MANAGER:RegisterForEvent(_name, EVENT_COMBAT_EVENT, OnCombatEvent)
    EVENT_MANAGER:RegisterForEvent(_name, EVENT_EFFECT_CHANGED, OnEffectChangedEvent)
    EVENT_MANAGER:RegisterForEvent(_name, EVENT_PLAYER_ACTIVATED, OnPlayerActivated)
    EVENT_MANAGER:RegisterForEvent(_name, EVENT_POWER_UPDATE, OnPowerUpdate)
    EVENT_MANAGER:RegisterForEvent(_name, EVENT_UNIT_ATTRIBUTE_VISUAL_ADDED, OnVisualAdded)
    EVENT_MANAGER:RegisterForEvent(_name, EVENT_UNIT_ATTRIBUTE_VISUAL_UPDATED, OnVisualUpdated)
    EVENT_MANAGER:RegisterForEvent(_name, EVENT_UNIT_ATTRIBUTE_VISUAL_REMOVED, OnVisualRemoved)

    -- Start update timer
        EVENT_MANAGER:RegisterForUpdate(_name, REFRESHRATE, OnTimedUpdate)

    -- Register callbacks
    CALLBACK_MANAGER:RegisterCallback(TGT_MAP_PING_CHANGED, OnData)
    CALLBACK_MANAGER:RegisterCallback(TGT_STATIC_ULTIMATE_ID_CHANGED, SetUltimate)
    CALLBACK_MANAGER:RegisterCallback(TAO_GROUP_CHANGED, UpdatePlayers)
    CALLBACK_MANAGER:RegisterCallback(TAO_UNIT_GROUPED_CHANGED, UpdateGroup)
    
    _logger:logTrace("TGT_PlayerHandler -> Initialized")
end

Compare with Previous | Blame