ESOUI SVN TaosGroupUltimate

[/] [trunk/] [TaosGroupUltimate/] [libs/] [LibMapPing/] [LibMapPing.lua] - Rev 61

Compare with Previous | Blame | View Log

local LIB_IDENTIFIER = "LibMapPing"
local lib = LibStub:NewLibrary(LIB_IDENTIFIER, 6)

if not lib then
    return -- already loaded and no upgrade necessary
end

local function Log(message, ...)
    df("[%s] %s", LIB_IDENTIFIER, message:format(...))
end

local MAP_PIN_TYPE_PLAYER_WAYPOINT = MAP_PIN_TYPE_PLAYER_WAYPOINT
local MAP_PIN_TYPE_PING = MAP_PIN_TYPE_PING
local MAP_PIN_TYPE_RALLY_POINT = MAP_PIN_TYPE_RALLY_POINT

local MAP_PIN_TAG_PLAYER_WAYPOINT = "waypoint"
local MAP_PIN_TAG_RALLY_POINT = "rally"
local PING_CATEGORY = "pings"

local PING_EVENT_WATCHDOG_TIME = 400 -- ms

local MAP_PIN_TAG = {
    [MAP_PIN_TYPE_PLAYER_WAYPOINT] = MAP_PIN_TAG_PLAYER_WAYPOINT,
    --[MAP_PIN_TYPE_PING] = group pings have individual tags for each member
    [MAP_PIN_TYPE_RALLY_POINT] = MAP_PIN_TAG_RALLY_POINT,
}

local originalPingMap, originalRemovePlayerWaypoint, originalRemoveRallyPoint
local GET_MAP_PING_FUNCTION = {} -- is initialized in Load()
local REMOVE_MAP_PING_FUNCTION = {} -- also initialized in Load()

--- MapPingState is an enumeration of the possible states of a map ping.
lib.MAP_PING_NOT_SET = 0 --- There is no ping.
lib.MAP_PING_NOT_SET_PENDING = 1 --- The ping has been removed, but EVENT_MAP_PING has not been processed.
lib.MAP_PING_SET_PENDING = 2 --- A ping was added, but EVENT_MAP_PING has not been processed.
lib.MAP_PING_SET = 3 --- There is a ping.

lib.mutePing = lib.mutePing or {}
lib.suppressPing = lib.suppressPing or {}
lib.pingState = lib.pingState or {}
lib.pendingPing = lib.pendingPing or {}
lib.cm = lib.cm or ZO_CallbackObject:New()
local g_mapPinManager = lib.mapPinManager

local function GetPingTagFromType(pingType)
    return MAP_PIN_TAG[pingType] or GetGroupUnitTagByIndex(GetGroupIndexByUnitTag("player")) or ""
end

local function GetKey(pingType, pingTag)
    pingTag = pingTag or GetPingTagFromType(pingType)
    return string.format("%d_%s", pingType, pingTag)
end

-- TODO keep an eye on worldmap.lua for changes
local function HandleMapPing(eventCode, pingEventType, pingType, pingTag, x, y, isPingOwner)
    local key = GetKey(pingType, pingTag)
    local data = lib.pendingPing[key]
    if data and data[1] == pingEventType then
        lib.pendingPing[key] = nil
    end
    if(pingEventType == PING_EVENT_ADDED) then
        lib.cm:FireCallbacks("BeforePingAdded", pingType, pingTag, x, y, isPingOwner)
        lib.pingState[key] = lib.MAP_PING_SET
        g_mapPinManager:RemovePins(PING_CATEGORY, pingType, pingTag)
        if(not lib:IsPingSuppressed(pingType, pingTag)) then
            g_mapPinManager:CreatePin(pingType, pingTag, x, y)
            if(isPingOwner and not lib:IsPingMuted(pingType, pingTag)) then
                PlaySound(SOUNDS.MAP_PING)
            end
        end
        lib.cm:FireCallbacks("AfterPingAdded", pingType, pingTag, x, y, isPingOwner)
    elseif(pingEventType == PING_EVENT_REMOVED) then
        lib.cm:FireCallbacks("BeforePingRemoved", pingType, pingTag, x, y, isPingOwner)
        lib.pingState[key] = lib.MAP_PING_NOT_SET
        g_mapPinManager:RemovePins(PING_CATEGORY, pingType, pingTag)
        if (isPingOwner and not lib:IsPingSuppressed(pingType, pingTag) and not lib:IsPingMuted(pingType, pingTag)) then
            PlaySound(SOUNDS.MAP_PING_REMOVE)
        end
        lib.cm:FireCallbacks("AfterPingRemoved", pingType, pingTag, x, y, isPingOwner)
    end
end

local function HandleMapPingEventNotFired()
    EVENT_MANAGER:UnregisterForUpdate(LIB_IDENTIFIER)
    for key, data in pairs(lib.pendingPing) do
        local pingEventType, pingType, x, y, zoneIndex = unpack(data)
        local pingTag = GetPingTagFromType(pingType)
        -- The event is delayed and thus may not match the current map anymore.
        if GetCurrentMapZoneIndex() ~= zoneIndex then
            -- The coords do not match the current map. Do not draw a pin.
            lib:SuppressPing(pingType, pingTag) -- Will be set to zero afterwards, see below.
        end
        HandleMapPing(0, pingEventType, pingType, pingTag, x, y, true)
        lib.pendingPing[key] = nil
        lib.mutePing[key] = 0
        lib.suppressPing[key] = 0
    end
end

local function ResetEventWatchdog(key, ...)
    lib.pendingPing[key] = {...}
    EVENT_MANAGER:UnregisterForUpdate(LIB_IDENTIFIER)
    EVENT_MANAGER:RegisterForUpdate(LIB_IDENTIFIER, PING_EVENT_WATCHDOG_TIME, HandleMapPingEventNotFired)
end

local function CustomPingMap(pingType, mapType, x, y)
    if(pingType == MAP_PIN_TYPE_PING and not IsUnitGrouped("player")) then return end
    local key = GetKey(pingType)
    lib.pingState[key] = lib.MAP_PING_SET_PENDING
    ResetEventWatchdog(key, PING_EVENT_ADDED, pingType, x, y, GetCurrentMapZoneIndex())
    return originalPingMap(pingType, mapType, x, y)
end

local function CustomGetMapPlayerWaypoint()
    if(lib:IsPingSuppressed(MAP_PIN_TYPE_PLAYER_WAYPOINT, MAP_PIN_TAG_PLAYER_WAYPOINT)) then
        return 0, 0
    end
    return GET_MAP_PING_FUNCTION[MAP_PIN_TYPE_PLAYER_WAYPOINT]()
end

local function CustomGetMapPing(pingTag)
    if(lib:IsPingSuppressed(MAP_PIN_TYPE_PING, pingTag)) then
        return 0, 0
    end
    return GET_MAP_PING_FUNCTION[MAP_PIN_TYPE_PING](pingTag)
end

local function CustomGetMapRallyPoint()
    if(lib:IsPingSuppressed(MAP_PIN_TYPE_RALLY_POINT, MAP_PIN_TAG_RALLY_POINT)) then
        return 0, 0
    end
    return GET_MAP_PING_FUNCTION[MAP_PIN_TYPE_RALLY_POINT]()
end

local function CustomRemovePlayerWaypoint()
    local key = GetKey(MAP_PIN_TYPE_PLAYER_WAYPOINT, MAP_PIN_TAG_PLAYER_WAYPOINT)
    lib.pingState[key] = lib.MAP_PING_NOT_SET_PENDING
    ResetEventWatchdog(key, PING_EVENT_REMOVED, MAP_PIN_TYPE_PLAYER_WAYPOINT, 0, 0, GetCurrentMapZoneIndex())
    return originalRemovePlayerWaypoint()
end

local function CustomRemoveMapPing()
    -- there is no such function for group pings, but we can set it to 0, 0 which effectively hides it
    PingMap(MAP_PIN_TYPE_PING, MAP_TYPE_LOCATION_CENTERED, 0, 0)
end

local function CustomRemoveRallyPoint()
    local key = GetKey(MAP_PIN_TYPE_RALLY_POINT, MAP_PIN_TAG_RALLY_POINT)
    lib.pingState[key] = lib.MAP_PING_NOT_SET_PENDING
    ResetEventWatchdog(key, PING_EVENT_REMOVED, MAP_PIN_TYPE_RALLY_POINT, 0, 0)
    originalRemoveRallyPoint()
end

--- Wrapper for PingMap.
--- pingType is one of the three possible MapDisplayPinType for map pings (MAP_PIN_TYPE_PLAYER_WAYPOINT, MAP_PIN_TYPE_PING or MAP_PIN_TYPE_RALLY_POINT).
--- mapType is usually MAP_TYPE_LOCATION_CENTERED.
--- x and y are the normalized coordinates on the current map.
function lib:SetMapPing(pingType, mapType, x, y)
    PingMap(pingType, mapType, x, y)
end

--- Wrapper for the different ping removal functions.
--- For waypoints and rally points it calls their respective removal function
--- For group pings it just sets the position to 0, 0 as there is no function to clear them
function lib:RemoveMapPing(pingType)
    if(REMOVE_MAP_PING_FUNCTION[pingType]) then
        REMOVE_MAP_PING_FUNCTION[pingType]()
    end
end

--- Wrapper for the different get ping functions. Returns coordinates regardless of their suppression state.
--- The game API functions return 0, 0 when the ping type is suppressed.
--- pingType is the same as for SetMapPing.
--- pingTag is optionally used if another group member's MAP_PIN_TYPE_PING should be returned (possible values: group1 .. group24).
function lib:GetMapPing(pingType, pingTag)
    local x, y = 0, 0
    if(GET_MAP_PING_FUNCTION[pingType]) then
        x, y = GET_MAP_PING_FUNCTION[pingType](pingTag or GetPingTagFromType(pingType))
    end
    return x, y
end

--- Returns the MapPingState for the pingType and pingTag.
function lib:GetMapPingState(pingType, pingTag)
    local key = GetKey(pingType, pingTag)
    local state = lib.pingState[key]
    if state == nil then
        local x, y = lib:GetMapPing(pingType, pingTag)
        state = (x ~= 0 or y ~= 0) and lib.MAP_PING_SET or lib.MAP_PING_NOT_SET
        lib.pingState[key] = state
    end
    return lib.pingState[key]
end
--- Returns true if ping state is MAP_PING_SET_PENDING or MAP_PING_SET
function lib:HasMapPing(pingType, pingTag)
    local state = lib:GetMapPingState(pingType, pingTag)
    return state == lib.MAP_PING_SET_PENDING or state == lib.MAP_PING_SET
end

--- Refreshes the pin icon for the pingType on the worldmap
--- Returns true if the pin has been refreshed.
function lib:RefreshMapPin(pingType, pingTag)
    if(not g_mapPinManager) then
        Log("PinManager not available. Using ZO_WorldMap_UpdateMap instead.")
        ZO_WorldMap_UpdateMap()
        return true
    end

    pingTag = pingTag or GetPingTagFromType(pingType)
    g_mapPinManager:RemovePins(PING_CATEGORY, pingType, pingTag)

    local x, y = lib:GetMapPing(pingType, pingTag)
    if(lib:IsPositionOnMap(x, y)) then
        g_mapPinManager:CreatePin(pingType, pingTag, x, y)
        return true
    end
    return false
end

--- Returns true if the normalized position is within 0 and 1.
function lib:IsPositionOnMap(x, y)
    return not (x < 0 or y < 0 or x > 1 or y > 1 or (x == 0 and y == 0))
end

--- Mutes the map ping of the specified type, so it does not make a sound when it is set.
--- Do not forget to call UnmutePing later, otherwise the sound will be permanently muted!
function lib:MutePing(pingType, pingTag)
    local key = GetKey(pingType, pingTag)
    local mute = lib.mutePing[key] or 0
    lib.mutePing[key] = mute + 1
end

--- Unmutes the map ping of the specified type.
--- Do not call this more often than you called MutePing, or you might interfere with other addons.
--- The sounds are played between the BeforePing* and AfterPing* callbacks are fired.
function lib:UnmutePing(pingType, pingTag)
    local key = GetKey(pingType, pingTag)
    local mute = (lib.mutePing[key] or 0) - 1
    if(mute < 0) then mute = 0 end
    lib.mutePing[key] = mute
end

--- Returns true if the map ping has been muted
function lib:IsPingMuted(pingType, pingTag)
    local key = GetKey(pingType, pingTag)
    return lib.mutePing[key] and lib.mutePing[key] > 0
end

--- Suppresses the map ping of the specified type, so that it neither makes a sound nor shows up on the map.
--- This also makes the API functions return 0, 0 for that ping.
--- In order to access the actual coordinates lib:GetMapPing has to be used.
--- Do not forget to call UnsuppressPing later, otherwise map pings won't work anymore for the user and other addons!
function lib:SuppressPing(pingType, pingTag)
    local key = GetKey(pingType, pingTag)
    local suppress = lib.suppressPing[key] or 0
    lib.suppressPing[key] = suppress + 1
end

--- Unsuppresses the map ping so it shows up again
--- Do not call this more often than you called SuppressPing, or you might interfere with other addons.
function lib:UnsuppressPing(pingType, pingTag)
    local key = GetKey(pingType, pingTag)
    local suppress = (lib.suppressPing[key] or 0) - 1
    if(suppress < 0) then suppress = 0 end
    lib.suppressPing[key] = suppress
end

--- Returns true if the map ping has been suppressed
function lib:IsPingSuppressed(pingType, pingTag)
    local key = GetKey(pingType, pingTag)
    return lib.suppressPing[key] and lib.suppressPing[key] > 0
end

local function InterceptMapPinManager()
    if (g_mapPinManager) then return end
    local orgRefreshCustomPins = ZO_WorldMapPins.RefreshCustomPins
    function ZO_WorldMapPins:RefreshCustomPins()
        g_mapPinManager = self
        lib.mapPinManager = self
    end
    ZO_WorldMap_RefreshCustomPinsOfType()
    ZO_WorldMapPins.RefreshCustomPins = orgRefreshCustomPins
end

--- Register to callbacks from the library.
--- Valid events are BeforePingAdded, AfterPingAdded, BeforePingRemoved and AfterPingRemoved.
--- These are fired at certain points during handling EVENT_MAP_PING.
function lib:RegisterCallback(eventName, callback)
    lib.cm:RegisterCallback(eventName, callback)
end

--- Unregister from callbacks. See lib:RegisterCallback.
function lib:UnregisterCallback(eventName, callback)
    lib.cm:UnregisterCallback(eventName, callback)
end

local function Unload()
    EVENT_MANAGER:UnregisterForEvent(LIB_IDENTIFIER, EVENT_ADD_ON_LOADED)
    EVENT_MANAGER:UnregisterForEvent(LIB_IDENTIFIER, EVENT_MAP_PING)
    PingMap = originalPingMap
    GetMapPlayerWaypoint = GET_MAP_PING_FUNCTION[MAP_PIN_TYPE_PLAYER_WAYPOINT]
    GetMapPing = GET_MAP_PING_FUNCTION[MAP_PIN_TYPE_PING]
    GetMapRallyPoint = GET_MAP_PING_FUNCTION[MAP_PIN_TYPE_RALLY_POINT]
    RemovePlayerWaypoint = originalRemovePlayerWaypoint
    RemoveRallyPoint = originalRemoveRallyPoint
end

local function Load()
    InterceptMapPinManager()

    originalPingMap = PingMap
    PingMap = CustomPingMap

    GET_MAP_PING_FUNCTION[MAP_PIN_TYPE_PLAYER_WAYPOINT] = GetMapPlayerWaypoint
    GET_MAP_PING_FUNCTION[MAP_PIN_TYPE_PING] = GetMapPing
    GET_MAP_PING_FUNCTION[MAP_PIN_TYPE_RALLY_POINT] = GetMapRallyPoint
    GetMapPlayerWaypoint = CustomGetMapPlayerWaypoint
    GetMapPing = CustomGetMapPing
    GetMapRallyPoint = CustomGetMapRallyPoint

    -- we want to use the altered versions in the library in order to set the correct ping state
    -- so we need to also save the originals
    originalRemovePlayerWaypoint = RemovePlayerWaypoint
    originalRemoveRallyPoint = RemoveRallyPoint
    RemovePlayerWaypoint = CustomRemovePlayerWaypoint
    RemoveRallyPoint = CustomRemoveRallyPoint
    REMOVE_MAP_PING_FUNCTION[MAP_PIN_TYPE_PLAYER_WAYPOINT] = CustomRemovePlayerWaypoint
    REMOVE_MAP_PING_FUNCTION[MAP_PIN_TYPE_PING] = CustomRemoveMapPing -- has no real api equivalent
    REMOVE_MAP_PING_FUNCTION[MAP_PIN_TYPE_RALLY_POINT] = CustomRemoveRallyPoint

    EVENT_MANAGER:RegisterForEvent(LIB_IDENTIFIER, EVENT_ADD_ON_LOADED, function(_, addonName)
        if(addonName == "ZO_Ingame") then
            EVENT_MANAGER:UnregisterForEvent(LIB_IDENTIFIER, EVENT_ADD_ON_LOADED)
            -- don't let worldmap do anything as we manage it instead
            EVENT_MANAGER:UnregisterForEvent("ZO_WorldMap", EVENT_MAP_PING)
            EVENT_MANAGER:RegisterForEvent(LIB_IDENTIFIER, EVENT_MAP_PING, HandleMapPing)
        end
    end)

    lib.Unload = Unload
end

if(lib.Unload) then lib.Unload() end
Load()

Compare with Previous | Blame