e5d7d42201
Adds _ComputeZoneSpaceDiff to HBD.lua that computes minimap pin projection in the parent zone's coordinate space (Eversong 1941) instead of using the world-coordinate delta. On Ascension, GetPlayerMapPosition returns Eversong-relative coords for players on Sunstrider, but GetWorldCoordinatesFromZone(1241) treats them as Sunstrider-relative, producing a wrong player world position and causing pins to drift or move opposite to the player. Fix: (1) Store original zone coords on minimap pins at registration (AddMinimapIconMap). (2) In drawMinimapPin, recover player's actual Eversong zone coords by reversing the 1241-bounds conversion. (3) Convert pin's 1241 zone coord to Eversong zone coord via 1241-world. (4) Delta in Eversong zone-space, scale to pixels using parent-width/mapRadius. Falls back to existing world-space math for pins without zone coords.
1479 lines
57 KiB
Lua
1479 lines
57 KiB
Lua
---@type QuestieMap
|
|
local QuestieMap = QuestieLoader:ImportModule("QuestieMap")
|
|
|
|
local mapData = QuestieCompat.UiMapData -- table { width, height, left, top, .instance, .name, .mapType }
|
|
local worldMapData = QuestieCompat.worldMapData -- table { width, height, left, top }
|
|
|
|
-- Keep a reference to the real HBD library (if available) so we can fall back to its map data
|
|
-- for maps not present in Questie's UiMapData (e.g., custom Ascension maps).
|
|
local RealHBD
|
|
|
|
-- Try to load the real HBD library under several known names, and if that fails,
|
|
-- scan the global environment for any table that looks like an HBD library (has .mapData).
|
|
local function LoadRealHBD()
|
|
-- Common library identifiers
|
|
for _, name in ipairs({"HereBeDragonsQuestie-2.0", "HereBeDragons-2.0", "HereBeDragons"}) do
|
|
local ok, lib = pcall(LibStub, name, true)
|
|
if ok and lib and lib.mapData then
|
|
return lib
|
|
end
|
|
end
|
|
-- Fallback: brute-force global scan for a table with a mapData field
|
|
-- Use pcall protection because Ascension's client may have protected globals.
|
|
local ok, _ = pcall(function()
|
|
for _, v in pairs(_G) do
|
|
if type(v) == "table" and v.mapData and type(v.mapData) == "table" then
|
|
RealHBD = v
|
|
error("_found") -- break out of pcall early
|
|
end
|
|
end
|
|
end)
|
|
-- RealHBD was set inside the pcall if found; otherwise stays nil
|
|
return RealHBD
|
|
end
|
|
|
|
-- Eager attempt at load; will also lazy-load on first use.
|
|
-- Wrap in pcall so HBD.lua doesn't fail to load if the global scan hits a protected table.
|
|
pcall(function()
|
|
RealHBD = LoadRealHBD()
|
|
end)
|
|
|
|
local HBD = {mapData = mapData}
|
|
QuestieCompat.HBD = HBD
|
|
|
|
-- Ascension zone remapping: the ghost map 946 should be treated as Eversong
|
|
-- (1941) for pin routing, but Sunstrider Isle (1241) must keep its native
|
|
-- render target. Visibility between Sunstrider and Eversong is handled
|
|
-- separately in HandleWorldMapPin() so we do not collapse the draw target.
|
|
local ZONE_REDIRECT = {
|
|
[946] = 1941, -- Ghost/transition map -> Eversong Woods (for Sunstrider loading)
|
|
}
|
|
|
|
--- Resolve a zone ID through the redirect table.
|
|
--- If the zone has a redirect, return the target zone; otherwise return the zone as-is.
|
|
local function ResolveZone(zone)
|
|
return ZONE_REDIRECT[zone] or zone
|
|
end
|
|
|
|
-- Expose for external use (e.g. QuestieCompat coordinate calibration)
|
|
HBD.ResolveZone = ResolveZone
|
|
|
|
-- Sunstrider calibration note:
|
|
-- 1241 is the correct uiMapId for Sunstrider Isle and 3431 is the matching
|
|
-- areaId. The map texture itself is roughly 510x500, but the Ascension minimap
|
|
-- radius is in live-world yards. Using 510x500 compresses all objective
|
|
-- distances and makes nearby pins look glued to the player. Match the existing
|
|
-- QuestieCompat calibrated pseudo-world scale instead.
|
|
local ASCENSION_ZONE_BOUNDS = {
|
|
[1241] = { 1353.0, 1353.0, -6983.33, 9766.67 },
|
|
[946] = { 1353.0, 1353.0, -6983.33, 9766.67 },
|
|
}
|
|
|
|
|
|
-- Apply Ascension bounds overrides immediately.
|
|
local _boundsApplied = false
|
|
local function ApplyAscensionBounds()
|
|
if _boundsApplied then return end
|
|
_boundsApplied = true
|
|
|
|
if not RealHBD then
|
|
pcall(function() RealHBD = LoadRealHBD() end)
|
|
end
|
|
for zoneId, bounds in pairs(ASCENSION_ZONE_BOUNDS) do
|
|
local data = mapData[zoneId]
|
|
if data then
|
|
data[1], data[2], data[3], data[4] = bounds[1], bounds[2], bounds[3], bounds[4]
|
|
end
|
|
if RealHBD and RealHBD.mapData and RealHBD.mapData[zoneId] then
|
|
local rData = RealHBD.mapData[zoneId]
|
|
rData[1], rData[2], rData[3], rData[4] = bounds[1], bounds[2], bounds[3], bounds[4]
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Debug helper for Sunstrider calibration.
|
|
-- Future agents: use this only for map-fit work on Sunstrider Isle.
|
|
-- areaId 3431 = Sunstrider Isle subzone, uiMapId 1241 = Sunstrider Isle map.
|
|
-- It prints the world-space and zone-space values for a 1241 point so we can
|
|
-- compare the source coords against the rendered pin location.
|
|
function HBD:DebugSunstriderPoint(x, y)
|
|
local worldX, worldY, instanceId = self:GetWorldCoordinatesFromZone(x, y, 1241)
|
|
local zoneX, zoneY = nil, nil
|
|
if worldX and worldY then
|
|
zoneX, zoneY = self:GetZoneCoordinatesFromWorld(worldX, worldY, 1241)
|
|
end
|
|
print(string.format(
|
|
"[SunstriderDebug] zone=1241 input=%.4f,%.4f world=%s,%s inst=%s roundtrip=%s,%s",
|
|
x or -1,
|
|
y or -1,
|
|
tostring(worldX),
|
|
tostring(worldY),
|
|
tostring(instanceId),
|
|
tostring(zoneX),
|
|
tostring(zoneY)
|
|
))
|
|
end
|
|
ApplyAscensionBounds()
|
|
|
|
-- Re-apply Ascension bounds once the player has logged in.
|
|
local _boundsDebugPrinted = false
|
|
local function PrintBoundsDebug()
|
|
if _boundsDebugPrinted then return end
|
|
_boundsDebugPrinted = true
|
|
ApplyAscensionBounds()
|
|
end
|
|
local f = CreateFrame("Frame")
|
|
f:RegisterEvent("PLAYER_LOGIN")
|
|
f:SetScript("OnEvent", function(self, event)
|
|
PrintBoundsDebug()
|
|
self:UnregisterEvent(event)
|
|
end)
|
|
|
|
--- Convert local/point coordinates to world coordinates in yards
|
|
--- @param x X position in 0-1 point coordinates
|
|
--- @param y Y position in 0-1 point coordinates
|
|
--- @param zone uiMapID of the zone
|
|
function HBD:GetWorldCoordinatesFromZone(x, y, zone)
|
|
-- Ascension: mapData[946] has been overridden with Eversong bounds.
|
|
-- mapData[1241] keeps its own calibrated Sunstrider bounds. Do not force
|
|
-- 1241 through the ghost-map redirect here; visibility sharing is handled
|
|
-- separately in HandleWorldMapPin().
|
|
local data = mapData[zone]
|
|
if not data or data[1] == 0 or data[2] == 0 then
|
|
-- Attempt to lazy-load the real HBD if we haven't yet
|
|
if not RealHBD then
|
|
pcall(function() RealHBD = LoadRealHBD() end)
|
|
end
|
|
if RealHBD and RealHBD.mapData then
|
|
data = RealHBD.mapData[zone]
|
|
end
|
|
if not data or data[1] == 0 or data[2] == 0 then
|
|
return nil, nil, nil
|
|
end
|
|
end
|
|
if not x or not y then return nil, nil, nil end
|
|
|
|
local width, height, left, top = data[1], data[2], data[3], data[4]
|
|
x, y = left - width * x, top - height * y
|
|
|
|
return x, y, data.instance
|
|
end
|
|
|
|
--- Convert world coordinates to local/point zone coordinates
|
|
--- @param x Global X position
|
|
--- @param y Global Y position
|
|
--- @param zone uiMapID of the zone
|
|
--- @param allowOutOfBounds Allow coordinates to go beyond the current map (ie. outside of the 0-1 range), otherwise nil will be returned
|
|
function HBD:GetZoneCoordinatesFromWorld(x, y, zone, allowOutOfBounds)
|
|
-- Ascension: mapData[946] has been overridden with Eversong bounds.
|
|
-- mapData[1241] keeps its own calibrated Sunstrider bounds. Callers should
|
|
-- pass the real uiMapId they want to project, not the visibility alias.
|
|
local data = mapData[zone]
|
|
if not data or data[1] == 0 or data[2] == 0 then
|
|
if not RealHBD then
|
|
pcall(function() RealHBD = LoadRealHBD() end)
|
|
end
|
|
if RealHBD and RealHBD.mapData then
|
|
data = RealHBD.mapData[zone]
|
|
end
|
|
if not data or data[1] == 0 or data[2] == 0 then
|
|
return nil, nil
|
|
end
|
|
end
|
|
if not x or not y then return nil, nil end
|
|
|
|
local width, height, left, top = data[1], data[2], data[3], data[4]
|
|
x, y = (left - x) / width, (top - y) / height
|
|
|
|
-- verify the coordinates fall into the zone
|
|
if not allowOutOfBounds and (x < 0 or x > 1 or y < 0 or y > 1) then return nil, nil end
|
|
|
|
return x, y
|
|
end
|
|
|
|
--- Convert world coordinates to local/point zone coordinates on the azeroth world map
|
|
-- @param x Global X position
|
|
-- @param y Global Y position
|
|
-- @param instance Instance to translate coordinates from
|
|
-- @param allowOutOfBounds Allow coordinates to go beyond the current map (ie. outside of the 0-1 range), otherwise nil will be returned
|
|
function HBD:GetAzerothWorldMapCoordinatesFromWorld(x, y, instance, allowOutOfBounds)
|
|
local data = worldMapData[instance]
|
|
if not data or data[1] == 0 or data[2] == 0 then return nil, nil end
|
|
if not x or not y then return nil, nil end
|
|
|
|
local width, height, left, top = data[1], data[2], data[3], data[4]
|
|
x, y = (left - x) / width, (top - y) / height
|
|
|
|
-- verify the coordinates fall into the zone
|
|
if not allowOutOfBounds and (x < 0 or x > 1 or y < 0 or y > 1) then return nil, nil end
|
|
|
|
return x, y
|
|
end
|
|
|
|
--- Return the distance from an origin position to a destination position in the same instance/continent.
|
|
-- @param instanceID instance ID
|
|
-- @param oX origin X
|
|
-- @param oY origin Y
|
|
-- @param dX destination X
|
|
-- @param dY destination Y
|
|
-- @return distance, deltaX, deltaY
|
|
function HBD:GetWorldDistance(instanceID, oX, oY, dX, dY)
|
|
if not oX or not oY or not dX or not dY then return nil, nil, nil end
|
|
local deltaX, deltaY = dX - oX, dY - oY
|
|
return (deltaX * deltaX + deltaY * deltaY)^0.5, deltaX, deltaY
|
|
end
|
|
|
|
--- Return the distance between two points on the same continent
|
|
-- @param oZone origin zone uiMapID
|
|
-- @param oX origin X, in local zone/point coordinates
|
|
-- @param oY origin Y, in local zone/point coordinates
|
|
-- @param dZone destination zone uiMapID
|
|
-- @param dX destination X, in local zone/point coordinates
|
|
-- @param dY destination Y, in local zone/point coordinates
|
|
-- @return distance, deltaX, deltaY in yards
|
|
function HBD:GetZoneDistance(oZone, oX, oY, dZone, dX, dY)
|
|
local oInstance, dInstance
|
|
oX, oY, oInstance = self:GetWorldCoordinatesFromZone(oX, oY, oZone)
|
|
if not oX then return nil, nil, nil end
|
|
|
|
-- translate dX, dY to the origin zone
|
|
dX, dY, dInstance = self:GetWorldCoordinatesFromZone(dX, dY, dZone)
|
|
if not dX then return nil, nil, nil end
|
|
|
|
if oInstance ~= dInstance then return nil, nil, nil end
|
|
|
|
return self:GetWorldDistance(oInstance, oX, oY, dX, dY)
|
|
end
|
|
|
|
-- Position cache: avoid hammering the C API more than 20 times per second.
|
|
-- These are invalidated on zone-change events below.
|
|
local _pos_cache_interval = 0.05
|
|
local _pzp_x, _pzp_y, _pzp_mapID, _pzp_time = nil, nil, nil, 0
|
|
local _pwp_x, _pwp_y, _pwp_inst, _pwp_time = nil, nil, nil, 0
|
|
|
|
--- Get the current world position of the player
|
|
-- The position is transformed to the current continent, if applicable
|
|
-- @return x, y, instanceID
|
|
function HBD:GetPlayerWorldPosition()
|
|
local now = GetTime()
|
|
if now - _pwp_time < _pos_cache_interval then
|
|
return _pwp_x, _pwp_y, _pwp_inst
|
|
end
|
|
|
|
local x, y, uiMapID = HBD:GetPlayerZonePosition()
|
|
if not x or not y then
|
|
_pwp_x, _pwp_y, _pwp_inst = nil, nil, nil
|
|
_pwp_time = now
|
|
return nil, nil, nil
|
|
end
|
|
|
|
local wx, wy, inst = HBD:GetWorldCoordinatesFromZone(x, y, uiMapID)
|
|
_pwp_x, _pwp_y, _pwp_inst = wx, wy, inst
|
|
_pwp_time = now
|
|
if wx and wy then
|
|
return wx, wy, inst
|
|
end
|
|
return nil, nil, nil
|
|
end
|
|
|
|
--- Get the current zone and level of the player
|
|
-- The returned mapFile can represent a micro dungeon, if the player currently is inside one.
|
|
-- @return uiMapID, mapType
|
|
function HBD:GetPlayerZone()
|
|
return QuestieCompat.GetCurrentPlayerPosition()
|
|
end
|
|
|
|
--- Get the current position of the player on a zone level
|
|
-- The returned values are local point coordinates, 0-1. The mapFile can represent a micro dungeon.
|
|
-- @param allowOutOfBounds Allow coordinates to go beyond the current map (ie. outside of the 0-1 range), otherwise nil will be returned
|
|
-- @return x, y, uiMapID, mapType
|
|
function HBD:GetPlayerZonePosition(allowOutOfBounds)
|
|
local now = GetTime()
|
|
if now - _pzp_time < _pos_cache_interval then
|
|
return _pzp_x, _pzp_y, _pzp_mapID
|
|
end
|
|
|
|
local uiMapID, x, y = QuestieCompat.GetCurrentPlayerPosition()
|
|
_pzp_x, _pzp_y, _pzp_mapID = x, y, uiMapID
|
|
_pzp_time = now
|
|
|
|
if uiMapID and x and y then
|
|
return x, y, uiMapID
|
|
end
|
|
return nil, nil, nil, nil
|
|
end
|
|
|
|
local function _InvalidatePositionCache()
|
|
_pzp_time = 0
|
|
_pwp_time = 0
|
|
end
|
|
|
|
-- Data Constants
|
|
local WORLD_MAP_ID = 947
|
|
|
|
-- upvalue lua api
|
|
local cos, sin, max = math.cos, math.sin, math.max
|
|
local type, pairs = type, pairs
|
|
|
|
-- upvalue wow api
|
|
local GetPlayerFacing = GetPlayerFacing
|
|
|
|
-- storage for minimap pins
|
|
local minimapPins = {}
|
|
local activeMinimapPins = {}
|
|
local minimapPinRegistry = {}
|
|
|
|
-- and worldmap pins
|
|
local worldmapPins = {}
|
|
local worldmapPinRegistry = {}
|
|
|
|
local pins = {
|
|
Minimap = Minimap,
|
|
updateFrame = CreateFrame("Frame"),
|
|
activeMinimapPins = activeMinimapPins,
|
|
worldmapPins = worldmapPins,
|
|
worldmapProvider = {
|
|
GetMap = function(self) return QuestieCompat.WorldMapFrame end,
|
|
},
|
|
}
|
|
QuestieCompat.HBDPins = pins
|
|
|
|
local minimap_size = {
|
|
indoor = {
|
|
[0] = 300, -- scale
|
|
[1] = 240, -- 1.25
|
|
[2] = 180, -- 5/3
|
|
[3] = 120, -- 2.5
|
|
[4] = 80, -- 3.75
|
|
[5] = 50, -- 6
|
|
},
|
|
outdoor = {
|
|
[0] = 466 + 2/3, -- scale
|
|
[1] = 400, -- 7/6
|
|
[2] = 333 + 1/3, -- 1.4
|
|
[3] = 266 + 2/6, -- 1.75
|
|
[4] = 200, -- 7/3
|
|
[5] = 133 + 1/3, -- 3.5
|
|
},
|
|
}
|
|
|
|
---@class MinimapShapes
|
|
local minimap_shapes = {
|
|
-- { upper-left, lower-left, upper-right, lower-right }
|
|
["SQUARE"] = { false, false, false, false },
|
|
["CORNER-TOPLEFT"] = { true, false, false, false },
|
|
["CORNER-TOPRIGHT"] = { false, false, true, false },
|
|
["CORNER-BOTTOMLEFT"] = { false, true, false, false },
|
|
["CORNER-BOTTOMRIGHT"] = { false, false, false, true },
|
|
["SIDE-LEFT"] = { true, true, false, false },
|
|
["SIDE-RIGHT"] = { false, false, true, true },
|
|
["SIDE-TOP"] = { true, false, true, false },
|
|
["SIDE-BOTTOM"] = { false, true, false, true },
|
|
["TRICORNER-TOPLEFT"] = { true, true, true, false },
|
|
["TRICORNER-TOPRIGHT"] = { true, false, true, true },
|
|
["TRICORNER-BOTTOMLEFT"] = { true, true, false, true },
|
|
["TRICORNER-BOTTOMRIGHT"] = { false, true, true, true },
|
|
}
|
|
|
|
local tableCache = setmetatable({}, {__mode='k'})
|
|
|
|
local function newCachedTable()
|
|
local t = next(tableCache)
|
|
if t then
|
|
tableCache[t] = nil
|
|
else
|
|
t = {}
|
|
end
|
|
return t
|
|
end
|
|
|
|
local function recycle(t)
|
|
tableCache[t] = true
|
|
end
|
|
|
|
-------------------------------------------------------------------------------------------
|
|
-- Minimap pin position logic
|
|
|
|
-- minimap rotation
|
|
local rotateMinimap = GetCVar("rotateMinimap") == "1"
|
|
|
|
-- is the minimap indoors or outdoors
|
|
local indoors = GetCVar("minimapZoom")+0 == pins.Minimap:GetZoom() and "outdoor" or "indoor"
|
|
|
|
local minimapPinCount, queueFullUpdate = 0, false
|
|
---@type unknown, MinimapShapes?
|
|
local minimapScale, minimapShape, mapRadius, minimapWidth, minimapHeight, mapSin, mapCos
|
|
local lastZoom, lastFacing, lastXY, lastYY
|
|
local lastDebugPinPoints = setmetatable({}, { __mode = "k" })
|
|
local lastDebugPinScreens = setmetatable({}, { __mode = "k" })
|
|
local lastDebugPlayerWorldX, lastDebugPlayerWorldY
|
|
|
|
local function _GetMinimapViewRadius()
|
|
if pins.Minimap and pins.Minimap.GetViewRadius then
|
|
return pins.Minimap:GetViewRadius()
|
|
end
|
|
end
|
|
|
|
local function _IsDebuggableMinimapPin(pin, data, distance, passesCutoff)
|
|
if not pin or not data then return false end
|
|
if passesCutoff then return true end
|
|
if data.uiMapID == 1241 or data.uiMapID == 1941 or data.uiMapID == 946 then return true end
|
|
if distance and distance < 150 then return true end
|
|
|
|
local iconData = pin.data
|
|
local label = iconData and (iconData.Name or iconData.name or iconData.Title)
|
|
if label and string.find(string.lower(label), "mana", 1, true) then return true end
|
|
if iconData and (iconData.Type == "available" or iconData.Type == "complete") then return true end
|
|
|
|
local id = iconData and (iconData.Id or iconData.id or iconData.questId or iconData.QuestId or iconData.ObjectiveTargetId)
|
|
return id == 8345 or id == 8346 or id == 15274 or id == 15278 or id == 15295
|
|
end
|
|
|
|
local function _GetMinimapPinDelta(data)
|
|
local xDist, yDist = lastXY - data.x, lastYY - data.y
|
|
|
|
if rotateMinimap then
|
|
local dx, dy = xDist, yDist
|
|
xDist = dx*mapCos - dy*mapSin
|
|
yDist = dx*mapSin + dy*mapCos
|
|
end
|
|
|
|
return xDist, yDist
|
|
end
|
|
|
|
local function _GetMinimapPinScreenMetrics(data)
|
|
local xDist, yDist = _GetMinimapPinDelta(data)
|
|
local centerDist = mapRadius and mapRadius > 0 and math.sqrt(xDist*xDist + yDist*yDist) / mapRadius or nil
|
|
local screenX = mapRadius and mapRadius > 0 and (xDist / mapRadius) * minimapWidth or nil
|
|
local screenY = mapRadius and mapRadius > 0 and -(yDist / mapRadius) * minimapHeight or nil
|
|
return xDist, yDist, centerDist, screenX, screenY
|
|
end
|
|
|
|
local function _DebugMinimapPin(pin, data, index, playerX, playerY, playerInstanceID, distance, passesCutoff, cutoff)
|
|
local iconData = pin and pin.data
|
|
local label = iconData and (iconData.Name or iconData.name or iconData.Title or iconData.Id or iconData.id) or "?"
|
|
local questId = iconData and (iconData.questId or iconData.QuestId or iconData.Id or iconData.id) or "?"
|
|
local targetId = iconData and (iconData.ObjectiveTargetId or iconData.objectiveTargetId or iconData.TargetId or iconData.targetId) or "?"
|
|
local pinMapX = pin and pin.x or "?"
|
|
local pinMapY = pin and pin.y or "?"
|
|
local parent = pin and pin:GetParent()
|
|
local point1, rel1, point2, xOff, yOff
|
|
if pin and pin.GetPoint then
|
|
point1, rel1, point2, xOff, yOff = pin:GetPoint()
|
|
end
|
|
local xDist, yDist, centerDist, screenX, screenY = _GetMinimapPinScreenMetrics(data)
|
|
|
|
print(string.format(
|
|
"[QD] MinimapPin #%d label=%s type=%s quest=%s target=%s ui=%s area=%s map=(%s,%s) world=(%s,%s) player=(%s,%s) inst=%s/%s dist=%s cutoff=%s pass=%s edge=%s delta=(%s,%s) center=%s screen=(%s,%s) shown=%s hidden=%s parent=%s point=(%s,%s,%s,%s,%s)",
|
|
index,
|
|
tostring(label),
|
|
tostring(iconData and iconData.Type or "?"),
|
|
tostring(questId),
|
|
tostring(targetId),
|
|
tostring(data.uiMapID),
|
|
tostring(pin and pin.AreaID or "?"),
|
|
tostring(pinMapX),
|
|
tostring(pinMapY),
|
|
tostring(data.x),
|
|
tostring(data.y),
|
|
tostring(playerX),
|
|
tostring(playerY),
|
|
tostring(data.instanceID),
|
|
tostring(playerInstanceID),
|
|
tostring(distance),
|
|
tostring(cutoff),
|
|
tostring(passesCutoff),
|
|
tostring(data.onEdge),
|
|
tostring(xDist),
|
|
tostring(yDist),
|
|
tostring(centerDist),
|
|
tostring(screenX),
|
|
tostring(screenY),
|
|
tostring(pin and pin.IsShown and pin:IsShown() or nil),
|
|
tostring(pin and pin.hidden),
|
|
tostring(parent and parent.GetName and parent:GetName() or parent),
|
|
tostring(point1),
|
|
tostring(rel1 and rel1.GetName and rel1:GetName() or rel1),
|
|
tostring(point2),
|
|
tostring(xOff),
|
|
tostring(yOff)
|
|
))
|
|
end
|
|
|
|
local function _DebugAnyMinimapPin(pin, data, index, label)
|
|
local iconData = pin and pin.data
|
|
local parent = pin and pin:GetParent()
|
|
local point1, rel1, point2, xOff, yOff
|
|
if pin and pin.GetPoint then
|
|
point1, rel1, point2, xOff, yOff = pin:GetPoint()
|
|
end
|
|
print(string.format(
|
|
"[QD] %s #%d label=%s type=%s ui=%s area=%s shown=%s hidden=%s parent=%s point=(%s,%s,%s,%s,%s) keep=%s edge=%s dataKeep=%s dataOnEdge=%s",
|
|
label,
|
|
index,
|
|
tostring(iconData and (iconData.Name or iconData.name or iconData.Title or iconData.Id or iconData.id) or "?"),
|
|
tostring(iconData and iconData.Type or "?"),
|
|
tostring(data and data.uiMapID),
|
|
tostring(pin and pin.AreaID or "?"),
|
|
tostring(pin and pin.IsShown and pin:IsShown() or nil),
|
|
tostring(pin and pin.hidden),
|
|
tostring(parent and parent.GetName and parent:GetName() or parent),
|
|
tostring(point1),
|
|
tostring(rel1 and rel1.GetName and rel1:GetName() or rel1),
|
|
tostring(point2),
|
|
tostring(xOff),
|
|
tostring(yOff),
|
|
tostring(pin and pin.shouldBeShowing),
|
|
tostring(pin and pin.onEdge),
|
|
tostring(data and data.keep),
|
|
tostring(data and data.onEdge)
|
|
))
|
|
end
|
|
|
|
local function _ComputeZoneSpaceDiff(data, pinWorldX, pinWorldY, playerWorldX, playerWorldY)
|
|
-- For pins on child maps (like Sunstrider 1241 inside Eversong 1941),
|
|
-- Ascension's GetPlayerMapPosition returns Eversong-relative coords, but
|
|
-- GetWorldCoordinatesFromZone(childZone) treats them as child-relative,
|
|
-- producing a wrong player world position.
|
|
--
|
|
-- Strategy: work entirely in the parent (Eversong/1941) zone space:
|
|
-- 1. Recover player's Eversong zone coords by reversing the (wrong)
|
|
-- 1241-bounds conversion: ep = (left_1241 - playerWorldX) / width_1241
|
|
-- 2. Convert pin's stored 1241 zone coord to Eversong zone coord:
|
|
-- pin 1241-zone → 1241-world (via 1241 bounds) → 1941-zone (via 1941 bounds)
|
|
-- 3. Delta in Eversong zone-space, scale to pixels.
|
|
local uiMapID = data and data.uiMapID
|
|
local zoneX = data and data.zoneX
|
|
local zoneY = data and data.zoneY
|
|
if not uiMapID or not zoneX or not zoneY then return nil end
|
|
|
|
-- Get the child-map bounds
|
|
local childMd = mapData[uiMapID]
|
|
if not childMd or childMd[1] == 0 or childMd[2] == 0 then
|
|
if RealHBD and RealHBD.mapData then
|
|
childMd = RealHBD.mapData[uiMapID]
|
|
end
|
|
end
|
|
if not childMd or childMd[1] == 0 or childMd[2] == 0 then return nil end
|
|
|
|
local childLeft, childTop = childMd[3], childMd[4]
|
|
local childWidth, childHeight = childMd[1], childMd[2]
|
|
|
|
-- Determine parent/common zone (Eversong 1941 for Sunstrider 1241)
|
|
local parentZone = ZONE_REDIRECT[uiMapID]
|
|
if not parentZone then
|
|
parentZone = (uiMapID == 1241) and 1941 or uiMapID
|
|
end
|
|
|
|
-- Get the parent zone bounds
|
|
local parentMd = mapData[parentZone]
|
|
if not parentMd or parentMd[1] == 0 or parentMd[2] == 0 then
|
|
if RealHBD and RealHBD.mapData then
|
|
parentMd = RealHBD.mapData[parentZone]
|
|
end
|
|
end
|
|
if not parentMd or parentMd[1] == 0 or parentMd[2] == 0 then return nil end
|
|
|
|
local parentLeft, parentTop = parentMd[3], parentMd[4]
|
|
local parentWidth, parentHeight = parentMd[1], parentMd[2]
|
|
|
|
-- Step 1: Recover player's actual Eversong zone coords.
|
|
-- playerWorldX was computed as childLeft - childWidth * ep (where ep is the
|
|
-- Eversong zone coord that GetPlayerMapPosition returned). Reverse it.
|
|
local playerEX = (childLeft - playerWorldX) / childWidth
|
|
local playerEY = (childTop - playerWorldY) / childHeight
|
|
|
|
-- Step 2: Convert pin's 1241 zone coord to Eversong world,
|
|
-- then to Eversong zone coord
|
|
local pinWorldX_child = childLeft - childWidth * zoneX
|
|
local pinWorldY_child = childTop - childHeight * zoneY
|
|
|
|
local pinEX = (parentLeft - pinWorldX_child) / parentWidth
|
|
local pinEY = (parentTop - pinWorldY_child) / parentHeight
|
|
|
|
-- Clamp to valid range
|
|
if pinEX < -0.5 or pinEX > 1.5 or pinEY < -0.5 or pinEY > 1.5 then
|
|
-- Pin is too far from the parent zone to be meaningful
|
|
return nil
|
|
end
|
|
|
|
-- Step 3: Delta in Eversong zone-space
|
|
local dX = pinEX - playerEX
|
|
local dY = pinEY - playerEY
|
|
|
|
-- Convert to minimap offset.
|
|
-- Eversong zone delta * parentWidth = world-yard delta.
|
|
-- Normalize by mapRadius, scale to pixel half-width.
|
|
local mapRad = (mapRadius and mapRadius > 0) and mapRadius or 1
|
|
local diffX = dX * (parentWidth / mapRad)
|
|
local diffY = dY * (parentHeight / mapRad)
|
|
|
|
return diffX, diffY
|
|
end
|
|
|
|
local function drawMinimapPin(pin, data)
|
|
local xDist, yDist = _GetMinimapPinDelta(data)
|
|
|
|
-- Try zone-space projection when the pin has uiMapID and zone coords.
|
|
-- This avoids the world-coordinate conversion path that depends on
|
|
-- ASCENSION_ZONE_BOUNDS being perfectly calibrated.
|
|
local zDiffX, zDiffY
|
|
if data and data.uiMapID and data.zoneX and data.zoneY and lastXY and lastYY then
|
|
zDiffX, zDiffY = _ComputeZoneSpaceDiff(
|
|
data, data.x, data.y, lastXY, lastYY)
|
|
end
|
|
|
|
-- Debug: trace pin projection when QuestieDebugPins is enabled
|
|
if _G.QuestieDebugPins and pin and data then
|
|
local iconData = pin.data
|
|
local label = iconData and (iconData.Name or iconData.name or iconData.Title or "?") or "?"
|
|
local pinType = iconData and iconData.Type or "?"
|
|
-- Only print for available/complete quest pins (the ? and ! markers)
|
|
if pinType == "available" or pinType == "complete" or pinType == "turnin" then
|
|
local diffX_debug = mapRadius and mapRadius ~= 0 and (xDist / mapRadius) or 0
|
|
local diffY_debug = mapRadius and mapRadius ~= 0 and (yDist / mapRadius) or 0
|
|
local mmpw = minimapWidth or 0
|
|
local mmph = minimapHeight or 0
|
|
local screenX_debug = diffX_debug * mmpw
|
|
local screenY_debug = -diffY_debug * mmph
|
|
if zDiffX then
|
|
print(string.format(
|
|
"[QD] drawPin ZONE label=%s type=%s zDiff=(%.4f,%.4f) zScreen=(%.1f,%.1f) | world delta=(%.2f,%.2f) diff=(%.4f,%.4f)",
|
|
tostring(label), tostring(pinType),
|
|
zDiffX, zDiffY, zDiffX * mmpw, -zDiffY * mmph,
|
|
xDist, yDist, diffX_debug, diffY_debug))
|
|
else
|
|
print(string.format(
|
|
"[QD] drawPin label=%s type=%s pinWorld=(%.2f,%.2f) playerWorld=(%.2f,%.2f) delta=(%.2f,%.2f) diff=(%.4f,%.4f) radius=%.2f screen=(%.1f,%.1f)",
|
|
tostring(label), tostring(pinType),
|
|
data.x or 0, data.y or 0,
|
|
lastXY or 0, lastYY or 0,
|
|
xDist, yDist,
|
|
diffX_debug, diffY_debug,
|
|
mapRadius or 0,
|
|
screenX_debug, screenY_debug))
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Use zone-space diff if available, otherwise fall back to world-space
|
|
local diffX, diffY
|
|
if zDiffX and zDiffY then
|
|
diffX, diffY = zDiffX, zDiffY
|
|
else
|
|
diffX = xDist / mapRadius
|
|
diffY = yDist / mapRadius
|
|
end
|
|
|
|
-- different minimap shapes
|
|
---@type boolean|number
|
|
local isRound = true
|
|
if minimapShape and not (xDist == 0 or yDist == 0) then
|
|
isRound = (xDist < 0) and 1 or 3
|
|
if yDist < 0 then
|
|
isRound = minimapShape[isRound]
|
|
else
|
|
isRound = minimapShape[isRound + 1]
|
|
end
|
|
end
|
|
|
|
-- calculate distance from the center of the map
|
|
local dist
|
|
if isRound then
|
|
dist = (diffX*diffX + diffY*diffY) / 0.9^2
|
|
else
|
|
dist = max(diffX*diffX, diffY*diffY) / 0.9^2
|
|
end
|
|
|
|
-- if distance > 1, then adapt node position to slide on the border
|
|
if dist > 1 and data.floatOnEdge then
|
|
dist = dist^0.5
|
|
diffX = diffX/dist
|
|
diffY = diffY/dist
|
|
end
|
|
|
|
-- Questie Modification.
|
|
-- data.floatOnEdge is replaced by (data.floatOnEdge and ((pin.texture and pin.texture.a and pin.texture.a ~= 0) or pin.texture == nil))
|
|
-- icons will now only float on edge if they have an opacity which is not 0 or if no texture exist.
|
|
data.distanceFromMinimapCenter = dist
|
|
if dist <= 1 or (data.floatOnEdge and ((pin.texture and pin.texture.a and pin.texture.a ~= 0) or pin.texture == nil)) then
|
|
pin:Show()
|
|
pin:ClearAllPoints()
|
|
pin:SetPoint("CENTER", pins.Minimap, "CENTER", diffX * minimapWidth, -diffY * minimapHeight)
|
|
data.onEdge = (dist > 1)
|
|
else
|
|
pin:Hide()
|
|
data.onEdge = nil
|
|
pin.keep = nil
|
|
end
|
|
end
|
|
|
|
local function _GetEffectiveMinimapPlayerWorldPosition()
|
|
-- For minimap pins we MUST use HBD's native world coordinate space.
|
|
-- Minimap pins store their positions in HBD world coords, so the player
|
|
-- position must also be in HBD world coords. The calibrated pseudo-world
|
|
-- space (used by the arrow) has a different origin/scale on Ascension and
|
|
-- must never be mixed with minimap pin positions.
|
|
local wx, wy, inst = HBD:GetPlayerWorldPosition()
|
|
if _G.QuestieDebugPins and wx and wy then
|
|
local px, py = pins.Minimap:GetCenter()
|
|
local indoors = IsIndoor and IsIndoor() or false
|
|
local viewRadius = _GetMinimapViewRadius()
|
|
local zoom = pins.Minimap and pins.Minimap:GetZoom() or 0
|
|
local shape = GetMinimapShape and GetMinimapShape() or "?"
|
|
local mmpw = pins.Minimap:GetWidth() and (pins.Minimap:GetWidth() / 2) or 0
|
|
local mmph = pins.Minimap:GetHeight() and (pins.Minimap:GetHeight() / 2) or 0
|
|
print(string.format(
|
|
"[QD] PlayerPos world=(%.2f,%.2f) viewRadius=%.2f zoom=%d shape=%s indoors=%s mmpSize=(%.1f,%.1f)",
|
|
wx, wy, viewRadius or -1, zoom, tostring(shape), tostring(indoors), mmpw, mmph
|
|
))
|
|
end
|
|
return wx, wy, inst
|
|
end
|
|
|
|
local function UpdateMinimapPins(force)
|
|
-- get the current player position
|
|
local x, y, instanceID = _GetEffectiveMinimapPlayerWorldPosition()
|
|
|
|
-- get data from the API for calculations
|
|
local zoom = pins.Minimap:GetZoom()
|
|
local diffZoom = zoom ~= lastZoom
|
|
|
|
-- for rotating minimap support
|
|
local facing
|
|
if rotateMinimap then
|
|
facing = GetPlayerFacing()
|
|
else
|
|
facing = lastFacing
|
|
end
|
|
|
|
-- check for all values to be available (starting with 7.1.0, instances don't report coordinates)
|
|
if not x or not y or (rotateMinimap and not facing) then
|
|
minimapPinCount = 0
|
|
for pin in pairs(activeMinimapPins) do
|
|
pin:Hide()
|
|
activeMinimapPins[pin] = nil
|
|
end
|
|
return
|
|
end
|
|
|
|
local newScale = pins.Minimap:GetScale()
|
|
if minimapScale ~= newScale then
|
|
minimapScale = newScale
|
|
force = true
|
|
end
|
|
|
|
if x ~= lastXY or y ~= lastYY or diffZoom or facing ~= lastFacing or force then
|
|
-- minimap information
|
|
minimapShape = GetMinimapShape and minimap_shapes[GetMinimapShape() or "ROUND"]
|
|
minimapWidth = pins.Minimap:GetWidth() / 2
|
|
minimapHeight = pins.Minimap:GetHeight() / 2
|
|
local viewRadius = _GetMinimapViewRadius()
|
|
if viewRadius then
|
|
mapRadius = viewRadius
|
|
elseif MinimapRadiusAPI then
|
|
mapRadius = C_Minimap.GetViewRadius()
|
|
else
|
|
local sizeTable = minimap_size[indoors] or minimap_size.outdoor
|
|
local size = sizeTable[zoom]
|
|
or sizeTable[5]
|
|
or sizeTable[4]
|
|
or sizeTable[3]
|
|
or sizeTable[2]
|
|
or sizeTable[1]
|
|
or sizeTable[0]
|
|
|
|
mapRadius = size / 2
|
|
end
|
|
|
|
-- update upvalues for icon placement
|
|
lastZoom = zoom
|
|
lastFacing = facing
|
|
lastXY, lastYY = x, y
|
|
|
|
if rotateMinimap then
|
|
mapSin = sin(facing)
|
|
mapCos = cos(facing)
|
|
end
|
|
|
|
local debugPins = _G.QuestieDebugPins
|
|
local debugPinIndex = 0
|
|
if debugPins then
|
|
_G.QuestieDebugPins = false
|
|
print(string.format("[QD] UpdateMinimapPins playerWorld=(%s,%s) inst=%s zoom=%s radius=%s size=(%s,%s) rotate=%s totalPins=%s",
|
|
tostring(x), tostring(y), tostring(instanceID), tostring(zoom), tostring(mapRadius),
|
|
tostring(minimapWidth), tostring(minimapHeight), tostring(rotateMinimap), tostring(minimapPinCount)))
|
|
end
|
|
local playerMoveX, playerMoveY
|
|
if debugPins and lastDebugPlayerWorldX and lastDebugPlayerWorldY then
|
|
playerMoveX = x - lastDebugPlayerWorldX
|
|
playerMoveY = y - lastDebugPlayerWorldY
|
|
end
|
|
if debugPins then
|
|
lastDebugPlayerWorldX, lastDebugPlayerWorldY = x, y
|
|
end
|
|
|
|
local debugDeferredPins = {}
|
|
local debugDeferredCount = 0
|
|
local debugLimit = _G.QuestieDebugPinLimit or 8
|
|
for pin, data in pairs(minimapPins) do
|
|
-- Use the live minimap radius when the client exposes it. Sunstrider
|
|
-- uses inflated pseudo-world bounds for placement, so mapData size is
|
|
-- no longer a good visibility cutoff there.
|
|
local cutoff = mapRadius and (mapRadius * 1.05) or 500
|
|
if not _GetMinimapViewRadius() and data.uiMapID and HBD.mapData[data.uiMapID] then
|
|
local w, h = HBD.mapData[data.uiMapID][1], HBD.mapData[data.uiMapID][2]
|
|
if w and h and w > 0 and h > 0 then
|
|
cutoff = math.max(100, math.min(500, math.sqrt(w*w + h*h) * 0.15))
|
|
end
|
|
end
|
|
local rawXDist, rawYDist = x - data.x, y - data.y
|
|
local distance = math.sqrt(rawXDist*rawXDist + rawYDist*rawYDist)
|
|
local passesCutoff = instanceID == data.instanceID and distance <= cutoff
|
|
if debugPins and debugPinIndex < debugLimit and passesCutoff then
|
|
debugPinIndex = debugPinIndex + 1
|
|
_DebugMinimapPin(pin, data, debugPinIndex, x, y, instanceID, distance, passesCutoff, cutoff)
|
|
elseif debugPins and _IsDebuggableMinimapPin(pin, data, distance, passesCutoff) then
|
|
debugDeferredCount = debugDeferredCount + 1
|
|
debugDeferredPins[debugDeferredCount] = {pin, data, distance, passesCutoff, cutoff}
|
|
end
|
|
if debugPins and pin and data then
|
|
local _, _, _, screenX, screenY = _GetMinimapPinScreenMetrics(data)
|
|
local lastScreen = lastDebugPinScreens[pin]
|
|
if lastScreen and screenX and screenY then
|
|
print(string.format(
|
|
"[QD] PinMotion name=%s pass=%s playerMove=(%s,%s) screenMove=(%s,%s) screenNow=(%s,%s) screenPrev=(%s,%s)",
|
|
tostring(pin:GetName()),
|
|
tostring(passesCutoff),
|
|
tostring(playerMoveX),
|
|
tostring(playerMoveY),
|
|
tostring(screenX - lastScreen[1]),
|
|
tostring(screenY - lastScreen[2]),
|
|
tostring(screenX),
|
|
tostring(screenY),
|
|
tostring(lastScreen[1]),
|
|
tostring(lastScreen[2])
|
|
))
|
|
end
|
|
if screenX and screenY then
|
|
lastDebugPinScreens[pin] = { screenX, screenY }
|
|
end
|
|
end
|
|
if passesCutoff then -- questie specific fix
|
|
activeMinimapPins[pin] = data
|
|
data.keep = true
|
|
-- draw the pin (this may reset data.keep if outside of the map)
|
|
drawMinimapPin(pin, data)
|
|
if debugPins and pin and pin.GetPoint then
|
|
local point1, rel1, point2, xOff, yOff = pin:GetPoint()
|
|
local pointKey = table.concat({
|
|
tostring(point1),
|
|
tostring(rel1 and rel1.GetName and rel1:GetName() or rel1),
|
|
tostring(point2),
|
|
tostring(xOff),
|
|
tostring(yOff)
|
|
}, "|")
|
|
if lastDebugPinPoints[pin] ~= pointKey then
|
|
print(string.format(
|
|
"[QD] PinAnchorChanged name=%s point=(%s,%s,%s,%s,%s) shown=%s hidden=%s parent=%s",
|
|
tostring(pin:GetName()),
|
|
tostring(point1),
|
|
tostring(rel1 and rel1.GetName and rel1:GetName() or rel1),
|
|
tostring(point2),
|
|
tostring(xOff),
|
|
tostring(yOff),
|
|
tostring(pin:IsShown()),
|
|
tostring(pin.hidden),
|
|
tostring(pin:GetParent() and pin:GetParent():GetName() or pin:GetParent())
|
|
))
|
|
lastDebugPinPoints[pin] = pointKey
|
|
end
|
|
end
|
|
else
|
|
pin:Hide()
|
|
data.onEdge = nil
|
|
data.keep = nil
|
|
activeMinimapPins[pin] = nil
|
|
end
|
|
end
|
|
|
|
if debugPins then
|
|
local i = 1
|
|
while debugPinIndex < debugLimit and i <= debugDeferredCount do
|
|
local entry = debugDeferredPins[i]
|
|
debugPinIndex = debugPinIndex + 1
|
|
_DebugMinimapPin(entry[1], entry[2], debugPinIndex, x, y, instanceID, entry[3], entry[4], entry[5])
|
|
i = i + 1
|
|
end
|
|
if debugPinIndex == 0 then
|
|
local fallbackIndex = 0
|
|
for pin, data in pairs(minimapPins) do
|
|
fallbackIndex = fallbackIndex + 1
|
|
if fallbackIndex > debugLimit then break end
|
|
_DebugAnyMinimapPin(pin, data, fallbackIndex, "MinimapPinState")
|
|
end
|
|
end
|
|
end
|
|
|
|
minimapPinCount = 0
|
|
for pin, data in pairs(activeMinimapPins) do
|
|
if not data.keep then
|
|
pin:Hide()
|
|
activeMinimapPins[pin] = nil
|
|
else
|
|
minimapPinCount = minimapPinCount + 1
|
|
data.keep = nil
|
|
end
|
|
end
|
|
if debugPins then
|
|
print("[QD] QuestieDebugPins complete; set QuestieDebugPins=true again for another one-shot trace.")
|
|
end
|
|
end
|
|
end
|
|
|
|
local function UpdateMinimapIconPosition()
|
|
-- get the current map zoom
|
|
local zoom = pins.Minimap:GetZoom()
|
|
local diffZoom = zoom ~= lastZoom
|
|
-- if the map zoom changed, run a full update sweep
|
|
if diffZoom then
|
|
UpdateMinimapPins()
|
|
return
|
|
end
|
|
|
|
-- we have no active minimap pins, just return early
|
|
if minimapPinCount == 0 then return end
|
|
|
|
local x, y = _GetEffectiveMinimapPlayerWorldPosition()
|
|
|
|
-- for rotating minimap support
|
|
local facing
|
|
if rotateMinimap then
|
|
facing = GetPlayerFacing()
|
|
else
|
|
facing = lastFacing
|
|
end
|
|
|
|
-- check for all values to be available (starting with 7.1.0, instances don't report coordinates)
|
|
if not x or not y or (rotateMinimap and not facing) then
|
|
UpdateMinimapPins()
|
|
return
|
|
end
|
|
|
|
local refresh
|
|
local newScale = pins.Minimap:GetScale()
|
|
if minimapScale ~= newScale then
|
|
minimapScale = newScale
|
|
refresh = true
|
|
end
|
|
|
|
if x ~= lastXY or y ~= lastYY or facing ~= lastFacing or refresh then
|
|
-- update radius of the map
|
|
local viewRadius = _GetMinimapViewRadius()
|
|
if viewRadius then
|
|
mapRadius = viewRadius
|
|
elseif MinimapRadiusAPI then
|
|
mapRadius = C_Minimap.GetViewRadius()
|
|
else
|
|
local sizeTable = minimap_size[indoors] or minimap_size.outdoor
|
|
local size = sizeTable[zoom]
|
|
or sizeTable[5]
|
|
or sizeTable[4]
|
|
or sizeTable[3]
|
|
or sizeTable[2]
|
|
or sizeTable[1]
|
|
or sizeTable[0]
|
|
|
|
mapRadius = size / 2
|
|
end
|
|
-- update upvalues for icon placement
|
|
lastXY, lastYY = x, y
|
|
lastFacing = facing
|
|
|
|
if rotateMinimap then
|
|
mapSin = sin(facing)
|
|
mapCos = cos(facing)
|
|
end
|
|
|
|
-- iterate all nodes and check if they are still in range of our minimap display
|
|
for pin, data in pairs(activeMinimapPins) do
|
|
-- update the position of the node
|
|
drawMinimapPin(pin, data)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function UpdateMinimapZoom()
|
|
if not _GetMinimapViewRadius() and not MinimapRadiusAPI then
|
|
local zoom = pins.Minimap:GetZoom()
|
|
if GetCVar("minimapZoom") == GetCVar("minimapInsideZoom") then
|
|
pins.Minimap:SetZoom(zoom < 2 and zoom + 1 or zoom - 1)
|
|
end
|
|
indoors = GetCVar("minimapZoom")+0 == pins.Minimap:GetZoom() and "outdoor" or "indoor"
|
|
pins.Minimap:SetZoom(zoom)
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------------------
|
|
-- WorldMap data provider
|
|
|
|
local Enum = {
|
|
-- https://wowpedia.fandom.com/wiki/Enum.UIMapType
|
|
UIMapType = {
|
|
Cosmic = 0,
|
|
World = 1,
|
|
Continent = 2,
|
|
Zone = 3,
|
|
Dungeon = 4,
|
|
Micro = 5,
|
|
Orphan = 6
|
|
}
|
|
}
|
|
|
|
local worldmapWidth, worldmapHeight
|
|
|
|
local function HandleWorldMapPin(icon, data)
|
|
if not WorldMapFrame:IsVisible() then return end
|
|
|
|
local uiMapID = QuestieCompat.GetCurrentUiMapID()
|
|
|
|
-- check for a valid map
|
|
if not uiMapID then return end
|
|
|
|
--Questie Modification
|
|
-- Child-map / zone-redirect exception: if we're viewing a child map (e.g. Sunstrider 1241)
|
|
-- and the pin belongs to a parent map (e.g. Eversong 1941) or a zone that shares the same
|
|
-- coordinate space, SHOW_CURRENT pins should still be visible.
|
|
-- Sunstrider and Eversong share visibility, but 1241 must remain the actual render target.
|
|
local effectiveUiMapID = ResolveZone(uiMapID)
|
|
local effectiveDataUiMapID = ResolveZone(data.uiMapID)
|
|
local sharesSunstriderSpace = (
|
|
(uiMapID == 1241 and data.uiMapID == 1941) or
|
|
(uiMapID == 1941 and data.uiMapID == 1241) or
|
|
(uiMapID == 946 and data.uiMapID == 1241) or
|
|
(uiMapID == 1241 and data.uiMapID == 946) or
|
|
(uiMapID == 946 and data.uiMapID == 1941) or
|
|
(uiMapID == 1941 and data.uiMapID == 946) or
|
|
(effectiveUiMapID == 1941 and effectiveDataUiMapID == 1941 and (uiMapID == 1241 or data.uiMapID == 1241 or uiMapID == 946 or data.uiMapID == 946))
|
|
)
|
|
local isChildMap = false
|
|
local ancestorMapID = HBD.mapData[uiMapID] and HBD.mapData[uiMapID].parentMapID
|
|
while ancestorMapID and HBD.mapData[ancestorMapID] do
|
|
if ancestorMapID == data.uiMapID then
|
|
isChildMap = true
|
|
break
|
|
end
|
|
ancestorMapID = HBD.mapData[ancestorMapID].parentMapID
|
|
end
|
|
-- Zone-redirect equivalence: if the viewed map and pin's map redirect to the same
|
|
-- target, they share the same coordinate space and should show each other's pins.
|
|
local isSameZoneSpace = (effectiveUiMapID == effectiveDataUiMapID) or sharesSunstriderSpace
|
|
|
|
if (Questie.db.profile.hideIconsOnContinents == true) and (HBD.mapData[uiMapID].mapType == Enum.UIMapType.Continent or uiMapID == 947) or (uiMapID ~= data.uiMapID and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT and not isChildMap and not isSameZoneSpace) then
|
|
icon:Hide();
|
|
return;
|
|
elseif(uiMapID == data.uiMapID and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT) or (isChildMap and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT) or (isSameZoneSpace and data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT) then
|
|
icon:Show();
|
|
end
|
|
|
|
if icon.type == "line" then return end
|
|
|
|
local x, y
|
|
if uiMapID == WORLD_MAP_ID then
|
|
-- should this pin show on the world map?
|
|
if uiMapID ~= data.uiMapID and data.worldMapShowFlag ~= HBD_PINS_WORLDMAP_SHOW_WORLD then return end
|
|
|
|
-- translate to the world map
|
|
x, y = HBD:GetAzerothWorldMapCoordinatesFromWorld(data.x, data.y, data.instanceID)
|
|
else
|
|
-- check that it matches the instance
|
|
if not HBD.mapData[uiMapID] or HBD.mapData[uiMapID].instance ~= data.instanceID then return end
|
|
|
|
if uiMapID ~= data.uiMapID then
|
|
local mapType = HBD.mapData[uiMapID].mapType
|
|
if not data.uiMapID then
|
|
if mapType == Enum.UIMapType.Continent and data.worldMapShowFlag >= HBD_PINS_WORLDMAP_SHOW_CONTINENT then
|
|
--pass
|
|
elseif mapType ~= Enum.UIMapType.Zone and mapType ~= Enum.UIMapType.Dungeon and mapType ~= Enum.UIMapType.Micro then
|
|
-- fail
|
|
return
|
|
end
|
|
else
|
|
local show = true -- Questie fix to show icons in neighbour areas
|
|
local parentMapID = HBD.mapData[data.uiMapID].parentMapID
|
|
while parentMapID and HBD.mapData[parentMapID] do
|
|
if parentMapID == uiMapID then
|
|
local parentMapType = HBD.mapData[parentMapID].mapType
|
|
-- show on any parent zones if they are normal zones
|
|
if data.worldMapShowFlag >= HBD_PINS_WORLDMAP_SHOW_PARENT and
|
|
(parentMapType == Enum.UIMapType.Zone or parentMapType == Enum.UIMapType.Dungeon or parentMapType == Enum.UIMapType.Micro) then
|
|
show = true
|
|
-- show on the continent
|
|
elseif data.worldMapShowFlag >= HBD_PINS_WORLDMAP_SHOW_CONTINENT and
|
|
parentMapType == Enum.UIMapType.Continent then
|
|
show = true
|
|
elseif data.worldMapShowFlag == HBD_PINS_WORLDMAP_SHOW_CURRENT then
|
|
-- Questie modifications!
|
|
show = false
|
|
end
|
|
break
|
|
-- worldmap is handled above already
|
|
else
|
|
parentMapID = HBD.mapData[parentMapID].parentMapID
|
|
end
|
|
end
|
|
|
|
if not show then return end
|
|
end
|
|
end
|
|
|
|
-- translate coordinates
|
|
-- Ascension: mapData[1241] has calibrated Sunstrider bounds matching
|
|
-- the game engine's coordinate space. Pins on zone 1241 use these
|
|
-- bounds for world coord conversion, so they align with the player.
|
|
x, y = HBD:GetZoneCoordinatesFromWorld(data.x, data.y, uiMapID)
|
|
-- [HBD-Pins] pin position debug disabled
|
|
end
|
|
|
|
if x and y then
|
|
icon:ClearAllPoints()
|
|
icon:SetPoint("CENTER", WorldMapButton, "TOPLEFT", x * worldmapWidth, -y * worldmapHeight)
|
|
icon:Show()
|
|
else
|
|
icon:Hide()
|
|
end
|
|
end
|
|
|
|
-- map event handling
|
|
local function UpdateMinimap()
|
|
UpdateMinimapZoom()
|
|
UpdateMinimapPins()
|
|
end
|
|
|
|
local function UpdateWorldMap()
|
|
if not WorldMapFrame:IsVisible() then return end
|
|
|
|
local scale = WorldMapButton:GetScale()
|
|
worldmapWidth = WorldMapButton:GetWidth()*scale
|
|
worldmapHeight = WorldMapButton:GetHeight()*scale
|
|
|
|
local mapScale = QuestieMap.GetScaleValue()
|
|
|
|
for icon, data in pairs(worldmapPins) do
|
|
icon:Hide()
|
|
--icon:ClearAllPoints()
|
|
|
|
QuestieMap.utils:RescaleIcon(icon, mapScale)
|
|
HandleWorldMapPin(icon, data)
|
|
end
|
|
end
|
|
pins.UpdateWorldMap = UpdateWorldMap
|
|
|
|
local last_update = 0
|
|
local function OnUpdateHandler(frame, elapsed)
|
|
last_update = last_update + elapsed
|
|
if last_update > 1 or queueFullUpdate then
|
|
UpdateMinimapPins(queueFullUpdate)
|
|
last_update = 0
|
|
queueFullUpdate = false
|
|
else
|
|
UpdateMinimapIconPosition()
|
|
end
|
|
end
|
|
pins.updateFrame:SetScript("OnUpdate", OnUpdateHandler)
|
|
|
|
local function OnEventHandler(frame, event, ...)
|
|
if event == "CVAR_UPDATE" then
|
|
local cvar, value = ...
|
|
if cvar == "ROTATE_MINIMAP" then
|
|
rotateMinimap = (value == "1")
|
|
queueFullUpdate = true
|
|
end
|
|
elseif event == "MINIMAP_UPDATE_ZOOM" then
|
|
UpdateMinimap()
|
|
elseif event == "PLAYER_LOGIN" then
|
|
-- recheck cvars after login
|
|
rotateMinimap = GetCVar("rotateMinimap") == "1"
|
|
elseif event == "PLAYER_ENTERING_WORLD" then
|
|
_InvalidatePositionCache()
|
|
UpdateMinimap()
|
|
UpdateWorldMap()
|
|
elseif event == "WORLD_MAP_UPDATE" then
|
|
UpdateWorldMap()
|
|
elseif string.find(event, "ZONE_CHANGED") then
|
|
_InvalidatePositionCache()
|
|
UpdateMinimap()
|
|
UpdateWorldMap()
|
|
end
|
|
end
|
|
|
|
pins.updateFrame:SetScript("OnEvent", OnEventHandler)
|
|
pins.updateFrame:RegisterEvent("CVAR_UPDATE")
|
|
pins.updateFrame:RegisterEvent("MINIMAP_UPDATE_ZOOM")
|
|
pins.updateFrame:RegisterEvent("WORLD_MAP_UPDATE")
|
|
pins.updateFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA")
|
|
pins.updateFrame:RegisterEvent("ZONE_CHANGED")
|
|
pins.updateFrame:RegisterEvent("ZONE_CHANGED_INDOORS")
|
|
pins.updateFrame:RegisterEvent("PLAYER_LOGIN")
|
|
pins.updateFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
|
|
-- Showing quest objectives does not trigger WORLD_MAP_UPDATE event
|
|
hooksecurefunc("WorldMapFrame_DisplayQuests", UpdateWorldMap)
|
|
|
|
--- Add a icon to the minimap (x/y world coordinate version)
|
|
-- Note: This API does not let you specify a map to limit the pin to, it'll be shown on all maps these coordinates are valid for.
|
|
-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier)
|
|
-- @param icon Icon Frame
|
|
-- @param instanceID Instance ID of the map to add the icon to
|
|
-- @param x X position in world coordinates
|
|
-- @param y Y position in world coordinates
|
|
-- @param floatOnEdge flag if the icon should float on the edge of the minimap when going out of range, or hide immediately (default false)
|
|
function pins:AddMinimapIconWorld(ref, icon, instanceID, x, y, floatOnEdge)
|
|
if not ref then
|
|
error(MAJOR..": AddMinimapIconWorld: 'ref' must not be nil", 2)
|
|
end
|
|
if type(icon) ~= "table" or not icon.SetPoint then
|
|
error(MAJOR..": AddMinimapIconWorld: 'icon' must be a frame", 2)
|
|
end
|
|
if type(instanceID) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then
|
|
error(MAJOR..": AddMinimapIconWorld: 'instanceID', 'x' and 'y' must be numbers", 2)
|
|
end
|
|
|
|
if not minimapPinRegistry[ref] then
|
|
minimapPinRegistry[ref] = {}
|
|
end
|
|
|
|
minimapPinRegistry[ref][icon] = true
|
|
|
|
local t = minimapPins[icon] or newCachedTable()
|
|
t.instanceID = instanceID
|
|
t.x = x
|
|
t.y = y
|
|
t.floatOnEdge = floatOnEdge
|
|
t.uiMapID = nil
|
|
t.showInParentZone = nil
|
|
|
|
minimapPins[icon] = t
|
|
queueFullUpdate = true
|
|
|
|
icon:SetParent(pins.MinimapGroup or pins.Minimap)
|
|
end
|
|
|
|
--- Add a icon to the minimap (UiMapID zone coordinate version)
|
|
-- The pin will only be shown on the map specified, or optionally its parent map if specified
|
|
-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier)
|
|
-- @param icon Icon Frame
|
|
-- @param uiMapID uiMapID of the map to place the icon on
|
|
-- @param x X position in local/point coordinates (0-1), relative to the zone
|
|
-- @param y Y position in local/point coordinates (0-1), relative to the zone
|
|
-- @param showInParentZone flag if the icon should be shown in its parent zone - ie. an icon in a microdungeon in the outdoor zone itself (default false)
|
|
-- @param floatOnEdge flag if the icon should float on the edge of the minimap when going out of range, or hide immediately (default false)
|
|
function pins:AddMinimapIconMap(ref, icon, uiMapID, x, y, showInParentZone, floatOnEdge)
|
|
if not ref then
|
|
error(MAJOR..": AddMinimapIconMap: 'ref' must not be nil", 2)
|
|
end
|
|
if type(icon) ~= "table" or not icon.SetPoint then
|
|
error(MAJOR..": AddMinimapIconMap: 'icon' must be a frame", 2)
|
|
end
|
|
if type(uiMapID) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then
|
|
error(MAJOR..": AddMinimapIconMap: 'uiMapID', 'x' and 'y' must be numbers", 2)
|
|
end
|
|
|
|
-- convert to world coordinates and use our known adding function
|
|
local xCoord, yCoord, instanceID = HBD:GetWorldCoordinatesFromZone(x, y, uiMapID)
|
|
if not xCoord then return end
|
|
|
|
-- Sunstrider is small enough that edge-floating objective pins read like
|
|
-- player-following pins. Hide out-of-radius pins instead.
|
|
if uiMapID == 1241 then
|
|
floatOnEdge = false
|
|
end
|
|
|
|
self:AddMinimapIconWorld(ref, icon, instanceID, xCoord, yCoord, floatOnEdge)
|
|
|
|
-- store extra information
|
|
minimapPins[icon].uiMapID = uiMapID
|
|
minimapPins[icon].showInParentZone = showInParentZone
|
|
-- store original 0-1 zone coords so drawMinimapPin can compute a
|
|
-- zone-space delta without going through world-conversion bounds
|
|
minimapPins[icon].zoneX = x
|
|
minimapPins[icon].zoneY = y
|
|
end
|
|
|
|
--- Check if a floating minimap icon is on the edge of the map
|
|
-- @param icon the minimap icon
|
|
function pins:IsMinimapIconOnEdge(icon)
|
|
if not icon then return false end
|
|
local data = minimapPins[icon]
|
|
if not data then return nil end
|
|
|
|
return data.onEdge
|
|
end
|
|
|
|
--- Remove a minimap icon
|
|
-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier)
|
|
-- @param icon Icon Frame
|
|
function pins:RemoveMinimapIcon(ref, icon)
|
|
if not ref or not icon or not minimapPinRegistry[ref] then return end
|
|
minimapPinRegistry[ref][icon] = nil
|
|
if minimapPins[icon] then
|
|
recycle(minimapPins[icon])
|
|
minimapPins[icon] = nil
|
|
activeMinimapPins[icon] = nil
|
|
end
|
|
icon:Hide()
|
|
end
|
|
|
|
--- Remove all minimap icons belonging to your addon (as tracked by "ref")
|
|
-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier)
|
|
function pins:RemoveAllMinimapIcons(ref)
|
|
if not ref or not minimapPinRegistry[ref] then return end
|
|
for icon in pairs(minimapPinRegistry[ref]) do
|
|
recycle(minimapPins[icon])
|
|
minimapPins[icon] = nil
|
|
activeMinimapPins[icon] = nil
|
|
icon:Hide()
|
|
end
|
|
wipe(minimapPinRegistry[ref])
|
|
end
|
|
|
|
--- Set the minimap object to position the pins on. Needs to support the usual functions a Minimap-type object exposes.
|
|
-- @param minimapObject The new minimap object, or nil to restore the default
|
|
function pins:SetMinimapObject(minimapObject)
|
|
pins.Minimap = minimapObject or Minimap
|
|
for pin in pairs(minimapPins) do
|
|
pin:SetParent(pins.Minimap)
|
|
end
|
|
UpdateMinimapPins(true)
|
|
end
|
|
|
|
-- world map constants
|
|
-- show worldmap pin only on zone map (Questie modification)
|
|
HBD_PINS_WORLDMAP_SHOW_CURRENT = -1
|
|
-- show worldmap pin on its parent zone map (if any)
|
|
HBD_PINS_WORLDMAP_SHOW_PARENT = 1
|
|
-- show worldmap pin on the continent map
|
|
HBD_PINS_WORLDMAP_SHOW_CONTINENT = 2
|
|
-- show worldmap pin on the continent and world map
|
|
HBD_PINS_WORLDMAP_SHOW_WORLD = 3
|
|
|
|
--- Add a icon to the world map (x/y world coordinate version)
|
|
-- Note: This API does not let you specify a map to limit the pin to, it'll be shown on all maps these coordinates are valid for.
|
|
-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier)
|
|
-- @param icon Icon Frame
|
|
-- @param instanceID Instance ID of the map to add the icon to
|
|
-- @param x X position in world coordinates
|
|
-- @param y Y position in world coordinates
|
|
-- @param showFlag Flag to control on which maps this pin will be shown
|
|
-- @param frameLevel Optional Frame Level type registered with the WorldMapFrame, defaults to PIN_FRAME_LEVEL_AREA_POI
|
|
function pins:AddWorldMapIconWorld(ref, icon, instanceID, x, y, showFlag, frameLevel)
|
|
if not ref then
|
|
error(MAJOR..": AddWorldMapIconWorld: 'ref' must not be nil", 2)
|
|
end
|
|
if type(icon) ~= "table" or not icon.SetPoint then
|
|
error(MAJOR..": AddWorldMapIconWorld: 'icon' must be a frame", 2)
|
|
end
|
|
if type(instanceID) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then
|
|
error(MAJOR..": AddWorldMapIconWorld: 'instanceID', 'x' and 'y' must be numbers", 2)
|
|
end
|
|
if showFlag ~= nil and type(showFlag) ~= "number" then
|
|
error(MAJOR..": AddWorldMapIconWorld: 'showFlag' must be a number (or nil)", 2)
|
|
end
|
|
|
|
if not worldmapPinRegistry[ref] then
|
|
worldmapPinRegistry[ref] = {}
|
|
end
|
|
|
|
worldmapPinRegistry[ref][icon] = true
|
|
|
|
local t = worldmapPins[icon] or newCachedTable()
|
|
t.instanceID = instanceID
|
|
t.x = x
|
|
t.y = y
|
|
t.uiMapID = nil
|
|
t.worldMapShowFlag = showFlag or 0
|
|
t.frameLevelType = frameLevel
|
|
|
|
worldmapPins[icon] = t
|
|
|
|
icon.icon = icon --LOL!
|
|
icon:SetParent(WorldMapButton)
|
|
HandleWorldMapPin(icon, t)
|
|
end
|
|
|
|
--- Add a icon to the world map (uiMapID zone coordinate version)
|
|
-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier)
|
|
-- @param icon Icon Frame
|
|
-- @param uiMapID uiMapID of the map to place the icon on
|
|
-- @param x X position in local/point coordinates (0-1), relative to the zone
|
|
-- @param y Y position in local/point coordinates (0-1), relative to the zone
|
|
-- @param showFlag Flag to control on which maps this pin will be shown
|
|
-- @param frameLevel Optional Frame Level type registered with the WorldMapFrame, defaults to PIN_FRAME_LEVEL_AREA_POI
|
|
function pins:AddWorldMapIconMap(ref, icon, uiMapID, x, y, showFlag, frameLevel)
|
|
if not ref then
|
|
error(MAJOR..": AddWorldMapIconMap: 'ref' must not be nil", 2)
|
|
end
|
|
if type(icon) ~= "table" or not icon.SetPoint then
|
|
error(MAJOR..": AddWorldMapIconMap: 'icon' must be a frame", 2)
|
|
end
|
|
if type(uiMapID) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then
|
|
error(MAJOR..": AddWorldMapIconMap: 'uiMapID', 'x' and 'y' must be numbers", 2)
|
|
end
|
|
if showFlag ~= nil and type(showFlag) ~= "number" then
|
|
error(MAJOR..": AddWorldMapIconMap: 'showFlag' must be a number (or nil)", 2)
|
|
end
|
|
|
|
-- convert to world coordinates
|
|
local xCoord, yCoord, instanceID = HBD:GetWorldCoordinatesFromZone(x, y, uiMapID)
|
|
if not xCoord then return end
|
|
|
|
if not worldmapPinRegistry[ref] then
|
|
worldmapPinRegistry[ref] = {}
|
|
end
|
|
|
|
worldmapPinRegistry[ref][icon] = true
|
|
|
|
local t = worldmapPins[icon] or newCachedTable()
|
|
t.instanceID = instanceID
|
|
t.x = xCoord
|
|
t.y = yCoord
|
|
t.uiMapID = uiMapID
|
|
t.worldMapShowFlag = showFlag or 0
|
|
t.frameLevelType = frameLevel
|
|
|
|
worldmapPins[icon] = t
|
|
|
|
icon.icon = icon --LOL!
|
|
icon:SetParent(WorldMapDetailFrame or WorldMapButton or WorldMapFrame)
|
|
HandleWorldMapPin(icon, t)
|
|
end
|
|
|
|
--- Remove a worldmap icon
|
|
-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier)
|
|
-- @param icon Icon Frame
|
|
function pins:RemoveWorldMapIcon(ref, icon)
|
|
if not ref or not icon or not worldmapPinRegistry[ref] then return end
|
|
worldmapPinRegistry[ref][icon] = nil
|
|
if worldmapPins[icon] then
|
|
recycle(worldmapPins[icon])
|
|
worldmapPins[icon] = nil
|
|
end
|
|
icon:Hide()
|
|
icon:ClearAllPoints()
|
|
icon:SetParent(UiParent)
|
|
end
|
|
|
|
--- Remove all worldmap icons belonging to your addon (as tracked by "ref")
|
|
-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier)
|
|
function pins:RemoveAllWorldMapIcons(ref)
|
|
if not ref or not worldmapPinRegistry[ref] then return end
|
|
for icon in pairs(worldmapPinRegistry[ref]) do
|
|
recycle(worldmapPins[icon])
|
|
worldmapPins[icon] = nil
|
|
icon:Hide()
|
|
icon:ClearAllPoints()
|
|
icon:SetParent(UiParent)
|
|
end
|
|
wipe(worldmapPinRegistry[ref])
|
|
end
|