Go to most recent revision | Compare with Previous | Blame | View Log
local LIB_IDENTIFIER = "LibMapPing" local lib = LibStub:NewLibrary(LIB_IDENTIFIER, 5) 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) lib.pendingPing[key] = nil 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 = unpack(data) local pingTag = GetPingTagFromType(pingType) 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) 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) 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()