local function DefaultCompareNilable(first, second) if first and second then return first < second elseif first then return true end return false end ------------------ -- Pins Manager -- ------------------ ZO_WorldMapPins_Manager = ZO_ObjectPool:Subclass() function ZO_WorldMapPins_Manager:Initialize(parentControl) local mouseInputGroup = ZO_MouseInputGroup:New(parentControl) local function CreatePin(pool) local pin = ZO_MapPin:New(parentControl) mouseInputGroup:Add(pin:GetControl(), ZO_MOUSE_INPUT_GROUP_MOUSE_OVER) return pin end local function ResetPin(pin) self:OnMouseOverPinReset(pin) pin:Reset() end ZO_ObjectPool.Initialize(self, CreatePin, ResetPin) self.mouseInputGroup = mouseInputGroup -- Each of these tables holds a method of mapping pin lookup indices to the actual object pool keys needed to release the pins later -- The reason this exists is because the game events will hold info like "remove this specific quest index", and at that point we -- need to be able to lookup a pin for game-event data rather than pinTag data, without iterating over every single pin in the -- active objects list. self.m_keyToPinMapping = { ["poi"] = {}, -- { [zone index 1] = { [objective index 1] = pinKey1, [objective index 2] = pinKey2, ... }, ... } ["loc"] = {}, ["quest"] = {}, -- { [quest index 1] = { [quest pin tag 1] = pinKey1, [quest pin tag 2] = pinKey2, ... }, ... } ["objective"] = {}, ["keep"] = {}, ["pings"] = {}, ["killLocation"] = {}, ["fastTravelKeep"] = {}, ["fastTravelWayshrine"] = {}, ["forwardCamp"] = {}, ["AvARespawn"] = {}, ["group"] = {}, ["restrictedLink"] = {}, ["suggestion"] = {}, ["worldEventUnit"] = {}, ["worldEventPOI"] = {}, ["antiquityDigSite"] = {}, ["companion"] = {}, ["skyshard"] = {}, } self.nextCustomPinType = MAP_PIN_TYPE_INVALID self.customPins = {} self.objectiveMovingPins = {} self.movingPins = {} self.currentMouseOverPins = {} self.previousMouseOverPins = {} self.mousedOverPinWasReset = false self.mouseExitPins = {} self.foundTooltipMouseOverPins = {} self.pinBlobPool = ZO_ControlPool:New("ZO_PinBlob", parentControl, "PinBlob") self.pinBlobPool:SetCustomResetBehavior(function(blobControl) blobControl:SetAlpha(1) end) self.pinPolygonBlobPool = ZO_ControlPool:New("ZO_PinPolygonBlob", parentControl, "PinPolygonBlob") self.pinPolygonBlobPool:SetCustomFactoryBehavior(function(blobControl) mouseInputGroup:Add(blobControl, ZO_MOUSE_INPUT_GROUP_MOUSE_OVER) end) self.pinPolygonBlobPool:SetCustomResetBehavior(function(blobControl) blobControl:SetHandler("OnMouseUp", nil) blobControl:SetHandler("OnMouseDown", nil) blobControl:SetAlpha(1) end) self.writhingWallPinFXPool = ZO_ControlPool:New("ZO_WrithingWallMapPinFX", parentControl, "WrithingWallMapPinFX") self.writhingWallPinFXPool:SetUsePooledObjectWrapper(true) self.writhingWallPinFXPool:SetCustomResetBehavior(function(writhingWallPinFX) writhingWallPinFX:SetParent(parentControl) end) self.pinFadeInAnimationPool = ZO_AnimationPool:New("ZO_WorldMapPinFadeIn") local function OnAnimationTimelineStopped(timeline) self.pinFadeInAnimationPool:ReleaseObject(timeline.key) end local function SetupTimeline(timeline) timeline:SetHandler("OnStop", OnAnimationTimelineStopped) end self.pinFadeInAnimationPool:SetCustomFactoryBehavior(SetupTimeline) local function ResetTimeline(animationTimeline) local pinAnimation = animationTimeline:GetAnimation(1) pinAnimation:SetAnimatedControl(nil) local areaAnimation = animationTimeline:GetAnimation(2) areaAnimation:SetAnimatedControl(nil) end self.pinFadeInAnimationPool:SetCustomResetBehavior(ResetTimeline) --Wait until the map mode has been set before fielding these updates since adding a pin depends on the map having a mode. local OnWorldMapModeChanged OnWorldMapModeChanged = function(modeData) if modeData then WORLD_MAP_QUEST_BREADCRUMBS:RegisterCallback("QuestAvailable", function(...) self:OnQuestAvailable(...) end) WORLD_MAP_QUEST_BREADCRUMBS:RegisterCallback("QuestRemoved", function(...) self:OnQuestRemoved(...) end) CALLBACK_MANAGER:UnregisterCallback("OnWorldMapModeChanged", OnWorldMapModeChanged) end end CALLBACK_MANAGER:RegisterCallback("OnWorldMapModeChanged", OnWorldMapModeChanged) FOCUSED_QUEST_TRACKER:RegisterCallback("QuestTrackerAssistStateChanged", function(...) self:OnAssistStateChanged(...) end) end function ZO_WorldMapPins_Manager:AcquirePinBlob() return self.pinBlobPool:AcquireObject() end function ZO_WorldMapPins_Manager:ReleasePinBlob(pinBlobKey) self.pinBlobPool:ReleaseObject(pinBlobKey) end function ZO_WorldMapPins_Manager:AcquirePinPolygonBlob() return self.pinPolygonBlobPool:AcquireObject() end function ZO_WorldMapPins_Manager:ReleasePinPolygonBlob(pinBlobKey) self.pinPolygonBlobPool:ReleaseObject(pinBlobKey) end function ZO_WorldMapPins_Manager:AcquireWrithingWallPinFX() return self.writhingWallPinFXPool:AcquireObject() end function ZO_WorldMapPins_Manager:AcquirePinFadeInAnimation() local animation, key = self.pinFadeInAnimationPool:AcquireObject() animation.key = key return animation end function ZO_WorldMapPins_Manager:OnQuestAvailable(questIndex) self:AddQuestPin(questIndex) end function ZO_WorldMapPins_Manager:OnQuestRemoved(questIndex) self:RemovePins("quest", questIndex) local questPingData = ZO_WorldMap_GetQuestPingData() if questPingData and questPingData.questIndex == questIndex then self:RemovePins("pings", MAP_PIN_TYPE_QUEST_PING) end end function ZO_WorldMapPins_Manager:OnAssistStateChanged(unassistedData, assistedData) if unassistedData then self:SetQuestPinsAssisted(unassistedData:GetJournalIndex(), false) end if assistedData then self:SetQuestPinsAssisted(assistedData:GetJournalIndex(), true) end WORLD_MAP_MANAGER:MarkPinTooltipDirty() end function ZO_WorldMapPins_Manager:SetQuestPinsAssisted(questIndex, assisted) local pins = self:GetActiveObjects() for pinKey, pin in pairs(pins) do local currentIndex = pin:GetQuestIndex() if currentIndex == questIndex then local currentPinType = pin:GetPinType() local trackingLevel = GetTrackingLevel(TRACK_TYPE_QUEST, questIndex) local newPinType = GetQuestPinTypeForTrackingLevel(currentPinType, trackingLevel) pin:ChangePinType(newPinType) end end end function ZO_WorldMapPins_Manager:PingQuest(questIndex, animation) local pins = self:GetActiveObjects() for pinKey, pin in pairs(pins) do local pinQuestIndex = pin:GetQuestIndex() if pinQuestIndex > -1 then if pinQuestIndex == questIndex then pin:PingMapPin(animation) else pin:ResetAnimation(ZO_MapPin.ANIM_CONSTANTS.RESET_ANIM_HIDE_CONTROL) end end end end do local GLOBAL_MAPS = { [MAPTYPE_WORLD] = true, [MAPTYPE_COSMIC] = true, } function ZO_WorldMapPins_Manager.IsCurrentMapGlobal() return GLOBAL_MAPS[GetMapType()] end end function ZO_WorldMapPins_Manager:AddQuestPin(questIndex) if not ZO_WorldMap_IsPinGroupShown(MAP_FILTER_QUESTS) then return end local questSteps = WORLD_MAP_QUEST_BREADCRUMBS:GetSteps(questIndex) if questSteps then local NO_RADIUS = nil local NO_BORDER_INFO = nil local IS_SYMBOLIC_LOCATION = true for stepIndex, questConditions in pairs(questSteps) do for conditionIndex, conditionData in pairs(questConditions) do local xLoc, yLoc = conditionData.xLoc, conditionData.yLoc if conditionData.insideCurrentMapWorld and ZO_WorldMap_IsNormalizedPointInsideMapBounds(xLoc, yLoc) then local tag = ZO_MapPin.CreateQuestPinTag(questIndex, stepIndex, conditionIndex) local areaRadius = self.IsCurrentMapGlobal() and 0 or conditionData.areaRadius tag.isBreadcrumb = conditionData.isBreadcrumb if not self.IsCurrentMapGlobal() or (FOCUSED_QUEST_TRACKER:IsOnTracker(TRACK_TYPE_QUEST, questIndex) and not IsZoneStoryAssisted()) then local pin = self:CreatePin(conditionData.pinType, tag, xLoc, yLoc, areaRadius, NO_BORDER_INFO, conditionData.symbolicState == QUEST_PIN_STATE_IS_SYMBOLIC_POSITION) if conditionData.symbolicState == QUEST_PIN_STATE_HAS_ADDITIONAL_SYMBOLIC_POSITION then local additionalSymbolicLocX, additionalSymbolicLocY = conditionData.additionalSymbolicLocX, conditionData.additionalSymbolicLocY local questPinTag = ZO_MapPin.CreateQuestPinTag(questIndex, stepIndex, conditionIndex) self:CreatePin(conditionData.pinType, questPinTag, additionalSymbolicLocX, additionalSymbolicLocY, NO_RADIUS, NO_BORDER_INFO, IS_SYMBOLIC_LOCATION) end if pin:DoesQuestDataMatchQuestPingData() then local questPinTag = ZO_MapPin.CreateQuestPinTag(questIndex, stepIndex, conditionIndex) self:CreatePin(MAP_PIN_TYPE_QUEST_PING, questPinTag, xLoc, yLoc, NO_RADIUS, NO_BORDER_INFO, conditionData.symbolicState == QUEST_PIN_STATE_IS_SYMBOLIC_POSITION) end end elseif conditionData.symbolicState == QUEST_PIN_STATE_HAS_ADDITIONAL_SYMBOLIC_POSITION then local additionalSymbolicLocX, additionalSymbolicLocY = conditionData.additionalSymbolicLocX, conditionData.additionalSymbolicLocY local questPinTag = ZO_MapPin.CreateQuestPinTag(questIndex, stepIndex, conditionIndex) self:CreatePin(conditionData.pinType, questPinTag, additionalSymbolicLocX, additionalSymbolicLocY, NO_RADIUS, NO_BORDER_INFO, IS_SYMBOLIC_LOCATION) end end end end end function ZO_WorldMapPins_Manager:GetNextCustomPinType() self.nextCustomPinType = self.nextCustomPinType + 1 return self.nextCustomPinType end function ZO_WorldMapPins_Manager:CreateCustomPinType(pinTypeString) local pinTypeId = self:GetNextCustomPinType() _G[pinTypeString] = pinTypeId return pinTypeId end function ZO_WorldMapPins_Manager:AddCustomPin(pinTypeString, pinTypeAddCallback, pinTypeOnResizeCallback, pinLayoutData, pinTooltipCreator) if _G[pinTypeString] ~= nil then return end local pinTypeId = self:CreateCustomPinType(pinTypeString) self.m_keyToPinMapping[pinTypeString] = {} self.customPins[pinTypeId] = { enabled = false, layoutCallback = pinTypeAddCallback, resizeCallback = pinTypeOnResizeCallback, pinTypeString = pinTypeString } ZO_MapPin.TOOLTIP_CREATORS[pinTypeId] = pinTooltipCreator ZO_MapPin.PIN_DATA[pinTypeId] = pinLayoutData end function ZO_WorldMapPins_Manager:SetCustomPinEnabled(pinType, enabled) local pinData = self.customPins[pinType] if pinData then pinData.enabled = enabled end end function ZO_WorldMapPins_Manager:IsCustomPinEnabled(pinType) local pinData = self.customPins[pinType] if pinData then return pinData.enabled end end function ZO_WorldMapPins_Manager:RefreshCustomPins(optionalPinType) for pinTypeId, pinData in pairs(self.customPins) do if optionalPinType == nil or optionalPinType == pinTypeId then self:RemovePins(pinData.pinTypeString) if pinData.enabled then pinData.layoutCallback(self) end end end end function ZO_WorldMapPins_Manager:MapPinLookupToPinKey(lookupType, majorIndex, keyIndex, pinKey) local lookupTable = self.m_keyToPinMapping[lookupType] local keys = lookupTable[majorIndex] if not keys then keys = {} lookupTable[majorIndex] = keys end keys[keyIndex] = pinKey end do local SYMBOLIC_PIN_SIZE = 28 local function SortGroupPinsFunction(pinA, pinB) local pinTypeA = pinA:GetPinType() local pinTypeB = pinB:GetPinType() if pinTypeA == pinTypeB then return pinA:GetPinId() < pinB:GetPinId() end if pinA:IsQuest() and pinB:IsQuest() then return pinTypeA < pinTypeB elseif pinA:IsQuest() then return true elseif pinB:IsQuest() then return false end if pinTypeA == MAP_PIN_TYPE_PLAYER then return true elseif pinTypeB == MAP_PIN_TYPE_PLAYER then return false end if pinA:IsGroup() and pinB:IsGroup() then return pinTypeA == MAP_PIN_TYPE_GROUP_LEADER elseif pinA:IsGroup() then return false elseif pinB:IsGroup() then return true end return pinTypeA < pinTypeB end local PIN_TYPE_QUEST = 1 local PIN_TYPE_PLAYER = 2 local PIN_TYPE_GROUP = 3 local function IsPlayerPin(pin) return pin:GetPinType() == MAP_PIN_TYPE_PLAYER end local function IsQuestPin(pin) return pin:IsQuest() or pin:GetPinType() == MAP_PIN_TYPE_QUEST_PING end local function IsGroupPin(pin) return pin:IsGroup() end local PIN_TYPE_FUNCTION = { [PIN_TYPE_QUEST] = IsQuestPin, [PIN_TYPE_PLAYER] = IsPlayerPin, [PIN_TYPE_GROUP] = IsGroupPin, } local function GetPinOverlapMultiplierX(pinIndex, totalPins) return ((totalPins - 1) * -0.5) + (pinIndex - 1) end local function GetPinOverlapMultiplierY(pinIndex, totalPins) return 0.5 end function ZO_WorldMapPins_Manager:UpdateSymbolicPins() local symbolicPinsByLocation = {} for _, activePin in self:ActiveObjectIterator() do if activePin:IsSymbolicPosition() then local locationFound = false local originalX, originalY = activePin:GetOriginalPosition() for _, locationPins in ipairs(symbolicPinsByLocation) do if locationPins.x == originalX and locationPins.y == originalY then table.insert(locationPins.activePins, activePin) locationFound = true end end if not locationFound then local locationData = { x = originalX, y = originalY, activePins = { activePin }, } table.insert(symbolicPinsByLocation, locationData) end end end for _, locationData in pairs(symbolicPinsByLocation) do table.sort(locationData.activePins, SortGroupPinsFunction) local totalPinTypes = 0 local pinsByType = {} for locationIndex, activePin in ipairs(locationData.activePins) do for pinTypeIndex = PIN_TYPE_QUEST, PIN_TYPE_GROUP do if PIN_TYPE_FUNCTION[pinTypeIndex](activePin) then if not pinsByType[pinTypeIndex] then pinsByType[pinTypeIndex] = {} totalPinTypes = totalPinTypes + 1 end table.insert(pinsByType[pinTypeIndex], activePin) end end end local index = 1 for pinType, typePins in pairs(pinsByType) do local offsetMultiplierX = GetPinOverlapMultiplierX(index, totalPinTypes) local offsetMultiplierY = GetPinOverlapMultiplierY(index, totalPinTypes) local isFirstOfTypeShown = false for i, pin in ipairs(typePins) do pin:SetOverlapOffsets(offsetMultiplierX * SYMBOLIC_PIN_SIZE, offsetMultiplierY * SYMBOLIC_PIN_SIZE) pin:UpdateLocation() if not isFirstOfTypeShown then pin:GetControl():SetAlpha(1) isFirstOfTypeShown = true elseif pinType == PIN_TYPE_GROUP then pin:GetControl():SetAlpha(0) else pin:GetControl():SetAlpha(1) end end index = index + 1 end end end function ZO_WorldMapPins_Manager:CreatePin(pinType, pinTag, xLoc, yLoc, radius, borderInformation, isSymbolicLoc) local pin, pinKey = self:AcquireObject() pin:SetData(pinType, pinTag) pin:SetOriginalPosition(xLoc, yLoc) pin:SetIsSymbolicPosition(isSymbolicLoc) pin:SetLocation(xLoc, yLoc, radius, borderInformation) self:UpdateSymbolicPins() if pinType == MAP_PIN_TYPE_PLAYER then pin:PingMapPin(ZO_MapPin.PulseAnimation) self.playerPin = pin end if not pin:ValidatePvPPinAllowed() then self:ReleaseObject(pinKey) return end if pin:IsPOI() then self:MapPinLookupToPinKey("poi", pin:GetPOIZoneIndex(), pin:GetPOIIndex(), pinKey) elseif pin:IsLocation() then self:MapPinLookupToPinKey("loc", pin:GetLocationIndex(), pin:GetLocationIndex(), pinKey) elseif pin:IsQuest() then self:MapPinLookupToPinKey("quest", pin:GetQuestIndex(), pinTag, pinKey) elseif pin:IsObjective() then self:MapPinLookupToPinKey("objective", pin:GetObjectiveKeepId(), pinTag, pinKey) elseif pin:IsKeepOrDistrict() then self:MapPinLookupToPinKey("keep", pin:GetKeepId(), pin:IsUnderAttackPin(), pinKey) elseif pin:IsMapPing() then self:MapPinLookupToPinKey("pings", pinType, pinTag, pinKey) elseif pin:IsKillLocation() then self:MapPinLookupToPinKey("killLocation", pinType, pinTag, pinKey) elseif pin:IsFastTravelKeep() then self:MapPinLookupToPinKey("fastTravelKeep", pin:GetFastTravelKeepId(), pin:GetFastTravelKeepId(), pinKey) elseif pin:IsFastTravelWayShrine() then self:MapPinLookupToPinKey("fastTravelWayshrine", pinType, pinTag, pinKey) elseif pin:IsForwardCamp() then self:MapPinLookupToPinKey("forwardCamp", pinType, pinTag, pinKey) elseif pin:IsAvARespawn() then self:MapPinLookupToPinKey("AvARespawn", pinType, pinTag, pinKey) elseif pin:IsGroup() then self:MapPinLookupToPinKey("group", pinType, pinTag, pinKey) elseif pin:IsRestrictedLink() then self:MapPinLookupToPinKey("restrictedLink", pinType, pinTag, pinKey) elseif pin:IsSuggestion() then self:MapPinLookupToPinKey("suggestion", pinType, pinTag, pinKey) elseif pin:IsWorldEventUnitPin() then self:MapPinLookupToPinKey("worldEventUnit", pin:GetWorldEventInstanceId(), pin:GetUnitTag(), pinKey) elseif pin:IsWorldEventPOIPin() then self:MapPinLookupToPinKey("worldEventPOI", pin:GetWorldEventInstanceId(), pinTag, pinKey) elseif pin:IsAntiquityDigSitePin() then self:MapPinLookupToPinKey("antiquityDigSite", pinType, pinTag, pinKey) elseif pin:IsCompanion() then self:MapPinLookupToPinKey("companion", pinType, pinTag, pinKey) elseif pin:IsSkyshard() then self:MapPinLookupToPinKey("skyshard", pinType, pinTag, pinKey) else local customPinData = self.customPins[pinType] if customPinData then self:MapPinLookupToPinKey(customPinData.pinTypeString, pinType, pinTag, pinKey) end end ZO_WorldMap_GetPanAndZoom():OnPinCreated() return pin end end function ZO_WorldMapPins_Manager:FindPin(lookupType, majorIndex, keyIndex) local lookupTable = self.m_keyToPinMapping[lookupType] local keys if majorIndex then keys = lookupTable[majorIndex] else keys = select(2, next(lookupTable)) end if keys then local pinKey if keyIndex then pinKey = keys[keyIndex] else pinKey = select(2, next(keys)) end if pinKey then return self:GetActiveObject(pinKey) end end end function ZO_WorldMapPins_Manager:AddPinsToArray(pins, lookupType, majorIndex) local lookupTable = self.m_keyToPinMapping[lookupType] local function AddPinsForKeys(keysTable) if keysTable then for _, pinKey in pairs(keysTable) do local pin = self:GetActiveObject(pinKey) if pin then table.insert(pins, pin) end end end end if majorIndex then local keys = lookupTable[majorIndex] AddPinsForKeys(keys) else for _, keys in pairs(lookupTable) do AddPinsForKeys(keys) end end return pins end function ZO_WorldMapPins_Manager:RemovePins(lookupType, majorIndex, keyIndex) local lookupTable = self.m_keyToPinMapping[lookupType] if majorIndex then local keys = lookupTable[majorIndex] if keys then if keyIndex then --Remove a specific pin local pinKey = keys[keyIndex] if pinKey then self:ReleaseObject(pinKey) keys[keyIndex] = nil end else --Remove all pins in the major index for _, pinKey in pairs(keys) do self:ReleaseObject(pinKey) end self.m_keyToPinMapping[lookupType][majorIndex] = {} end end else --Remove all pins of the lookup type for _, keys in pairs(lookupTable) do for _, pinKey in pairs(keys) do self:ReleaseObject(pinKey) end end self.m_keyToPinMapping[lookupType] = {} end end function ZO_WorldMapPins_Manager:UpdatePinsForMapSizeChange() local pins = self:GetActiveObjects() for pinKey, pin in pairs(pins) do pin:UpdateLocation() pin:UpdateSize() end for pinTypeId, pinData in pairs(self.customPins) do if pinData.enabled and pinData.resizeCallback then pinData.resizeCallback(self, ZO_MAP_CONSTANTS.MAP_WIDTH, ZO_MAP_CONSTANTS.MAP_HEIGHT) end end end function ZO_WorldMapPins_Manager:GetWayshrinePin(nodeIndex) local pins = self:GetActiveObjects() for pinKey, pin in pairs(pins) do local currentIndex = pin:GetFastTravelNodeIndex() if currentIndex == nodeIndex then return pin end end end function ZO_WorldMapPins_Manager:GetQuestConditionPin(questIndex) local pins = self:GetActiveObjects() for pinKey, pin in pairs(pins) do local currentIndex = pin:GetQuestIndex() if currentIndex == questIndex then return pin end end end function ZO_WorldMapPins_Manager:GetPlayerPin() return self.playerPin end do local function IsMapShowingBattlegroundContext(bgContext) local _, queryType = ZO_WorldMap_GetCampaign() return (queryType == BGQUERY_LOCAL and IsLocalBattlegroundContext(bgContext)) or (queryType == BGQUERY_ASSIGNED_CAMPAIGN and IsAssignedBattlegroundContext(bgContext)) end function ZO_WorldMapPins_Manager:RefreshObjectives() self:RemovePins("objective") ZO_ClearNumericallyIndexedTable(self.objectiveMovingPins) local mapFilterType = GetMapFilterType() if mapFilterType ~= MAP_FILTER_TYPE_AVA_CYRODIIL and mapFilterType ~= MAP_FILTER_TYPE_BATTLEGROUND then return end local numObjectives = GetNumObjectives() local worldMapAvAPinsShown = ZO_WorldMap_IsPinGroupShown(MAP_FILTER_AVA_OBJECTIVES) for i = 1, numObjectives do local keepId, objectiveId, bgContext = GetObjectiveIdsForIndex(i) local isEnabled = IsObjectiveEnabled(keepId, objectiveId, bgContext) if isEnabled then local isVisible = IsObjectiveObjectVisible(keepId, objectiveId, bgContext) if ZO_WorldMap_IsObjectiveShown(keepId, objectiveId, bgContext) and IsMapShowingBattlegroundContext(bgContext) then --spawn locations local spawnPinType, spawnX, spawnY = GetObjectiveSpawnPinInfo(keepId, objectiveId, bgContext) if spawnPinType ~= MAP_PIN_TYPE_INVALID then if worldMapAvAPinsShown then if ZO_WorldMap_IsNormalizedPointInsideMapBounds(spawnX, spawnY) then local spawnTag = ZO_MapPin.CreateObjectivePinTag(keepId, objectiveId, bgContext) self:CreatePin(spawnPinType, spawnTag, spawnX, spawnY) end end end --return locations local returnPinType, returnX, returnY, returnContinuousUpdate = GetObjectiveReturnPinInfo(keepId, objectiveId, bgContext) if returnPinType ~= MAP_PIN_TYPE_INVALID then local returnTag = ZO_MapPin.CreateObjectivePinTag(keepId, objectiveId, bgContext) local returnPin = self:CreatePin(returnPinType, returnTag, returnX, returnY) if returnContinuousUpdate then table.insert(self.objectiveMovingPins, returnPin) end end -- current locations local pinType, currentX, currentY, continuousUpdate = GetObjectivePinInfo(keepId, objectiveId, bgContext) if isVisible and pinType ~= MAP_PIN_TYPE_INVALID then if worldMapAvAPinsShown then if ZO_WorldMap_IsNormalizedPointInsideMapBounds(currentX, currentY) then local objectiveTag = ZO_MapPin.CreateObjectivePinTag(keepId, objectiveId, bgContext) local objectivePin = self:CreatePin(pinType, objectiveTag, currentX, currentY) if objectivePin then local auraPinType = GetObjectiveAuraPinInfo(keepId, objectiveId, bgContext) local auraPin if auraPinType ~= MAP_PIN_TYPE_INVALID then local auraTag = ZO_MapPin.CreateObjectivePinTag(keepId, objectiveId, bgContext) auraPin = self:CreatePin(auraPinType, auraTag, currentX, currentY) objectivePin:AddScaleChild(auraPin) end if continuousUpdate then table.insert(self.objectiveMovingPins, objectivePin) if auraPin then table.insert(self.objectiveMovingPins, auraPin) end end end end end end end end end end end function ZO_WorldMapPins_Manager:RefreshGroupPins() self:RemovePins("group") if ZO_WorldMap_IsPinGroupShown(MAP_FILTER_GROUP_MEMBERS) then local isInDungeon = GetMapContentType() == MAP_CONTENT_DUNGEON local isInHouse = GetCurrentZoneHouseId() ~= 0 for i = 1, MAX_GROUP_SIZE_THRESHOLD do local groupTag = ZO_Group_GetUnitTagForGroupIndex(i) local isBreadcrumbed = IsUnitWorldMapPositionBreadcrumbed(groupTag) if DoesUnitExist(groupTag) and not AreUnitsEqual("player", groupTag) and IsUnitOnline(groupTag) then local isGroupMemberHiddenByInstance = false -- If we're in an instance and it has its own map, it's going to be a dungeon map or house. Don't show on the map if we're on different instances/layers -- If it doesn't have its own map, we're okay to show the group member regardless of instance if DoesCurrentMapMatchMapForPlayerLocation() and IsGroupMemberInSameWorldAsPlayer(groupTag) and (isInDungeon or isInHouse) then if not IsGroupMemberInSameInstanceAsPlayer(groupTag) then -- We're in the same world as the group member, but a different instance isGroupMemberHiddenByInstance = true elseif not IsGroupMemberInSameLayerAsPlayer(groupTag) then -- We're in the same instance as the group member, but a different layer isGroupMemberHiddenByInstance = not isBreadcrumbed end end if not isGroupMemberHiddenByInstance then local x, y, _, isInCurrentMap, isSymbolicLocation = GetMapPlayerPosition(groupTag) if isInCurrentMap then local isLeader = IsUnitGroupLeader(groupTag) local tagData = groupTag if isBreadcrumbed then tagData = { groupTag = groupTag, isBreadcrumb = true } end local NO_RADIUS = nil local NO_BORDER_INFO = nil self:CreatePin(isLeader and MAP_PIN_TYPE_GROUP_LEADER or MAP_PIN_TYPE_GROUP, tagData, x, y, NO_RADIUS, NO_BORDER_INFO, isSymbolicLocation) end end end end end end function ZO_WorldMapPins_Manager:UpdateMovingPins() do local playerPin = self:GetPlayerPin() local xLoc, yLoc, _, isShownInCurrentMap, isSymbolicLocation = GetMapPlayerPosition("player") playerPin:SetOriginalPosition(xLoc, yLoc) playerPin:SetIsSymbolicPosition(isSymbolicLocation) playerPin:SetLocation(xLoc, yLoc) if isShownInCurrentMap then playerPin:SetHidden(false) local rotation = isSymbolicLocation and 0 or GetPlayerCameraHeading() playerPin:SetRotation(rotation) else playerPin:SetHidden(true) end end local movingPins = self.movingPins self:AddPinsToArray(movingPins, "group") for _, groupPin in ipairs(movingPins) do local xLoc, yLoc = GetMapPlayerPosition(groupPin:GetUnitTag()) groupPin:SetLocation(xLoc, yLoc) end ZO_ClearNumericallyIndexedTable(movingPins) local companionPin = self:FindPin("companion") if companionPin then local xLoc, yLoc = GetMapPlayerPosition(companionPin:GetUnitTag()) companionPin:SetLocation(xLoc, yLoc) end for _, objectivePin in ipairs(self.objectiveMovingPins) do local _, currentX, currentY = GetObjectivePinInfo(objectivePin:GetObjectiveKeepId(), objectivePin:GetObjectiveObjectiveId(), objectivePin:GetBattlegroundContext()) objectivePin:SetLocation(currentX, currentY) end self:AddPinsToArray(movingPins, "worldEventUnit") for _, worldEventPin in ipairs(movingPins) do local xLoc, yLoc = GetMapPlayerPosition(worldEventPin:GetUnitTag()) worldEventPin:SetLocation(xLoc, yLoc) end ZO_ClearNumericallyIndexedTable(movingPins) self:UpdateSymbolicPins() end --[[ Utilities to build lists of pins the mouse is currently over and was previously over so the world map knows how to properly call the OnMouseExit and OnMouseEnter events on the pins. --]] function ZO_WorldMapPins_Manager:BuildMouseOverPinLists(cursorPositionX, cursorPositionY) -- Determine if the mouse is even over the world map local isMouseOverWorldMap = ZO_WorldMap_IsMouseOverMap() -- Swap lists self.previousMouseOverPins, self.currentMouseOverPins = self.currentMouseOverPins, self.previousMouseOverPins local currentMouseOverPins = self.currentMouseOverPins -- Update any pins that were moused over in the current list that may no longer be in the active pins for pin, isMousedOver in pairs(currentMouseOverPins) do if isMousedOver then currentMouseOverPins[pin] = isMouseOverWorldMap and pin:MouseIsOver(cursorPositionX, cursorPositionY) end end local stickyPin = ZO_WorldMap_GetStickyPin() -- Update active list and determine the sticky pin! stickyPin:ClearNearestCandidate() local pins = self:GetActiveObjects() for _, pin in pairs(pins) do currentMouseOverPins[pin] = isMouseOverWorldMap and pin:MouseIsOver(cursorPositionX, cursorPositionY) stickyPin:ConsiderPin(pin, cursorPositionX, cursorPositionY) end stickyPin:SetStickyPinFromNearestCandidate() -- Determine which pins need to have their mouse enter called and which need to have their mouse exit called. -- Return whether or not the lists for current and previous changed so that nothing is updated unecessarily local wasPreviouslyMousedOver, doMouseEnter, doMouseExit local listsChanged = false local needsContinuousTooltipUpdates = false for pin, isMousedOver in pairs(currentMouseOverPins) do wasPreviouslyMousedOver = self.previousMouseOverPins[pin] doMouseEnter = isMousedOver and not wasPreviouslyMousedOver doMouseExit = not isMousedOver and wasPreviouslyMousedOver self.mouseExitPins[pin] = doMouseExit listsChanged = listsChanged or doMouseEnter or doMouseExit needsContinuousTooltipUpdates = needsContinuousTooltipUpdates or (isMousedOver and pin:NeedsContinuousTooltipUpdates()) end return listsChanged, needsContinuousTooltipUpdates end function ZO_WorldMapPins_Manager:ShouldDoMouseExitForPin(pin) return self.mouseExitPins[pin] end -- Addon compatibility function ZO_WorldMapPins_Manager:DoMouseExitForPin(pin) WORLD_MAP_MANAGER:DoMouseExitForPin(pinType) end function ZO_WorldMapPins_Manager:ResetMouseOverPins() for pin, mousedOver in pairs(self.currentMouseOverPins) do if mousedOver then pin:SetTargetScale(1) end end ZO_ClearTable(self.currentMouseOverPins) ZO_ClearTable(self.previousMouseOverPins) end function ZO_WorldMapPins_Manager:OnMouseOverPinReset(pin) if self.currentMouseOverPins[pin] then self.mousedOverPinWasReset = true WORLD_MAP_MANAGER:DoMouseExitForPin(pin) end self.currentMouseOverPins[pin] = nil self.previousMouseOverPins[pin] = nil --If we are showing a menu to choose a pin action and one of those pins is removed from the map then we need to handle that here WORLD_MAP_CHOICE_DIALOG_GAMEPAD:OnPinRemovedFromMap(pin) if ZO_MapPin.pinsInKeyboardMapChoiceDialog and ZO_MapPin.pinsInKeyboardMapChoiceDialog[pin] then ClearMenu() end end function ZO_WorldMapPins_Manager:GetCurrentMouseOverPins() return self.currentMouseOverPins end function ZO_WorldMapPins_Manager:UpdateMouseOverPins() local cursorPositionX local cursorPositionY if SCENE_MANAGER:IsCurrentSceneGamepad() then cursorPositionX, cursorPositionY = ZO_WorldMapScroll:GetCenter() else cursorPositionX, cursorPositionY = GetUIMousePosition() end local mouseOverListChanged, needsContinuousTooltipUpdates = self:BuildMouseOverPinLists(cursorPositionX, cursorPositionY) local mouseOverPinsChanged = mouseOverListChanged or self.mousedOverPinWasReset self.mousedOverPinWasReset = false return mouseOverPinsChanged, needsContinuousTooltipUpdates end --[[ Pin Click Handlers --]] do local function GetShownHandlersForPin(pin, mouseButton) if pin and ZO_MapPin.PIN_CLICK_HANDLERS[mouseButton] then local handlers = ZO_MapPin.PIN_CLICK_HANDLERS[mouseButton][pin:GetPinType()] if handlers then for i = 1, #handlers do local handler = handlers[i] if handler.show == nil or handler.show(pin) then if handler.GetDynamicHandlers then return handler.GetDynamicHandlers(pin) else return { handler } end end end end end return nil end local function GetFirstShownHandlerForPin(pin, mouseButton) local handlers = GetShownHandlersForPin(pin, mouseButton) if handlers then return handlers[1] end return nil end function ZO_WorldMapPins_Manager:WouldPinHandleClick(pinControl, button, ctrl, alt, shift) if ctrl or alt then return false end if pinControl then local pin = ZO_MapPin.GetMapPinForControl(pinControl) local validPinHandler = GetFirstShownHandlerForPin(pin, button) if validPinHandler then return true end end for pin, isMousedOver in pairs(self.currentMouseOverPins) do if isMousedOver then local validHandler = GetFirstShownHandlerForPin(pin, button) if validHandler then return true end end end end function ZO_WorldMapPins_Manager:GetPinHandlers(mouseButton) local pinDatas = ZO_MapPin.pinDatas ZO_ClearNumericallyIndexedTable(pinDatas) for pin, isMousedOver in pairs(self.currentMouseOverPins) do if isMousedOver then local shownHandlers = GetShownHandlersForPin(pin, mouseButton) if shownHandlers then for _, handler in ipairs(shownHandlers) do local duplicate = false local duplicatesFunction = handler.duplicates if duplicatesFunction then for _, pinData in ipairs(pinDatas) do --if these handlers are of the same type if handler == pinData.handler then if duplicatesFunction(pin, pinData.pin) then duplicate = true break end end end end if not duplicate then table.insert(pinDatas, {handler = handler, pin = pin}) end end end end end return pinDatas end end function ZO_WorldMapPins_Manager.ChoosePinOption(pin, handler) if handler.show and not handler.show(pin) then --If something changed and we shouldn't be showing this option anymore then... if handler.failedAfterBeingShownError then --If we have some error text for this case then show it in a dialog local text if type(handler.failedAfterBeingShownError) == "function" then text = handler.failedAfterBeingShownError(pin) else text = handler.failedAfterBeingShownError end ZO_Dialogs_ShowPlatformDialog("WORLD_MAP_CHOICE_FAILED", nil, { mainTextParams = { text } }) end --Then skip doing the action return end handler.callback(pin) end do local function SortPinDatas(firstData, secondData) local firstEntryName = ZO_Eval(firstData.handler.name, firstData.pin) local secondEntryName = ZO_Eval(secondData.handler.name, secondData.pin) local compareResult = DefaultCompareNilable(firstEntryName, secondEntryName) if compareResult ~= nil then return compareResult end return false end local function OnMenuHiddenCallback() ZO_MapPin.pinsInKeyboardMapChoiceDialog = nil end function ZO_WorldMapPins_Manager:HandlePinClicked(pinControl, mouseButton, ctrl, alt, shift) if ctrl or alt then return end local pinDatas = self:GetPinHandlers(mouseButton) self:RemoveInvalidSpawnLocations(pinDatas) if #pinDatas == 1 then pinDatas[1].handler.callback(pinDatas[1].pin) elseif #pinDatas > 1 then if IsInGamepadPreferredMode() then ZO_Dialogs_ShowGamepadDialog("WORLD_MAP_CHOICE_GAMEPAD", { mouseOverPinHandlers = pinDatas }) else ClearMenu() ZO_MapPin.pinsInKeyboardMapChoiceDialog = { } table.sort(pinDatas, SortPinDatas) for _, pinData in ipairs(pinDatas) do local handler = pinData.handler local pin = pinData.pin local name = handler.name if type(name) == "function" then name = name(pin) end AddMenuItem(name, function() self.ChoosePinOption(pin, handler) end) ZO_MapPin.pinsInKeyboardMapChoiceDialog[pin] = true end SetMenuHiddenCallback(OnMenuHiddenCallback) ShowMenu(pinControl) end end end end function ZO_WorldMapPins_Manager:RemoveInvalidSpawnLocations(pinDatas) for i, pinData in ZO_NumericallyIndexedTableReverseIterator(pinDatas) do if ZO_MapPin.IsReviveLocation(pinData.handler) and not ZO_MapPin.CanReviveAtPin(pinData.pin, pinData.handler) then table.remove(pinDatas, i) end end end ----------------------- -- Locations Manager -- ----------------------- -- Set up the place names text that appears on the map... ZO_MapLocationPins_Manager = ZO_ControlPool:Subclass() function ZO_MapLocationPins_Manager:Initialize(container) ZO_ControlPool.Initialize(self, "ZO_MapLocation", container, "Landmark") self.m_minFontSize = 17 self.m_maxFontSize = 32 self.m_cachedFontStrings = {} self:SetFontScale(1) end function ZO_MapLocationPins_Manager:SetFontScale(scale) if scale ~= self.m_fontScale then self.m_fontScale = scale self.m_cachedFontStrings = {} end end function ZO_MapLocationPins_Manager:GetFontString(size) -- apply scale to the (unscaled) input size, clamp it, and arive at final font string. -- unscale by global ui scale because we want the font to get a little bigger at smaller ui scales to approximately cover the same map area... local fontString = self.m_cachedFontStrings[size] if not fontString then fontString = string.format("$(BOLD_FONT)|%d|soft-shadow-thin", zo_round(size / GetUIGlobalScale())) self.m_cachedFontStrings[size] = fontString end return fontString end function ZO_MapLocationPins_Manager:AddLocation(locationIndex) if IsMapLocationVisible(locationIndex) then local icon, x, y = GetMapLocationIcon(locationIndex) if icon ~= "" and ZO_WorldMap_IsNormalizedPointInsideMapBounds(x, y) then local tag = ZO_MapPin.CreateLocationPinTag(locationIndex, icon) ZO_WorldMap_GetPinManager():CreatePin(MAP_PIN_TYPE_LOCATION, tag, x, y) end end end function ZO_MapLocationPins_Manager:RefreshLocations() self:ReleaseAllObjects() ZO_WorldMap_GetPinManager():RemovePins("loc") for i = 1, GetNumMapLocations() do self:AddLocation(i) end end ---------------- -- Sticky Pin -- ---------------- --[[ Sticky Pin Utilities for gamepad map control (utilized by mouse over list construction) --]] local BASE_STICKY_DISTANCE_UNITS = { MIN = 50, MAX = 75, } ZO_WorldMapStickyPin = ZO_InitializingObject:Subclass() function ZO_WorldMapStickyPin:Initialize() self.thresholdDistanceSq = BASE_STICKY_DISTANCE_UNITS.MIN * BASE_STICKY_DISTANCE_UNITS.MIN self.enabled = true end function ZO_WorldMapStickyPin:SetEnabled(enabled) self.enabled = enabled end function ZO_WorldMapStickyPin:UpdateThresholdDistance(currentNormalizedZoom) local stickyDistance = zo_lerp(BASE_STICKY_DISTANCE_UNITS.MIN, BASE_STICKY_DISTANCE_UNITS.MAX, currentNormalizedZoom) self.thresholdDistanceSq = stickyDistance * stickyDistance end function ZO_WorldMapStickyPin:SetStickyPin(pin) self.pin = pin end function ZO_WorldMapStickyPin:GetStickyPin() return self.pin end function ZO_WorldMapStickyPin:ClearStickyPin(mover) if self.movingToPin and self:GetStickyPin() then mover:ClearTargetOffset() end self:SetStickyPin(nil) end function ZO_WorldMapStickyPin:MoveToStickyPin(mover) local movingToPin = self:GetStickyPin() if movingToPin then self.movingToPin = movingToPin local useCurrentZoom = true mover:PanToPin(movingToPin, useCurrentZoom) end end function ZO_WorldMapStickyPin:SetStickyPinFromNearestCandidate() self:SetStickyPin(self.nearestCandidate) end function ZO_WorldMapStickyPin:ClearNearestCandidate() self.nearestCandidate = nil self.nearestCandidateDistanceSq = 0 end function ZO_WorldMapStickyPin:ConsiderPin(pin, x, y) if self.enabled and not pin:IsHidden() then local pinGroup = pin:GetPinGroup() if pinGroup == nil or WORLD_MAP_MANAGER:AreStickyPinsEnabledForPinGroup(pinGroup) then local distanceSq = pin:DistanceToSq(x, y) if distanceSq < self.thresholdDistanceSq then if not self.nearestCandidate or distanceSq < self.nearestCandidateDistanceSq then self.nearestCandidate = pin self.nearestCandidateDistanceSq = distanceSq end end end end end