/
-- HereBeDragons-Pins is a library to show pins/icons on the world map and minimap |
-- HereBeDragons-Pins-2.0 is not supported on WoW 7.x |
if select(4, GetBuildInfo()) < 80000 then |
return |
end |
local MAJOR, MINOR = "HereBeDragons-Pins-2.0", 5 |
assert(LibStub, MAJOR .. " requires LibStub") |
local pins, oldversion = LibStub:NewLibrary(MAJOR, MINOR) |
if not pins then return end |
local HBD = LibStub("HereBeDragons-2.0") |
pins.updateFrame = pins.updateFrame or CreateFrame("Frame") |
-- storage for minimap pins |
pins.minimapPins = pins.minimapPins or {} |
pins.activeMinimapPins = pins.activeMinimapPins or {} |
pins.minimapPinRegistry = pins.minimapPinRegistry or {} |
-- and worldmap pins |
pins.worldmapPins = pins.worldmapPins or {} |
pins.worldmapPinRegistry = pins.worldmapPinRegistry or {} |
pins.worldmapPinsPool = pins.worldmapPinsPool or CreateFramePool("FRAME") |
pins.worldmapProvider = pins.worldmapProvider or CreateFromMixins(MapCanvasDataProviderMixin) |
pins.worldmapProviderPin = pins.worldmapProviderPin or CreateFromMixins(MapCanvasPinMixin) |
-- store a reference to the active minimap object |
pins.Minimap = pins.Minimap or Minimap |
-- 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 |
-- upvalue data tables |
local minimapPins = pins.minimapPins |
local activeMinimapPins = pins.activeMinimapPins |
local minimapPinRegistry = pins.minimapPinRegistry |
local worldmapPins = pins.worldmapPins |
local worldmapPinRegistry = pins.worldmapPinRegistry |
local worldmapPinsPool = pins.worldmapPinsPool |
local worldmapProvider = pins.worldmapProvider |
local worldmapProviderPin = pins.worldmapProviderPin |
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 |
}, |
} |
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 |
local minimapScale, minimapShape, mapRadius, minimapWidth, minimapHeight, mapSin, mapCos |
local lastZoom, lastFacing, lastXY, lastYY |
local function drawMinimapPin(pin, data) |
local xDist, yDist = lastXY - data.x, lastYY - data.y |
-- handle rotation |
if rotateMinimap then |
local dx, dy = xDist, yDist |
xDist = dx*mapCos - dy*mapSin |
yDist = dx*mapSin + dy*mapCos |
end |
-- adapt delta position to the map radius |
local diffX = xDist / mapRadius |
local diffY = yDist / mapRadius |
-- different minimap shapes |
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 |
if dist <= 1 or data.floatOnEdge 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 IsParentMap(originMapId, toCheckMapId) |
local parentMapID = HBD.mapData[originMapId].parent |
while parentMapID and HBD.mapData[parentMapID] do |
local mapType = HBD.mapData[parentMapID].mapType |
if mapType ~= Enum.UIMapType.Zone and mapType ~= Enum.UIMapType.Dungeon and mapType ~= Enum.UIMapType.Micro then |
return false |
end |
if parentMapID == toCheckMapId then |
return true |
end |
parentMapID = HBD.mapData[parentMapID].parent |
end |
return false |
end |
local function UpdateMinimapPins(force) |
-- get the current player position |
local x, y, instanceID = HBD:GetPlayerWorldPosition() |
local mapID = HBD:GetPlayerZone() |
-- 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, data 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"] |
mapRadius = minimap_size[indoors][zoom] / 2 |
minimapWidth = pins.Minimap:GetWidth() / 2 |
minimapHeight = pins.Minimap:GetHeight() / 2 |
-- update upvalues for icon placement |
lastZoom = zoom |
lastFacing = facing |
lastXY, lastYY = x, y |
if rotateMinimap then |
mapSin = sin(facing) |
mapCos = cos(facing) |
end |
for pin, data in pairs(minimapPins) do |
if data.instanceID == instanceID and (not data.uiMapID or data.uiMapID == mapID or (data.showInParentZone and IsParentMap(data.uiMapID, mapID))) then |
activeMinimapPins[pin] = data |
data.keep = true |
-- draw the pin (this may reset data.keep if outside of the map) |
drawMinimapPin(pin, data) |
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 |
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 = HBD:GetPlayerWorldPosition() |
-- 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 |
mapRadius = minimap_size[indoors][zoom] / 2 |
-- 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() |
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 |
------------------------------------------------------------------------------------------- |
-- WorldMap data provider |
-- setup pin pool |
worldmapPinsPool.parent = WorldMapFrame:GetCanvas() |
worldmapPinsPool.creationFunc = function(framePool) |
local frame = CreateFrame(framePool.frameType, nil, framePool.parent) |
frame:SetSize(1, 1) |
return Mixin(frame, worldmapProviderPin) |
end |
worldmapPinsPool.resetterFunc = function(pinPool, pin) |
FramePool_HideAndClearAnchors(pinPool, pin) |
pin:OnReleased() |
pin.pinTemplate = nil |
pin.owningMap = nil |
end |
-- register pin pool with the world map |
WorldMapFrame.pinPools["HereBeDragonsPinsTemplate"] = worldmapPinsPool |
-- provider base API |
function worldmapProvider:RemoveAllData() |
self:GetMap():RemoveAllPinsByTemplate("HereBeDragonsPinsTemplate") |
end |
function worldmapProvider:RemovePinByIcon(icon) |
for pin in self:GetMap():EnumeratePinsByTemplate("HereBeDragonsPinsTemplate") do |
if pin.icon == icon then |
self:GetMap():RemovePin(pin) |
end |
end |
end |
function worldmapProvider:RemovePinsByRef(ref) |
for pin in self:GetMap():EnumeratePinsByTemplate("HereBeDragonsPinsTemplate") do |
if pin.icon and worldmapPinRegistry[ref][pin.icon] then |
self:GetMap():RemovePin(pin) |
end |
end |
end |
function worldmapProvider:RefreshAllData(fromOnShow) |
self:RemoveAllData() |
for icon, data in pairs(worldmapPins) do |
self:HandlePin(icon, data) |
end |
end |
function worldmapProvider:HandlePin(icon, data) |
local uiMapID = self:GetMap():GetMapID() |
-- check for a valid map |
if not uiMapID 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 = false |
local parentMapID = HBD.mapData[data.uiMapID].parent |
while parentMapID and HBD.mapData[parentMapID] do |
if parentMapID == uiMapID then |
local mapType = HBD.mapData[parentMapID].mapType |
-- show on any parent zones if they are normal zones |
if data.worldMapShowFlag >= HBD_PINS_WORLDMAP_SHOW_PARENT and |
(mapType == Enum.UIMapType.Zone or mapType == Enum.UIMapType.Dungeon or mapType == Enum.UIMapType.Micro) then |
show = true |
-- show on the continent |
elseif data.worldMapShowFlag >= HBD_PINS_WORLDMAP_SHOW_CONTINENT and |
mapType == Enum.UIMapType.Continent then |
show = true |
end |
break |
-- worldmap is handled above already |
else |
parentMapID = HBD.mapData[parentMapID].parent |
end |
end |
if not show then return end |
end |
end |
-- translate coordinates |
x, y = HBD:GetZoneCoordinatesFromWorld(data.x, data.y, uiMapID) |
end |
if x and y then |
self:GetMap():AcquirePin("HereBeDragonsPinsTemplate", icon, x, y) |
end |
end |
-- map pin base API |
function worldmapProviderPin:OnLoad() |
self:UseFrameLevelType("PIN_FRAME_LEVEL_AREA_POI") |
self:SetScalingLimits(1, 1.0, 1.2) |
end |
function worldmapProviderPin:OnAcquired(icon, x, y) |
self:SetPosition(x, y) |
self.icon = icon |
icon:SetParent(self) |
icon:ClearAllPoints() |
icon:SetPoint("CENTER", self, "CENTER") |
icon:Show() |
end |
function worldmapProviderPin:OnReleased() |
if self.icon then |
self.icon:Hide() |
self.icon:SetParent(UIParent) |
self.icon:ClearAllPoints() |
self.icon = nil |
end |
end |
-- register with the world map |
WorldMapFrame:AddDataProvider(worldmapProvider) |
-- map event handling |
local function UpdateMinimap() |
UpdateMinimapZoom() |
UpdateMinimapPins() |
end |
local function UpdateWorldMap() |
worldmapProvider:RefreshAllData() |
end |
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 |
UpdateMinimap() |
UpdateWorldMap() |
end |
end |
pins.updateFrame:SetScript("OnEvent", OnEventHandler) |
pins.updateFrame:UnregisterAllEvents() |
pins.updateFrame:RegisterEvent("CVAR_UPDATE") |
pins.updateFrame:RegisterEvent("MINIMAP_UPDATE_ZOOM") |
pins.updateFrame:RegisterEvent("PLAYER_LOGIN") |
pins.updateFrame:RegisterEvent("PLAYER_ENTERING_WORLD") |
HBD.RegisterCallback(pins, "PlayerZoneChanged", UpdateMinimap) |
--- 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") |
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.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") |
end |
if type(icon) ~= "table" or not icon.SetPoint then |
error(MAJOR..": AddMinimapIconMap: 'icon' must be a frame") |
end |
if type(uiMapID) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then |
error(MAJOR..": AddMinimapIconMap: 'uiMapID', 'x' and 'y' must be numbers") |
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 |
self:AddMinimapIconWorld(ref, icon, instanceID, xCoord, yCoord, floatOnEdge) |
-- store extra information |
minimapPins[icon].uiMapID = uiMapID |
minimapPins[icon].showInParentZone = showInParentZone |
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 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 |
function pins:AddWorldMapIconWorld(ref, icon, instanceID, x, y, showFlag) |
if not ref then |
error(MAJOR..": AddWorldMapIconWorld: 'ref' must not be nil") |
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 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 |
worldmapPins[icon] = t |
worldmapProvider:HandlePin(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 |
function pins:AddWorldMapIconMap(ref, icon, uiMapID, x, y, showFlag) |
if not ref then |
error(MAJOR..": AddWorldMapIconMap: 'ref' must not be nil") |
end |
if type(icon) ~= "table" or not icon.SetPoint then |
error(MAJOR..": AddWorldMapIconMap: 'icon' must be a frame") |
end |
if type(uiMapID) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then |
error(MAJOR..": AddWorldMapIconMap: 'uiMapID', 'x' and 'y' must be numbers") |
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 |
worldmapPins[icon] = t |
worldmapProvider:HandlePin(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 |
worldmapProvider:RemovePinByIcon(icon) |
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 |
end |
worldmapProvider:RemovePinsByRef(ref) |
wipe(worldmapPinRegistry[ref]) |
end |
--- Return the angle and distance from the player to the specified pin |
-- @param icon icon object (minimap or worldmap) |
-- @return angle, distance where angle is in radians and distance in yards |
function pins:GetVectorToIcon(icon) |
if not icon then return nil, nil end |
local data = minimapPins[icon] or worldmapPins[icon] |
if not data then return nil, nil end |
local x, y, instance = HBD:GetPlayerWorldPosition() |
if not x or not y or instance ~= data.instanceID then return nil end |
return HBD:GetWorldVector(instance, x, y, data.x, data.y) |
end |
## Interface: 80000 |
## Title: Lib: HereBeDragons |
## Notes: HereBeDragons is a data API for the World of Warcraft mapping system |
## Author: Nevcairiel |
## X-eMail: h.leppkes@gmail.com |
## X-Category: Library |
## X-License: BSD |
## X-Website: http://www.wowace.com/addons/herebedragons/ |
## Version: 2.00-release |
LibStub\LibStub.lua |
CallbackHandler-1.0\CallbackHandler-1.0.lua |
# WoW 7.x version |
HereBeDragons-1.0.lua |
HereBeDragons-Pins-1.0.lua |
# WoW 8.0+ version |
HereBeDragons-2.0.lua |
HereBeDragons-Pins-2.0.lua |
# Migration Data for 7.x -> 8.0 migration |
HereBeDragons-Migrate.lua |
-- HereBeDragons-Migrate is not supported on WoW 7.x or earlier |
if select(4, GetBuildInfo()) < 80000 then |
return |
end |
local MAJOR, MINOR = "HereBeDragons-Migrate", 2 |
assert(LibStub, MAJOR .. " requires LibStub") |
local HBDMigrate, oldversion = LibStub:NewLibrary(MAJOR, MINOR) |
if not HBDMigrate then return end |
local SetupMigrationData |
local MapMigrationData, mapFileToIdMap, uiMapIdToIdMap |
--- Return the uiMapId from the specified mapAreaId/floor combination |
-- @param mapId mapAreaId to lookup |
-- @param floor floor to lookup (if nil, the first floor will be used) |
-- @return The uiMapId corresponding to this map, if any |
function HBDMigrate:GetUIMapIDFromMapAreaId(mapId, floor) |
if not mapId then return nil end |
local data = MapMigrationData[mapId] |
if not data then return nil end |
if not floor then |
if data[0] then |
floor = 0 |
elseif data.defaultFloor then |
floor = data.defaultFloor |
else |
for i = 1, 50 do |
if data[i] then |
floor = i |
break |
end |
end |
data.defaultFloor = floor |
end |
end |
return data[floor] |
end |
--- Return the uiMapId from the specified mapFile/floor combination |
-- @param mapFile mapFile to lookup |
-- @param floor floor to lookup (if nil, the first floor will be used) |
-- @return The uiMapId corresponding to this map, if any |
function HBDMigrate:GetUIMapIDFromMapFile(mapFile, floor) |
if not mapFile then return nil end |
if not mapFileToIdMap then SetupMigrationData() end |
return self:GetUIMapIDFromMapAreaId(mapFileToIdMap[mapFile], floor) |
end |
--- Return the legacy map information for the specified uiMapId |
-- @param uiMapId uiMapId to lookup |
-- @return mapAreaId, floor, mapFile |
function HBDMigrate:GetLegacyMapInfo(uiMapId) |
if not uiMapId then return nil end |
if not uiMapIdToIdMap then SetupMigrationData() end |
local c = uiMapIdToIdMap[uiMapId] |
if not c then return end |
local m, f = floor(c / 10000), (c % 10000) |
return m, f, MapMigrationData[m].mapFile |
end |
MapMigrationData = { |
[4] = { mapFile = "Durotar", [0] = 1, [8] = 2, [12] = 5, [19] = 6, [11] = 4, [10] = 3}, |
[9] = { mapFile = "Mulgore", [0] = 7, [6] = 8, [7] = 9}, |
[11] = { mapFile = "Barrens", [0] = 10, [20] = 11}, |
[13] = { mapFile = "Kalimdor", [0] = 12}, |
[14] = { mapFile = "Azeroth", [0] = 13}, |
[16] = { mapFile = "Arathi", [0] = 14}, |
[17] = { mapFile = "Badlands", [0] = 15, [18] = 16}, |
[19] = { mapFile = "BlastedLands", [0] = 17}, |
[20] = { mapFile = "Tirisfal", [0] = 18, [13] = 19, [25] = 20}, |
[21] = { mapFile = "Silverpine", [0] = 21}, |
[22] = { mapFile = "WesternPlaguelands", [0] = 22}, |
[23] = { mapFile = "EasternPlaguelands", [0] = 23, [20] = 24}, |
[24] = { mapFile = "HillsbradFoothills", [0] = 25}, |
[26] = { mapFile = "Hinterlands", [0] = 26}, |
[27] = { mapFile = "DunMorogh", [6] = 28, [7] = 29, [11] = 31, [10] = 30, [0] = 27}, |
[28] = { mapFile = "SearingGorge", [0] = 32, [15] = 34, [14] = 33, [16] = 35}, |
[29] = { mapFile = "BurningSteppes", [0] = 36}, |
[30] = { mapFile = "Elwynn", [1] = 38, [2] = 39, [0] = 37, [19] = 40, [21] = 41}, |
[32] = { mapFile = "DeadwindPass", [0] = 42, [24] = 45, [22] = 43, [23] = 44, [27] = 46}, |
[758] = { mapFile = "TheBastionofTwilight", [1] = 294, [2] = 295, [3] = 296}, |
[886] = { mapFile = "TerraceOfEndlessSpring", [0] = 456}, |
[1014] = { mapFile = "Dalaran70", [0] = 625, [12] = 629, [4] = 626, [11] = 628, [10] = 627}, |
[759] = { mapFile = "HallsofOrigination", [1] = 297, [2] = 298, [3] = 299}, |
[887] = { mapFile = "SiegeofNiuzaoTemple", [1] = 458, [2] = 459, [0] = 457}, |
[1015] = { mapFile = "Azsuna", [0] = 630, [17] = 631, [19] = 633, [18] = 632}, |
[760] = { mapFile = "RazorfenDowns", [1] = 300}, |
[888] = { mapFile = "ShadowglenStart", [0] = 460}, |
[761] = { mapFile = "RazorfenKraul", [1] = 301}, |
[889] = { mapFile = "ValleyofTrialsStart", [0] = 461}, |
[1017] = { mapFile = "Stormheim", [1] = 635, [0] = 634, [28] = 640, [27] = 639, [26] = 638, [9] = 636, [25] = 637}, |
[762] = { mapFile = "ScarletMonastery", [1] = 302, [2] = 303, [3] = 304, [4] = 305}, |
[890] = { mapFile = "CampNaracheStart", [0] = 462}, |
[1018] = { mapFile = "Valsharah", [0] = 641, [13] = 642, [15] = 644, [14] = 643}, |
[763] = { mapFile = "Scholomance", [1] = 306, [2] = 307, [3] = 308, [4] = 309}, |
[891] = { mapFile = "EchoIslesStart", [0] = 463, [9] = 464}, |
[510] = { mapFile = "CrystalsongForest", [0] = 127}, |
[40] = { mapFile = "Wetlands", [0] = 56}, |
[764] = { mapFile = "ShadowfangKeep", [1] = 310, [2] = 311, [3] = 312, [4] = 313, [5] = 314, [6] = 315, [7] = 316}, |
[892] = { mapFile = "DeathknellStart", [0] = 465, [12] = 466}, |
[1020] = { mapFile = "TwistingNether70", [0] = 645}, |
[765] = { mapFile = "Stratholme", [1] = 317, [2] = 318}, |
[893] = { mapFile = "SunstriderIsleStart", [0] = 467}, |
[1021] = { mapFile = "BrokenShore", [1] = 647, [2] = 648, [0] = 646}, |
[766] = { mapFile = "AhnQiraj", [1] = 319, [2] = 320, [3] = 321}, |
[894] = { mapFile = "AmmenValeStart", [0] = 468}, |
[1022] = { mapFile = "Helheim", [0] = 649}, |
[767] = { mapFile = "ThroneofTides", [1] = 322, [2] = 323}, |
[895] = { mapFile = "NewTinkertownStart", [0] = 469, [8] = 470}, |
[512] = { mapFile = "StrandoftheAncients", [0] = 128}, |
[640] = { mapFile = "Deepholm", [1] = 208, [2] = 209, [0] = 207}, |
[768] = { mapFile = "TheStonecore", [1] = 324}, |
[896] = { mapFile = "MogushanVaults", [1] = 471, [2] = 472, [3] = 473}, |
[1024] = { mapFile = "Highmountain", [0] = 650, [29] = 657, [8] = 653, [16] = 654, [5] = 651, [40] = 660, [20] = 655, [21] = 656, [6] = 652, [31] = 659, [30] = 658}, |
[321] = { mapFile = "Orgrimmar", [1] = 86, [0] = 85}, |
[769] = { mapFile = "Skywall", [1] = 325}, |
[897] = { mapFile = "HeartofFear", [1] = 474, [2] = 475}, |
[1026] = { mapFile = "HellfireRaid", [1] = 662, [2] = 663, [3] = 664, [4] = 665, [5] = 666, [6] = 667, [7] = 668, [8] = 669, [9] = 670, [0] = 661}, |
[161] = { mapFile = "Tanaris", [0] = 71, [17] = 74, [15] = 72, [16] = 73, [18] = 75}, |
[1027] = { mapFile = "AraukNashalIntroScenario", [0] = 671}, |
[898] = { mapFile = "Scholomance", [1] = 476, [2] = 477, [3] = 478, [4] = 479}, |
[1028] = { mapFile = "MardumtheShatteredAbyss", [1] = 673, [2] = 674, [3] = 675, [0] = 672}, |
[899] = { mapFile = "ProvingGrounds", [1] = 480}, |
[772] = { mapFile = "AhnQirajTheFallenKingdom", [0] = 327}, |
[900] = { mapFile = "AncientMoguCrypt", [1] = 481, [2] = 482}, |
[1032] = { mapFile = "VaultOfTheWardensDH", [1] = 677, [2] = 678, [3] = 679}, |
[81] = { mapFile = "StonetalonMountains", [0] = 65}, |
[773] = { mapFile = "ThroneoftheFourWinds", [1] = 328}, |
[1034] = { mapFile = "HelmouthShallows", [0] = 694}, |
[1035] = { mapFile = "ValhallasWarriorOrderHome", [1] = 695}, |
[775] = { mapFile = "CoTMountHyjal", [0] = 329}, |
[520] = { mapFile = "TheNexus", [1] = 129}, |
[776] = { mapFile = "GruulsLair", [1] = 330}, |
[521] = { mapFile = "CoTStratholme", [1] = 131, [0] = 130}, |
[1041] = { mapFile = "HallsofValor", [1] = 704, [2] = 705, [0] = 703}, |
[522] = { mapFile = "Ahnkahet", [1] = 132}, |
[906] = { mapFile = "DustwallowMarshScenarioAlliance", [0] = 483}, |
[523] = { mapFile = "UtgardeKeep", [1] = 133, [2] = 134, [3] = 135}, |
[779] = { mapFile = "MagtheridonsLair", [1] = 331}, |
[524] = { mapFile = "UtgardePinnacle", [1] = 136, [2] = 137}, |
[41] = { mapFile = "Teldrassil", [2] = 58, [3] = 59, [4] = 60, [0] = 57, [5] = 61}, |
[780] = { mapFile = "CoilfangReservoir", [1] = 332}, |
[525] = { mapFile = "HallsofLightning", [1] = 138, [2] = 139}, |
[781] = { mapFile = "ZulAman", [0] = 333}, |
[526] = { mapFile = "Ulduar77", [1] = 140}, |
[782] = { mapFile = "TempestKeep", [1] = 334}, |
[527] = { mapFile = "TheEyeofEternity", [1] = 141}, |
[911] = { mapFile = "KrasarangAlliance", [0] = 486}, |
[528] = { mapFile = "Nexus80", [1] = 143, [2] = 144, [3] = 145, [4] = 146, [0] = 142}, |
[912] = { mapFile = "KrasarangPatience", [0] = 487}, |
[529] = { mapFile = "Ulduar", [1] = 148, [2] = 149, [3] = 150, [4] = 151, [5] = 152, [0] = 147}, |
[1057] = { mapFile = "MaelstromShaman", [0] = 726}, |
[530] = { mapFile = "Gundrak", [1] = 154, [0] = 153}, |
[1059] = { mapFile = "TerraceofEndlessSpringScenario", [0] = 728}, |
[914] = { mapFile = "VoljinScenario", [1] = 489, [0] = 488}, |
[531] = { mapFile = "TheObsidianSanctum", [0] = 155}, |
[532] = { mapFile = "VaultofArchavon", [1] = 156}, |
[533] = { mapFile = "AzjolNerub", [1] = 157, [2] = 158, [3] = 159}, |
[789] = { mapFile = "SunwellPlateau", [1] = 336, [0] = 335}, |
[534] = { mapFile = "DrakTharonKeep", [1] = 160, [2] = 161}, |
[1067] = { mapFile = "DarkheartThicket", [0] = 733}, |
[535] = { mapFile = "Naxxramas", [1] = 162, [2] = 163, [3] = 164, [4] = 165, [5] = 166, [6] = 167}, |
[1069] = { mapFile = "TheBeyond", [1] = 736}, |
[919] = { mapFile = "BlackTempleScenario", [1] = 491, [2] = 492, [3] = 493, [4] = 494, [5] = 495, [6] = 496, [7] = 497, [0] = 490}, |
[536] = { mapFile = "VioletHold", [1] = 168}, |
[1071] = { mapFile = "FirelandsShaman", [0] = 738}, |
[920] = { mapFile = "KrasarangHorde", [0] = 498}, |
[1072] = { mapFile = "TrueshotLodge", [0] = 739}, |
[793] = { mapFile = "ZulGurub", [0] = 337}, |
[461] = { mapFile = "ArathiBasin", [0] = 93}, |
[1075] = { mapFile = "AbyssalMawShamanAcquisition", [1] = 742, [2] = 743}, |
[922] = { mapFile = "DeeprunTram", [1] = 499, [2] = 500}, |
[1076] = { mapFile = "UlduarMagni", [1] = 744, [2] = 745, [3] = 746}, |
[795] = { mapFile = "MoltenFront", [0] = 338}, |
[462] = { mapFile = "EversongWoods", [0] = 94}, |
[34] = { mapFile = "Duskwood", [0] = 47}, |
[42] = { mapFile = "Darkshore", [0] = 62}, |
[796] = { mapFile = "BlackTemple", [1] = 340, [2] = 341, [3] = 342, [4] = 343, [5] = 344, [6] = 345, [7] = 346, [0] = 339}, |
[924] = { mapFile = "DalaranCity", [1] = 501, [2] = 502}, |
[541] = { mapFile = "HrothgarsLanding", [0] = 170}, |
[797] = { mapFile = "HellfireRamparts", [1] = 347}, |
[925] = { mapFile = "BrawlgarArena", [1] = 503}, |
[542] = { mapFile = "TheArgentColiseum", [1] = 171}, |
[798] = { mapFile = "MagistersTerrace", [1] = 348, [2] = 349}, |
[543] = { mapFile = "TheArgentColiseum", [1] = 172, [2] = 173}, |
[799] = { mapFile = "Karazhan", [1] = 350, [2] = 351, [3] = 352, [4] = 353, [5] = 354, [6] = 355, [7] = 356, [8] = 357, [9] = 358, [10] = 359, [11] = 360, [12] = 361, [13] = 362, [14] = 363, [15] = 364, [16] = 365, [17] = 366}, |
[464] = { mapFile = "AzuremystIsle", [0] = 97, [2] = 98, [3] = 99}, |
[544] = { mapFile = "TheLostIsles", [1] = 175, [2] = 176, [3] = 177, [4] = 178, [0] = 174}, |
[800] = { mapFile = "Firelands", [1] = 368, [2] = 369, [0] = 367}, |
[928] = { mapFile = "IsleoftheThunderKing", [1] = 505, [2] = 506, [0] = 504}, |
[545] = { mapFile = "Gilneas", [1] = 180, [2] = 181, [3] = 182, [0] = 179}, |
[673] = { mapFile = "TheCapeOfStranglethorn", [0] = 210}, |
[401] = { mapFile = "AlteracValley", [0] = 91}, |
[929] = { mapFile = "IsleOfGiants", [0] = 507}, |
[1090] = { mapFile = "TolBaradWarlockScenario", [1] = 774, [0] = 773}, |
[201] = { mapFile = "UngoroCrater", [0] = 78, [14] = 79}, |
[930] = { mapFile = "ThunderKingRaid", [1] = 508, [2] = 509, [3] = 510, [4] = 511, [5] = 512, [6] = 513, [7] = 514, [8] = 515}, |
[1092] = { mapFile = "AzuremystIsleScenario", [0] = 776}, |
[803] = { mapFile = "TheNexusLegendary", [1] = 370}, |
[466] = { mapFile = "Expansion01", [0] = 101}, |
[1094] = { mapFile = "NightmareRaid", [1] = 777, [2] = 778, [3] = 779, [4] = 780, [5] = 781, [6] = 782, [7] = 783, [8] = 784, [9] = 785, [10] = 786, [11] = 787, [12] = 788, [13] = 789}, |
[1096] = { mapFile = "AszunaDungeonExterior", [0] = 790}, |
[101] = { mapFile = "Desolace", [0] = 66, [22] = 68, [21] = 67}, |
[933] = { mapFile = "IsleoftheThunderKingScenario", [1] = 517, [0] = 516}, |
[806] = { mapFile = "TheJadeForest", [6] = 372, [7] = 373, [15] = 374, [16] = 375, [0] = 371}, |
[934] = { mapFile = "ThunderKingLootRoom", [1] = 518}, |
[1100] = { mapFile = "KarazhanScenario", [1] = 794, [2] = 795, [3] = 796, [4] = 797}, |
[807] = { mapFile = "ValleyoftheFourWinds", [0] = 376, [14] = 377}, |
[935] = { mapFile = "GoldRush", [0] = 519}, |
[1102] = { mapFile = "ArcwayScenario", [1] = 798}, |
[680] = { mapFile = "Ragefire", [1] = 213}, |
[808] = { mapFile = "TheWanderingIsle", [0] = 378}, |
[1104] = { mapFile = "MageCampaignTheOculus", [1] = 800, [2] = 801, [3] = 802, [4] = 803, [0] = 799}, |
[341] = { mapFile = "Ironforge", [0] = 87}, |
[809] = { mapFile = "KunLaiSummit", [0] = 379, [8] = 380, [9] = 381, [10] = 382, [20] = 386, [11] = 383, [21] = 387, [12] = 384, [17] = 385}, |
[937] = { mapFile = "ValeOfEternalBlossomsScenario", [1] = 521, [0] = 520}, |
[810] = { mapFile = "TownlongWastes", [0] = 388, [13] = 389}, |
[938] = { mapFile = "EmberdeepScenario", [1] = 522}, |
[811] = { mapFile = "ValeofEternalBlossoms", [1] = 391, [2] = 392, [3] = 393, [4] = 394, [0] = 390, [19] = 396, [18] = 395}, |
[939] = { mapFile = "DunMoroghScenario", [0] = 523}, |
[35] = { mapFile = "LochModan", [0] = 48}, |
[43] = { mapFile = "Ashenvale", [0] = 63}, |
[940] = { mapFile = "tempKrasarangHordeBase", [0] = 524}, |
[685] = { mapFile = "RuinsofGilneasCity", [0] = 218}, |
[813] = { mapFile = "NetherstormArena", [0] = 397}, |
[471] = { mapFile = "TheExodar", [0] = 103}, |
[1114] = { mapFile = "HelheimRaid", [1] = 807, [2] = 808, [0] = 806}, |
[686] = { mapFile = "ZulFarrak", [0] = 219}, |
[1115] = { mapFile = "LegionKarazhanDungeon", [1] = 809, [2] = 810, [3] = 811, [4] = 812, [5] = 813, [6] = 814, [7] = 815, [8] = 816, [9] = 817, [10] = 818, [11] = 819, [12] = 820, [13] = 821, [14] = 822}, |
[1116] = { mapFile = "PitofSaronDK", [0] = 823}, |
[687] = { mapFile = "TheTempleOfAtalHakkar", [1] = 220}, |
[688] = { mapFile = "BlackfathomDeeps", [1] = 221, [2] = 222, [3] = 223}, |
[816] = { mapFile = "WellofEternity", [0] = 398}, |
[281] = { mapFile = "Winterspring", [0] = 83}, |
[689] = { mapFile = "StranglethornVale", [0] = 224}, |
[473] = { mapFile = "ShadowmoonValley", [0] = 104}, |
[141] = { mapFile = "Dustwallow", [0] = 70}, |
[690] = { mapFile = "TheStockade", [1] = 225}, |
[946] = { mapFile = "Talador", [0] = 535, [13] = 536, [14] = 537, [30] = 538}, |
[691] = { mapFile = "Gnomeregan", [1] = 226, [2] = 227, [3] = 228, [4] = 229}, |
[819] = { mapFile = "HourofTwilight", [1] = 400, [0] = 399}, |
[947] = { mapFile = "ShadowmoonValleyDR", [0] = 539, [22] = 541, [15] = 540}, |
[1126] = {[0] = 824}, |
[692] = { mapFile = "Uldaman", [1] = 230, [2] = 231}, |
[820] = { mapFile = "EndTime", [1] = 402, [2] = 403, [3] = 404, [4] = 405, [5] = 406, [0] = 401}, |
[948] = { mapFile = "SpiresOfArak", [0] = 542}, |
[181] = { mapFile = "Aszhara", [0] = 76}, |
[1220] = {[0] = 981}, |
[1129] = { mapFile = "CaveoftheBloodtotemScenario", [1] = 826}, |
[949] = { mapFile = "Gorgrond", [0] = 543, [17] = 545, [21] = 549, [20] = 548, [19] = 547, [16] = 544, [18] = 546}, |
[1130] = { mapFile = "StratholmePaladinClassMount", [1] = 827}, |
[1219] = {[1] = 975, [2] = 976, [3] = 977, [4] = 978, [5] = 979, [6] = 980, [0] = 974}, |
[1131] = { mapFile = "TheEyeofEternityMageClassMount", [1] = 828}, |
[950] = { mapFile = "NagrandDraenor", [11] = 552, [12] = 553, [0] = 550, [10] = 551}, |
[1132] = { mapFile = "HallsOfValorWarriorClassMount", [1] = 829}, |
[1050] = { mapFile = "WarlockClassShrine", [0] = 717}, |
[823] = { mapFile = "DarkmoonFaireIsland", [1] = 408, [0] = 407}, |
[476] = { mapFile = "BloodmystIsle", [0] = 106}, |
[1216] = { mapFile = "VoidElfScenario", [0] = 972}, |
[696] = { mapFile = "MoltenCore", [1] = 232}, |
[824] = { mapFile = "DragonSoul", [1] = 410, [2] = 411, [3] = 412, [4] = 413, [5] = 414, [6] = 415, [0] = 409}, |
[1215] = { mapFile = "VoidElfHub", [0] = 971}, |
[1136] = { mapFile = "ColdridgeValleyScenario", [0] = 834}, |
[697] = { mapFile = "ZulGurub", [0] = 233}, |
[1137] = { mapFile = "TheDeadminesPetBattle", [1] = 835, [2] = 836}, |
[477] = { mapFile = "Nagrand", [0] = 107}, |
[1052] = { mapFile = "DemonHunterOrderHallTerrain", [1] = 720, [2] = 721, [0] = 719}, |
[1054] = { mapFile = "TheVioletHoldAcquisition", [1] = 723}, |
[1139] = { mapFile = "ArathiBasinWinter", [0] = 837}, |
[1212] = { mapFile = "LightforgedVindicaar", [1] = 940, [2] = 941}, |
[1140] = { mapFile = "BattleforBlackrockMountain", [0] = 838}, |
[699] = { mapFile = "DireMaul", [1] = 235, [2] = 236, [3] = 237, [4] = 238, [5] = 239, [6] = 240, [0] = 234}, |
[1211] = {[0] = 939}, |
[478] = { mapFile = "TerokkarForest", [0] = 108}, |
[36] = { mapFile = "Redridge", [0] = 49}, |
[700] = { mapFile = "TwilightHighlands", [0] = 241}, |
[1143] = { mapFile = "GnomereganPetBattle", [1] = 840, [2] = 841, [3] = 842}, |
[1210] = {[0] = 938}, |
[1144] = { mapFile = "SmallBattlegroundC", [0] = 843}, |
[1066] = { mapFile = "LegionVioletHoldDungeon", [1] = 732}, |
[1145] = {[0] = 844}, |
[479] = { mapFile = "Netherstorm", [0] = 109}, |
[1146] = { mapFile = "TombofSargerasDungeon", [1] = 845, [2] = 846, [3] = 847, [4] = 848, [5] = 849}, |
[1204] = {[1] = 934, [2] = 935}, |
[1147] = { mapFile = "TombRaid", [1] = 850, [2] = 851, [3] = 852, [4] = 853, [5] = 854, [6] = 855, [7] = 856}, |
[1202] = { mapFile = "LightforgedDraeneiSwamp", [0] = 933}, |
[1148] = { mapFile = "ThroneoftheFourWinds", [1] = 857}, |
[1201] = { mapFile = "InvasionPointVal", [0] = 932}, |
[1149] = { mapFile = "AssaultonBrokenShoreScenario", [0] = 858}, |
[480] = { mapFile = "SilvermoonCity", [0] = 110}, |
[1150] = {[0] = 859}, |
[704] = { mapFile = "BlackrockDepths", [1] = 242, [2] = 243}, |
[1151] = { mapFile = "TheRubySanctumDKMountScenario", [0] = 860}, |
[1200] = { mapFile = "InvasionPointSangua", [0] = 931}, |
[1152] = { mapFile = "FelwingLedgeMardumArea", [0] = 861}, |
[1199] = { mapFile = "InvasionPointNaigtal", [0] = 930}, |
[1153] = {[0] = 862}, |
[481] = { mapFile = "ShattrathCity", [0] = 111}, |
[1154] = {[0] = 863}, |
[1068] = { mapFile = "MageClassShrine", [1] = 734, [2] = 735}, |
[1155] = {[0] = 864}, |
[241] = { mapFile = "Moonglade", [0] = 80}, |
[1156] = { mapFile = "StormheimInvasionScenario", [1] = 865, [2] = 866}, |
[1070] = { mapFile = "TheVortexPinnacle", [1] = 737}, |
[1157] = { mapFile = "AzsunaInvasionScenario", [1] = 867}, |
[482] = { mapFile = "NetherstormArena", [0] = 112}, |
[1158] = { mapFile = "ValsharahInvasionScenario", [1] = 868}, |
[708] = { mapFile = "TolBarad", [0] = 244}, |
[1159] = { mapFile = "HighmountainInvasionScenario", [1] = 869, [2] = 870}, |
[964] = { mapFile = "OgreMines", [1] = 573}, |
[1160] = { mapFile = "LostGlacierDKMountScenario", [0] = 871}, |
[709] = { mapFile = "TolBaradDailyArea", [0] = 245}, |
[1161] = { mapFile = "StormstoutBreweryScenario", [1] = 873, [2] = 874, [0] = 872}, |
[121] = { mapFile = "Feralas", [0] = 69}, |
[1162] = {[0] = 875}, |
[710] = { mapFile = "TheShatteredHalls", [1] = 246}, |
[1163] = {[0] = 876}, |
[1073] = { mapFile = "ArtifactSubtletyRogueAcquisition", [1] = 740, [2] = 741}, |
[1164] = { mapFile = "HallsofValor", [0] = 877}, |
[1078] = { mapFile = "Niskara", [0] = 748}, |
[1165] = { mapFile = "DemonHunterOrderHallTerrain", [1] = 879, [2] = 880, [0] = 878}, |
[1079] = { mapFile = "SuamarCatacombsDungeon", [1] = 749}, |
[1166] = { mapFile = "TheEyeofEternityMageClassMount", [1] = 881}, |
[1080] = { mapFile = "ThunderTotem", [0] = 750}, |
[1081] = { mapFile = "BlackRookHoldDungeon", [1] = 751, [2] = 752, [3] = 753, [4] = 754, [5] = 755, [6] = 756}, |
[1082] = { mapFile = "UrsocsLairScenario", [0] = 757}, |
[1084] = { mapFile = "GloamingReef", [0] = 758}, |
[1085] = { mapFile = "70BlackTempleLegion", [1] = 759}, |
[1086] = { mapFile = "MalornesNightmare", [0] = 760}, |
[485] = { mapFile = "Northrend", [0] = 113}, |
[1170] = { mapFile = "ArgusMacAree", [0] = 882, [3] = 883, [4] = 884}, |
[1087] = { mapFile = "SuramarNoblesDistrict", [1] = 762, [2] = 763, [0] = 761}, |
[1171] = { mapFile = "ArgusCore", [0] = 885, [6] = 887, [5] = 886}, |
[970] = { mapFile = "TanaanJungleIntro", [1] = 578, [0] = 577}, |
[1172] = { mapFile = "HallOfCommunion", [1] = 888}, |
[1091] = { mapFile = "TheExodar", [0] = 775}, |
[1173] = { mapFile = "TKArcatrazScenario", [1] = 889, [2] = 890}, |
[486] = { mapFile = "BoreanTundra", [0] = 114}, |
[37] = { mapFile = "StranglethornJungle", [0] = 50}, |
[1097] = { mapFile = "ArtifactBrewmasterScenario", [1] = 791, [2] = 792}, |
[1175] = {[0] = 895}, |
[61] = { mapFile = "ThousandNeedles", [0] = 64}, |
[1176] = {[0] = 896}, |
[717] = { mapFile = "RuinsofAhnQiraj", [0] = 247}, |
[1177] = { mapFile = "DragonblightChromieScenario", [1] = 898, [2] = 899, [3] = 900, [4] = 901, [5] = 902, [0] = 897}, |
[973] = { mapFile = "garrisonsmvalliance_tier1", [0] = 582}, |
[1178] = { mapFile = "ArgusDungeon", [0] = 903}, |
[718] = { mapFile = "OnyxiasLair", [1] = 248}, |
[1099] = { mapFile = "BlackRookHoldScenario", [0] = 793}, |
[1174] = { mapFile = "AzuremystScenario", [1] = 892, [2] = 893, [3] = 894, [0] = 891}, |
[1142] = { mapFile = "PriestClassMountScenario", [1] = 839}, |
[1135] = { mapFile = "ArgusSurface", [1] = 831, [2] = 832, [0] = 830, [7] = 833}, |
[1127] = { mapFile = "WailingCavernsPetBattle", [1] = 825}, |
[488] = { mapFile = "Dragonblight", [0] = 115}, |
[1105] = { mapFile = "ScarletMonestaryDK", [1] = 804, [2] = 805}, |
[720] = { mapFile = "Uldum", [0] = 249}, |
[1183] = { mapFile = "SilithusBrawl", [0] = 904}, |
[976] = { mapFile = "garrisonffhorde", [27] = 586, [28] = 587, [26] = 585}, |
[1184] = { mapFile = "Argus", [0] = 994}, |
[721] = { mapFile = "BlackrockSpire", [1] = 250, [2] = 251, [3] = 252, [4] = 253, [5] = 254, [6] = 255}, |
[1185] = {[0] = 906}, |
[1088] = { mapFile = "SuramarRaid", [1] = 764, [2] = 765, [3] = 766, [4] = 767, [5] = 768, [6] = 769, [7] = 770, [8] = 771, [9] = 772}, |
[1186] = { mapFile = "AzeriteBG", [0] = 907}, |
[722] = { mapFile = "AuchenaiCrypts", [1] = 256, [2] = 257}, |
[1187] = {[0] = 908}, |
[978] = { mapFile = "Ashran", [0] = 588, [29] = 589}, |
[1188] = { mapFile = "ArgusRaid", [1] = 910, [2] = 911, [3] = 912, [4] = 913, [5] = 914, [6] = 915, [7] = 916, [8] = 917, [9] = 918, [10] = 919, [11] = 920, [0] = 909}, |
[723] = { mapFile = "SethekkHalls", [1] = 258, [2] = 259}, |
[851] = { mapFile = "DustwallowMarshScenario", [0] = 416}, |
[490] = { mapFile = "GrizzlyHills", [0] = 116}, |
[1190] = { mapFile = "InvasionPointAurinor", [0] = 921}, |
[724] = { mapFile = "ShadowLabyrinth", [1] = 260}, |
[1191] = { mapFile = "InvasionPointBonich", [0] = 922}, |
[980] = { mapFile = "garrisonffhorde_tier1", [0] = 590}, |
[1192] = { mapFile = "InvasionPointCengar", [0] = 923}, |
[725] = { mapFile = "TheBloodFurnace", [1] = 261}, |
[1193] = { mapFile = "InvasionPointNaigtal", [0] = 924}, |
[491] = { mapFile = "HowlingFjord", [0] = 117}, |
[1194] = { mapFile = "InvasionPointSangua", [0] = 925}, |
[726] = { mapFile = "TheUnderbog", [1] = 262}, |
[1195] = { mapFile = "InvasionPointVal", [0] = 926}, |
[1077] = { mapFile = "TheDreamgrove", [0] = 747}, |
[1196] = { mapFile = "InvasionPointAurinor", [0] = 927}, |
[727] = { mapFile = "TheSteamvault", [1] = 263, [2] = 264}, |
[1197] = { mapFile = "InvasionPointBonich", [0] = 928}, |
[492] = { mapFile = "IcecrownGlacier", [0] = 118}, |
[1198] = { mapFile = "InvasionPointCengar", [0] = 929}, |
[728] = { mapFile = "TheSlavePens", [1] = 265}, |
[856] = { mapFile = "TempleofKotmogu", [0] = 417}, |
[984] = { mapFile = "DraenorAuchindoun", [1] = 593}, |
[601] = { mapFile = "TheForgeofSouls", [1] = 183}, |
[729] = { mapFile = "TheBotanica", [1] = 266}, |
[857] = { mapFile = "Krasarang", [1] = 419, [2] = 420, [3] = 421, [0] = 418}, |
[493] = { mapFile = "SholazarBasin", [0] = 119}, |
[602] = { mapFile = "PitofSaron", [0] = 184}, |
[730] = { mapFile = "TheMechanar", [1] = 267, [2] = 268}, |
[858] = { mapFile = "DreadWastes", [0] = 422}, |
[986] = { mapFile = "TaladorScenario", [0] = 594}, |
[603] = { mapFile = "HallsofReflection", [1] = 185}, |
[731] = { mapFile = "TheArcatraz", [1] = 269, [2] = 270, [3] = 271}, |
[1205] = {[0] = 936}, |
[987] = { mapFile = "IronDocks", [1] = 595}, |
[38] = { mapFile = "SwampOfSorrows", [0] = 51}, |
[732] = { mapFile = "ManaTombs", [1] = 272}, |
[860] = { mapFile = "STVDiamondMineBG", [1] = 423}, |
[988] = { mapFile = "FoundryRaid", [1] = 596, [2] = 597, [3] = 598, [4] = 599, [5] = 600}, |
[605] = { mapFile = "Kezan", [6] = 196, [7] = 197, [5] = 195, [0] = 194}, |
[733] = { mapFile = "CoTTheBlackMorass", [0] = 273}, |
[1065] = { mapFile = "NeltharionsLair", [0] = 731}, |
[495] = { mapFile = "TheStormPeaks", [0] = 120}, |
[606] = { mapFile = "Hyjal", [0] = 198}, |
[734] = { mapFile = "CoTHillsbradFoothills", [0] = 274}, |
[862] = { mapFile = "Pandaria", [0] = 424}, |
[1060] = { mapFile = "DeepholmShamanAcquisition", [1] = 729}, |
[607] = { mapFile = "SouthernBarrens", [0] = 199}, |
[1056] = { mapFile = "MaelstromShamanHubIntro", [0] = 725}, |
[1213] = {[0] = 942}, |
[496] = { mapFile = "ZulDrak", [0] = 121}, |
[1214] = {[0] = 943}, |
[736] = { mapFile = "GilneasBattleground2", [0] = 275}, |
[864] = { mapFile = "Northshire", [0] = 425, [3] = 426}, |
[1051] = { mapFile = "DreadscarRift", [0] = 718}, |
[609] = { mapFile = "TheRubySanctum", [0] = 200}, |
[737] = { mapFile = "TheMaelstrom", [0] = 276}, |
[1217] = { mapFile = "TheSunwellUnlockScenario", [1] = 973}, |
[993] = { mapFile = "BlackrockTrainDepotDungeon", [1] = 606, [2] = 607, [3] = 608, [4] = 609}, |
[610] = { mapFile = "VashjirKelpForest", [0] = 201}, |
[1049] = { mapFile = "ArtifactSkywall", [1] = 716}, |
[866] = { mapFile = "ColdridgeValley", [0] = 427, [9] = 428}, |
[994] = { mapFile = "HighmaulRaid", [1] = 611, [2] = 612, [3] = 613, [4] = 614, [5] = 615, [0] = 610}, |
[611] = { mapFile = "GilneasCity", [0] = 202}, |
[1048] = { mapFile = "EmeraldDreamway", [0] = 715}, |
[867] = { mapFile = "EastTemple", [1] = 429, [2] = 430}, |
[995] = { mapFile = "UpperBlackrockSpire", [1] = 616, [2] = 617, [3] = 618}, |
[1047] = { mapFile = "Niskara", [0] = 714}, |
[1046] = { mapFile = "AszunaDungeon", [0] = 713}, |
[1045] = { mapFile = "VaultOfTheWardens", [1] = 710, [2] = 711, [3] = 712}, |
[1044] = { mapFile = "MonkOrderHallTheWanderingIsle", [0] = 709}, |
[613] = { mapFile = "Vashjir", [0] = 203}, |
[1042] = { mapFile = "HelheimDungeonDock", [1] = 707, [2] = 708, [0] = 706}, |
[1040] = { mapFile = "NetherlightTemple", [1] = 702}, |
[499] = { mapFile = "Sunwell", [0] = 122}, |
[614] = { mapFile = "VashjirDepths", [0] = 204}, |
[1039] = { mapFile = "IcecrownCitadelDeathKnight", [1] = 698, [2] = 699, [3] = 700, [4] = 701}, |
[1038] = { mapFile = "HulnFlashback", [0] = 697}, |
[1037] = { mapFile = "StormheimArtifactProtWarrior", [0] = 696}, |
[615] = { mapFile = "VashjirRuins", [0] = 205}, |
[1033] = { mapFile = "Suramar", [24] = 683, [33] = 685, [35] = 687, [39] = 691, [41] = 692, [42] = 693, [32] = 684, [34] = 686, [36] = 688, [38] = 690, [37] = 689, [22] = 681, [23] = 682, [0] = 680}, |
[871] = { mapFile = "ScarletHalls", [1] = 431, [2] = 432}, |
[1031] = { mapFile = "BrokenShorePaladin", [0] = 676}, |
[301] = { mapFile = "StormwindCity", [0] = 84}, |
[475] = { mapFile = "BladesEdgeMountains", [0] = 105}, |
[382] = { mapFile = "Undercity", [0] = 998}, |
[953] = { mapFile = "OrgrimmarRaid", [1] = 557, [2] = 558, [3] = 559, [4] = 560, [5] = 561, [6] = 562, [7] = 563, [8] = 564, [9] = 565, [10] = 566, [11] = 567, [12] = 568, [13] = 569, [14] = 570, [0] = 556}, |
[1007] = { mapFile = "BrokenIsles", [0] = 619}, |
[989] = { mapFile = "SpiresofArakDungeon", [1] = 601, [2] = 602}, |
[873] = { mapFile = "TheHiddenPass", [0] = 433, [5] = 434}, |
[501] = { mapFile = "LakeWintergrasp", [0] = 123}, |
[983] = { mapFile = "DefenseofKarabor", [0] = 592}, |
[971] = { mapFile = "garrisonsmvalliance", [24] = 580, [25] = 581, [23] = 579}, |
[874] = { mapFile = "ScarletCathedral", [1] = 435, [2] = 436}, |
[969] = { mapFile = "ShadowmoonDungeon", [1] = 574, [2] = 575, [3] = 576}, |
[261] = { mapFile = "Silithus", [0] = 81, [13] = 82}, |
[747] = { mapFile = "LostCityofTolvir", [0] = 277}, |
[875] = { mapFile = "TheGreatWall", [1] = 437, [2] = 438}, |
[502] = { mapFile = "ScarletEnclave", [0] = 124}, |
[39] = { mapFile = "Westfall", [0] = 52, [17] = 55, [4] = 53, [5] = 54}, |
[962] = { mapFile = "Draenor", [0] = 572}, |
[876] = { mapFile = "StormstoutBrewery", [1] = 439, [2] = 440, [3] = 441, [4] = 442}, |
[955] = { mapFile = "CelestialChallenge", [0] = 571}, |
[951] = { mapFile = "TimelessIsle", [0] = 554, [22] = 555}, |
[749] = { mapFile = "WailingCaverns", [1] = 279}, |
[877] = { mapFile = "ShadowpanHideout", [1] = 444, [2] = 445, [3] = 446, [0] = 443}, |
[945] = { mapFile = "TanaanJungle", [0] = 534}, |
[941] = { mapFile = "FrostfireRidge", [1] = 526, [2] = 527, [3] = 528, [4] = 529, [6] = 530, [7] = 531, [8] = 532, [0] = 525, [9] = 533}, |
[750] = { mapFile = "Maraudon", [1] = 280, [2] = 281}, |
[878] = { mapFile = "BrewmasterScenario01", [0] = 447}, |
[684] = { mapFile = "RuinsofGilneas", [0] = 217}, |
[362] = { mapFile = "ThunderBluff", [0] = 88}, |
[751] = { mapFile = "TheMaelstromContinent", [0] = 948}, |
[182] = { mapFile = "Felwood", [0] = 77}, |
[504] = { mapFile = "Dalaran", [1] = 125, [2] = 126}, |
[465] = { mapFile = "Hellfire", [0] = 100}, |
[752] = { mapFile = "BaradinHold", [1] = 282}, |
[880] = { mapFile = "TheJadeForestScenario", [0] = 448}, |
[1008] = { mapFile = "OvergrownOutpost", [1] = 621, [0] = 620}, |
[443] = { mapFile = "WarsongGulch", [0] = 92}, |
[753] = { mapFile = "BlackrockCaverns", [1] = 283, [2] = 284}, |
[881] = { mapFile = "ValleyOfPowerScenario", [0] = 449}, |
[1009] = { mapFile = "AshranAllianceFactionHub", [0] = 622}, |
[626] = { mapFile = "TwinPeaks", [0] = 206}, |
[754] = { mapFile = "BlackwingDescent", [1] = 285, [2] = 286}, |
[882] = { mapFile = "BrewmasterScenario03", [0] = 450}, |
[1010] = { mapFile = "HillsbradFoothillsBG", [0] = 623}, |
[463] = { mapFile = "Ghostlands", [1] = 96, [0] = 95}, |
[755] = { mapFile = "BlackwingLair", [1] = 287, [2] = 288, [3] = 289, [4] = 290}, |
[883] = { mapFile = "Tyrivess", [0] = 451}, |
[1011] = { mapFile = "AshranHordeFactionHub", [0] = 624}, |
[381] = { mapFile = "Darnassus", [0] = 89}, |
[756] = { mapFile = "TheDeadmines", [1] = 291, [2] = 292}, |
[884] = { mapFile = "KunLaiPassScenario", [0] = 452}, |
[540] = { mapFile = "IsleofConquest", [0] = 169}, |
[604] = { mapFile = "IcecrownCitadel", [1] = 186, [2] = 187, [3] = 188, [4] = 189, [5] = 190, [6] = 191, [7] = 192, [8] = 193}, |
[757] = { mapFile = "GrimBatol", [1] = 293}, |
[885] = { mapFile = "MogushanPalace", [1] = 453, [2] = 454, [3] = 455}, |
[467] = { mapFile = "Zangarmarsh", [0] = 102}, |
} |
function SetupMigrationData() |
mapFileToIdMap = {} |
for id, t in pairs(MapMigrationData) do |
if t.mapFile then |
mapFileToIdMap[t.mapFile] = id |
end |
end |
uiMapIdToIdMap = {} |
for id, t in pairs(MapMigrationData) do |
for floor, uiMapId in pairs(t) do |
if floor ~= "mapFile" and floor ~= "defaultFloor" then |
uiMapIdToIdMap[uiMapId] = id * 10000 + floor |
end |
end |
end |
end |
-- HereBeDragons is a data API for the World of Warcraft mapping system |
-- HereBeDragons-1.0 is not supported on WoW 8.0 |
if select(4, GetBuildInfo()) >= 80000 then |
return |
end |
local MAJOR, MINOR = "HereBeDragons-1.0", 33 |
assert(LibStub, MAJOR .. " requires LibStub") |
local HereBeDragons, oldversion = LibStub:NewLibrary(MAJOR, MINOR) |
if not HereBeDragons then return end |
local CBH = LibStub("CallbackHandler-1.0") |
HereBeDragons.eventFrame = HereBeDragons.eventFrame or CreateFrame("Frame") |
HereBeDragons.mapData = HereBeDragons.mapData or {} |
HereBeDragons.continentZoneMap = HereBeDragons.continentZoneMap or { [-1] = { [0] = WORLDMAP_COSMIC_ID }, [0] = { [0] = WORLDMAP_AZEROTH_ID }} |
HereBeDragons.mapToID = HereBeDragons.mapToID or { Cosmic = WORLDMAP_COSMIC_ID, World = WORLDMAP_AZEROTH_ID } |
HereBeDragons.microDungeons = HereBeDragons.microDungeons or {} |
HereBeDragons.transforms = HereBeDragons.transforms or {} |
HereBeDragons.callbacks = HereBeDragons.callbacks or CBH:New(HereBeDragons, nil, nil, false) |
-- constants |
local TERRAIN_MATCH = "_terrain%d+$" |
-- Lua upvalues |
local PI2 = math.pi * 2 |
local atan2 = math.atan2 |
local pairs, ipairs = pairs, ipairs |
local type = type |
local band = bit.band |
-- WoW API upvalues |
local UnitPosition = UnitPosition |
-- data table upvalues |
local mapData = HereBeDragons.mapData -- table { width, height, left, top } |
local continentZoneMap = HereBeDragons.continentZoneMap |
local mapToID = HereBeDragons.mapToID |
local microDungeons = HereBeDragons.microDungeons |
local transforms = HereBeDragons.transforms |
local currentPlayerZoneMapID, currentPlayerLevel, currentMapFile, currentMapIsMicroDungeon |
-- Override instance ids for phased content |
local instanceIDOverrides = { |
-- Draenor |
[1152] = 1116, -- Horde Garrison 1 |
[1330] = 1116, -- Horde Garrison 2 |
[1153] = 1116, -- Horde Garrison 3 |
[1154] = 1116, -- Horde Garrison 4 (unused) |
[1158] = 1116, -- Alliance Garrison 1 |
[1331] = 1116, -- Alliance Garrison 2 |
[1159] = 1116, -- Alliance Garrison 3 |
[1160] = 1116, -- Alliance Garrison 4 (unused) |
[1191] = 1116, -- Ashran PvP Zone |
[1203] = 1116, -- Frostfire Finale Scenario |
[1207] = 1116, -- Talador Finale Scenario |
[1277] = 1116, -- Defense of Karabor Scenario (SMV) |
[1402] = 1116, -- Gorgrond Finale Scenario |
[1464] = 1116, -- Tanaan |
[1465] = 1116, -- Tanaan |
-- Legion |
[1478] = 1220, -- Temple of Elune Scenario (Val'Sharah) |
[1495] = 1220, -- Protection Paladin Artifact Scenario (Stormheim) |
[1498] = 1220, -- Havoc Demon Hunter Artifact Scenario (Suramar) |
[1502] = 1220, -- Dalaran Underbelly |
[1533] = 0, -- Karazhan Artifact Scenario |
[1612] = 1220, -- Feral Druid Artifact Scenario (Suramar) |
[1626] = 1220, -- Suramar Withered Scenario |
[1662] = 1220, -- Suramar Invasion Scenario |
} |
-- unregister and store all WORLD_MAP_UPDATE registrants, to avoid excess processing when |
-- retrieving info from stateful map APIs |
local wmuRegistry |
local function UnregisterWMU() |
wmuRegistry = {GetFramesRegisteredForEvent("WORLD_MAP_UPDATE")} |
for _, frame in ipairs(wmuRegistry) do |
frame:UnregisterEvent("WORLD_MAP_UPDATE") |
end |
end |
-- restore WORLD_MAP_UPDATE to all frames in the registry |
local function RestoreWMU() |
assert(wmuRegistry) |
for _, frame in ipairs(wmuRegistry) do |
frame:RegisterEvent("WORLD_MAP_UPDATE") |
end |
wmuRegistry = nil |
end |
-- gather map info, but only if this isn't an upgrade (or the upgrade version forces a re-map) |
if not oldversion or oldversion < 33 then |
-- wipe old data, if required, otherwise the upgrade path isn't triggered |
if oldversion then |
wipe(mapData) |
wipe(microDungeons) |
end |
local MAPS_TO_REMAP = { |
-- alliance garrison |
[973] = 971, |
[974] = 971, |
[975] = 971, |
[991] = 971, |
-- horde garrison |
[980] = 976, |
[981] = 976, |
[982] = 976, |
[990] = 976, |
} |
-- some zones will remap initially, but have a fixup later |
local REMAP_FIXUP_EXEMPT = { |
-- main draenor garrison maps |
[971] = true, |
[976] = true, |
-- legion class halls |
[1072] = { Z = 10, mapFile = "TrueshotLodge" }, -- true shot lodge |
[1077] = { Z = 7, mapFile = "TheDreamgrove" }, -- dreamgrove |
} |
local function processTransforms() |
wipe(transforms) |
for _, tID in ipairs(GetWorldMapTransforms()) do |
local terrainMapID, newTerrainMapID, _, _, transformMinY, transformMaxY, transformMinX, transformMaxX, offsetY, offsetX, flags = GetWorldMapTransformInfo(tID) |
-- flag 4 indicates the transform is only for the flight map |
if band(flags, 4) ~= 4 and (offsetY ~= 0 or offsetX ~= 0) then |
local transform = { |
instanceID = terrainMapID, |
newInstanceID = newTerrainMapID, |
minY = transformMinY, |
maxY = transformMaxY, |
minX = transformMinX, |
maxX = transformMaxX, |
offsetY = offsetY, |
offsetX = offsetX |
} |
table.insert(transforms, transform) |
end |
end |
end |
local function applyMapTransforms(instanceID, left, right, top, bottom) |
for _, transformData in ipairs(transforms) do |
if transformData.instanceID == instanceID then |
if left < transformData.maxX and right > transformData.minX and top < transformData.maxY and bottom > transformData.minY then |
instanceID = transformData.newInstanceID |
left = left + transformData.offsetX |
right = right + transformData.offsetX |
top = top + transformData.offsetY |
bottom = bottom + transformData.offsetY |
break |
end |
end |
end |
return instanceID, left, right, top, bottom |
end |
-- gather the data of one zone (by mapID) |
local function processZone(id) |
if not id or mapData[id] then return end |
-- set the map and verify it could be set |
local success = SetMapByID(id) |
if not success then |
return |
elseif id ~= GetCurrentMapAreaID() and not REMAP_FIXUP_EXEMPT[id] then |
-- this is an alias zone (phasing terrain changes), just skip it and remap it later |
if not MAPS_TO_REMAP[id] then |
MAPS_TO_REMAP[id] = GetCurrentMapAreaID() |
end |
return |
end |
-- dimensions of the map |
local originalInstanceID, _, _, left, right, top, bottom = GetAreaMapInfo(id) |
local instanceID = originalInstanceID |
if (left and top and right and bottom and (left ~= 0 or top ~= 0 or right ~= 0 or bottom ~= 0)) then |
instanceID, left, right, top, bottom = applyMapTransforms(originalInstanceID, left, right, top, bottom) |
mapData[id] = { left - right, top - bottom, left, top } |
else |
mapData[id] = { 0, 0, 0, 0 } |
end |
mapData[id].instance = instanceID |
mapData[id].name = GetMapNameByID(id) |
-- store the original instance id (ie. not remapped for map transforms) for micro dungeons |
mapData[id].originalInstance = originalInstanceID |
local mapFile = type(REMAP_FIXUP_EXEMPT[id]) == "table" and REMAP_FIXUP_EXEMPT[id].mapFile or GetMapInfo() |
if mapFile then |
-- remove phased terrain from the map names |
mapFile = mapFile:gsub(TERRAIN_MATCH, "") |
if not mapToID[mapFile] then mapToID[mapFile] = id end |
mapData[id].mapFile = mapFile |
end |
local C, Z = GetCurrentMapContinent(), GetCurrentMapZone() |
-- maps that remap generally have wrong C/Z info, so allow the fixup table to override it |
if type(REMAP_FIXUP_EXEMPT[id]) == "table" then |
C = REMAP_FIXUP_EXEMPT[id].C or C |
Z = REMAP_FIXUP_EXEMPT[id].Z or Z |
end |
mapData[id].C = C or -100 |
mapData[id].Z = Z or -100 |
if mapData[id].C > 0 and mapData[id].Z >= 0 then |
-- store C/Z lookup table |
if not continentZoneMap[C] then |
continentZoneMap[C] = {} |
end |
if not continentZoneMap[C][Z] then |
continentZoneMap[C][Z] = id |
end |
end |
-- retrieve floors |
local floors = { GetNumDungeonMapLevels() } |
-- offset floors for terrain map |
if DungeonUsesTerrainMap() then |
for i = 1, #floors do |
floors[i] = floors[i] + 1 |
end |
end |
-- check for fake floors |
if #floors == 0 and GetCurrentMapDungeonLevel() > 0 then |
floors[1] = GetCurrentMapDungeonLevel() |
mapData[id].fakefloor = GetCurrentMapDungeonLevel() |
end |
mapData[id].floors = {} |
mapData[id].numFloors = #floors |
for i = 1, mapData[id].numFloors do |
local f = floors[i] |
SetDungeonMapLevel(f) |
local _, right, bottom, left, top = GetCurrentMapDungeonLevel() |
if left and top and right and bottom then |
instanceID, left, right, top, bottom = applyMapTransforms(originalInstanceID, left, right, top, bottom) |
mapData[id].floors[f] = { left - right, top - bottom, left, top } |
mapData[id].floors[f].instance = mapData[id].instance |
elseif f == 1 and DungeonUsesTerrainMap() then |
mapData[id].floors[f] = { mapData[id][1], mapData[id][2], mapData[id][3], mapData[id][4] } |
mapData[id].floors[f].instance = mapData[id].instance |
end |
end |
-- setup microdungeon storage if the its a zone map or has no floors of its own |
if (mapData[id].C > 0 and mapData[id].Z > 0) or mapData[id].numFloors == 0 then |
if not microDungeons[originalInstanceID] then |
microDungeons[originalInstanceID] = { global = {} } |
end |
end |
end |
local function processMicroDungeons() |
for _, dID in ipairs(GetDungeonMaps()) do |
local floorIndex, minX, maxX, minY, maxY, terrainMapID, parentWorldMapID, flags = GetDungeonMapInfo(dID) |
-- apply transform |
local originalTerrainMapID = terrainMapID |
terrainMapID, maxX, minX, maxY, minY = applyMapTransforms(terrainMapID, maxX, minX, maxY, minY) |
-- check if this zone can have microdungeons |
if microDungeons[originalTerrainMapID] then |
-- store per-zone info |
if not microDungeons[originalTerrainMapID][parentWorldMapID] then |
microDungeons[originalTerrainMapID][parentWorldMapID] = {} |
end |
microDungeons[originalTerrainMapID][parentWorldMapID][floorIndex] = { maxX - minX, maxY - minY, maxX, maxY } |
microDungeons[originalTerrainMapID][parentWorldMapID][floorIndex].instance = terrainMapID |
-- store global info, as some microdungeon are associated to the wrong zone when phasing is involved (garrison, and more) |
-- but only store the first, since there can be overlap on the same continent otherwise |
if not microDungeons[originalTerrainMapID].global[floorIndex] then |
microDungeons[originalTerrainMapID].global[floorIndex] = microDungeons[originalTerrainMapID][parentWorldMapID][floorIndex] |
end |
end |
end |
end |
local function fixupZones() |
-- fake cosmic map |
mapData[WORLDMAP_COSMIC_ID] = {0, 0, 0, 0} |
mapData[WORLDMAP_COSMIC_ID].instance = -1 |
mapData[WORLDMAP_COSMIC_ID].mapFile = "Cosmic" |
mapData[WORLDMAP_COSMIC_ID].floors = {} |
mapData[WORLDMAP_COSMIC_ID].C = -1 |
mapData[WORLDMAP_COSMIC_ID].Z = 0 |
mapData[WORLDMAP_COSMIC_ID].name = WORLD_MAP |
-- fake azeroth world map |
-- the world map has one "floor" per continent it contains, which allows |
-- using these floors to translate coordinates from and to the world map. |
-- note: due to artistic differences in the drawn azeroth maps, the values |
-- used for the continents are estimates and not perfectly accurate |
mapData[WORLDMAP_AZEROTH_ID] = { 63570, 42382, 53730, 19600 } -- Eastern Kingdoms, or floor 0 |
mapData[WORLDMAP_AZEROTH_ID].floors = { |
-- Kalimdor |
[1] = { 65700, 43795, 11900, 23760, instance = 1 }, |
-- Northrend |
[571] = { 65700, 43795, 33440, 11960, instance = 571 }, |
-- Pandaria |
[870] = { 58520, 39015, 29070, 34410, instance = 870 }, |
-- Broken Isles |
[1220] = { 96710, 64476, 63100, 29960, instance = 1220 }, |
} |
mapData[WORLDMAP_AZEROTH_ID].instance = 0 |
mapData[WORLDMAP_AZEROTH_ID].mapFile = "World" |
mapData[WORLDMAP_AZEROTH_ID].C = 0 |
mapData[WORLDMAP_AZEROTH_ID].Z = 0 |
mapData[WORLDMAP_AZEROTH_ID].name = WORLD_MAP |
-- alliance draenor garrison |
if mapData[971] then |
mapData[971].Z = 5 |
mapToID["garrisonsmvalliance_tier1"] = 971 |
mapToID["garrisonsmvalliance_tier2"] = 971 |
mapToID["garrisonsmvalliance_tier3"] = 971 |
end |
-- horde draenor garrison |
if mapData[976] then |
mapData[976].Z = 3 |
mapToID["garrisonffhorde_tier1"] = 976 |
mapToID["garrisonffhorde_tier2"] = 976 |
mapToID["garrisonffhorde_tier3"] = 976 |
end |
-- remap zones with alias IDs |
for remapID, validMapID in pairs(MAPS_TO_REMAP) do |
if mapData[validMapID] then |
mapData[remapID] = mapData[validMapID] |
end |
end |
end |
local function gatherMapData() |
-- unregister WMU to reduce the processing burden |
UnregisterWMU() |
-- load transforms |
processTransforms() |
-- load the main zones |
-- these should be processed first so they take precedence in the mapFile lookup table |
local continents = {GetMapContinents()} |
for i = 1, #continents, 2 do |
processZone(continents[i]) |
local zones = {GetMapZones((i + 1) / 2)} |
for z = 1, #zones, 2 do |
processZone(zones[z]) |
end |
end |
-- process all other zones, this includes dungeons and more |
local areas = GetAreaMaps() |
for idx, zoneID in pairs(areas) do |
processZone(zoneID) |
end |
-- fix a few zones with data lookup problems |
fixupZones() |
-- and finally, the microdungeons |
processMicroDungeons() |
-- restore WMU |
RestoreWMU() |
end |
gatherMapData() |
end |
-- Transform a set of coordinates based on the defined map transformations |
local function applyCoordinateTransforms(x, y, instanceID) |
for _, transformData in ipairs(transforms) do |
if transformData.instanceID == instanceID then |
if transformData.minX <= x and transformData.maxX >= x and transformData.minY <= y and transformData.maxY >= y then |
instanceID = transformData.newInstanceID |
x = x + transformData.offsetX |
y = y + transformData.offsetY |
break |
end |
end |
end |
if instanceIDOverrides[instanceID] then |
instanceID = instanceIDOverrides[instanceID] |
end |
return x, y, instanceID |
end |
-- get the data table for a map and its level (floor) |
local function getMapDataTable(mapID, level) |
if not mapID then return nil end |
if type(mapID) == "string" then |
mapID = mapID:gsub(TERRAIN_MATCH, "") |
mapID = mapToID[mapID] |
end |
local data = mapData[mapID] |
if not data then return nil end |
if (type(level) ~= "number" or level == 0) and data.fakefloor then |
level = data.fakefloor |
end |
if type(level) == "number" and level > 0 then |
if data.floors[level] then |
return data.floors[level] |
elseif data.originalInstance and microDungeons[data.originalInstance] then |
if microDungeons[data.originalInstance][mapID] and microDungeons[data.originalInstance][mapID][level] then |
return microDungeons[data.originalInstance][mapID][level] |
elseif microDungeons[data.originalInstance].global[level] then |
return microDungeons[data.originalInstance].global[level] |
end |
end |
else |
return data |
end |
end |
local StartUpdateTimer |
local function UpdateCurrentPosition() |
UnregisterWMU() |
-- save active map and level |
local prevContinent |
local prevMapID, prevLevel = GetCurrentMapAreaID(), GetCurrentMapDungeonLevel() |
-- handle continent maps (751 is the maelstrom continent, which fails with SetMapByID) |
if not prevMapID or prevMapID < 0 or prevMapID == 751 then |
prevContinent = GetCurrentMapContinent() |
end |
-- set current map |
SetMapToCurrentZone() |
-- retrieve active values |
local newMapID, newLevel = GetCurrentMapAreaID(), GetCurrentMapDungeonLevel() |
local mapFile, _, _, isMicroDungeon, microFile = GetMapInfo() |
-- we want to ignore any terrain phasings |
if mapFile then |
mapFile = mapFile:gsub(TERRAIN_MATCH, "") |
end |
-- hack to update the mapfile for the garrison map (as it changes when the player updates his garrison) |
-- its not ideal to only update it when the player is in the garrison, but updates should only really happen then |
if (newMapID == 971 or newMapID == 976) and mapData[newMapID] and mapFile ~= mapData[newMapID].mapFile then |
mapData[newMapID].mapFile = mapFile |
end |
-- restore previous map |
if prevContinent then |
SetMapZoom(prevContinent) |
else |
-- reset map if it changed, or we need to go back to level 0 |
if prevMapID and (prevMapID ~= newMapID or (prevLevel ~= newLevel and prevLevel == 0)) then |
SetMapByID(prevMapID) |
end |
if prevLevel and prevLevel > 0 then |
SetDungeonMapLevel(prevLevel) |
end |
end |
RestoreWMU() |
if newMapID ~= currentPlayerZoneMapID or newLevel ~= currentPlayerLevel then |
-- store micro dungeon map lookup, if available |
if microFile and not mapToID[microFile] then mapToID[microFile] = newMapID end |
-- update upvalues and signal callback |
currentPlayerZoneMapID, currentPlayerLevel, currentMapFile, currentMapIsMicroDungeon = newMapID, newLevel, microFile or mapFile, isMicroDungeon |
HereBeDragons.callbacks:Fire("PlayerZoneChanged", currentPlayerZoneMapID, currentPlayerLevel, currentMapFile, currentMapIsMicroDungeon) |
end |
-- start a timer to update in micro dungeons since multi-level micro dungeons do not reliably fire events |
if isMicroDungeon then |
StartUpdateTimer() |
end |
end |
-- upgradeable timer callback, don't want to keep calling the old function if the library is upgraded |
HereBeDragons.UpdateCurrentPosition = UpdateCurrentPosition |
local function UpdateTimerCallback() |
-- signal that the timer ran |
HereBeDragons.updateTimerActive = nil |
-- run update now |
HereBeDragons.UpdateCurrentPosition() |
end |
function StartUpdateTimer() |
if not HereBeDragons.updateTimerActive then |
-- prevent running multiple timers |
HereBeDragons.updateTimerActive = true |
-- and queue an update |
C_Timer.After(1, UpdateTimerCallback) |
end |
end |
local function OnEvent(frame, event, ...) |
UpdateCurrentPosition() |
end |
HereBeDragons.eventFrame:SetScript("OnEvent", OnEvent) |
HereBeDragons.eventFrame:UnregisterAllEvents() |
HereBeDragons.eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA") |
HereBeDragons.eventFrame:RegisterEvent("ZONE_CHANGED") |
HereBeDragons.eventFrame:RegisterEvent("ZONE_CHANGED_INDOORS") |
HereBeDragons.eventFrame:RegisterEvent("NEW_WMO_CHUNK") |
HereBeDragons.eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD") |
-- if we're loading after entering the world (ie. on demand), update position now |
if IsLoggedIn() then |
UpdateCurrentPosition() |
end |
--- Return the localized zone name for a given mapID or mapFile |
-- @param mapID numeric mapID or mapFile |
function HereBeDragons:GetLocalizedMap(mapID) |
if type(mapID) == "string" then |
mapID = mapID:gsub(TERRAIN_MATCH, "") |
mapID = mapToID[mapID] |
end |
return mapData[mapID] and mapData[mapID].name or nil |
end |
--- Return the map id to a mapFile |
-- @param mapFile Map File |
function HereBeDragons:GetMapIDFromFile(mapFile) |
if mapFile then |
mapFile = mapFile:gsub(TERRAIN_MATCH, "") |
return mapToID[mapFile] |
end |
return nil |
end |
--- Return the mapFile to a map ID |
-- @param mapID Map ID |
function HereBeDragons:GetMapFileFromID(mapID) |
return mapData[mapID] and mapData[mapID].mapFile or nil |
end |
--- Lookup the map ID for a Continent / Zone index combination |
-- @param C continent index from GetCurrentMapContinent |
-- @param Z zone index from GetCurrentMapZone |
function HereBeDragons:GetMapIDFromCZ(C, Z) |
if C and continentZoneMap[C] then |
return Z and continentZoneMap[C][Z] |
end |
return nil |
end |
--- Lookup the C/Z values for map |
-- @param mapID the MapID |
function HereBeDragons:GetCZFromMapID(mapID) |
if mapData[mapID] then |
return mapData[mapID].C, mapData[mapID].Z |
end |
return nil, nil |
end |
--- Get the size of the zone |
-- @param mapID Map ID or MapFile of the zone |
-- @param level Optional map level |
-- @return width, height of the zone, in yards |
function HereBeDragons:GetZoneSize(mapID, level) |
local data = getMapDataTable(mapID, level) |
if not data then return 0, 0 end |
return data[1], data[2] |
end |
--- Get the number of floors for a map |
-- @param mapID map ID or mapFile of the zone |
function HereBeDragons:GetNumFloors(mapID) |
if not mapID then return 0 end |
if type(mapID) == "string" then |
mapID = mapID:gsub(TERRAIN_MATCH, "") |
mapID = mapToID[mapID] |
end |
if not mapData[mapID] or not mapData[mapID].numFloors then return 0 end |
return mapData[mapID].numFloors |
end |
--- Get a list of all map IDs |
-- @return array-style table with all known/valid map IDs |
function HereBeDragons:GetAllMapIDs() |
local t = {} |
for id in pairs(mapData) do |
table.insert(t, id) |
end |
return t |
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 MapID or MapFile of the zone |
-- @param level Optional level of the zone |
function HereBeDragons:GetWorldCoordinatesFromZone(x, y, zone, level) |
local data = getMapDataTable(zone, level) |
if not data or data[1] == 0 or data[2] == 0 then return nil, nil, nil 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 MapID or MapFile of the zone |
-- @param level Optional level 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 HereBeDragons:GetZoneCoordinatesFromWorld(x, y, zone, level, allowOutOfBounds) |
local data = getMapDataTable(zone, level) |
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 |
--- Translate zone coordinates from one zone to another |
-- @param x X position in 0-1 point coordinates, relative to the origin zone |
-- @param y Y position in 0-1 point coordinates, relative to the origin zone |
-- @param oZone Origin Zone, mapID or mapFile |
-- @param oLevel Origin Zone Level |
-- @param dZone Destination Zone, mapID or mapFile |
-- @param dLevel Destination Zone Level |
-- @param allowOutOfBounds Allow coordinates to go beyond the current map (ie. outside of the 0-1 range), otherwise nil will be returned |
function HereBeDragons:TranslateZoneCoordinates(x, y, oZone, oLevel, dZone, dLevel, allowOutOfBounds) |
local xCoord, yCoord, instance = self:GetWorldCoordinatesFromZone(x, y, oZone, oLevel) |
if not xCoord then return nil, nil end |
local data = getMapDataTable(dZone, dLevel) |
if not data or data.instance ~= instance then return nil, nil end |
return self:GetZoneCoordinatesFromWorld(xCoord, yCoord, dZone, dLevel, allowOutOfBounds) |
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 HereBeDragons: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 map id or mapfile |
-- @param oLevel optional origin zone level (floor) |
-- @param oX origin X, in local zone/point coordinates |
-- @param oY origin Y, in local zone/point coordinates |
-- @param dZone destination zone map id or mapfile |
-- @param dLevel optional destination zone level (floor) |
-- @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 HereBeDragons:GetZoneDistance(oZone, oLevel, oX, oY, dZone, dLevel, dX, dY) |
local oX, oY, oInstance = self:GetWorldCoordinatesFromZone(oX, oY, oZone, oLevel) |
if not oX then return nil, nil, nil end |
-- translate dX, dY to the origin zone |
local dX, dY, dInstance = self:GetWorldCoordinatesFromZone(dX, dY, dZone, dLevel) |
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 |
--- Return the angle and 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 angle, distance where angle is in radians and distance in yards |
function HereBeDragons:GetWorldVector(instanceID, oX, oY, dX, dY) |
local distance, deltaX, deltaY = self:GetWorldDistance(instanceID, oX, oY, dX, dY) |
if not distance then return nil, nil end |
-- calculate the angle from deltaY and deltaX |
local angle = atan2(-deltaX, deltaY) |
-- normalize the angle |
if angle > 0 then |
angle = PI2 - angle |
else |
angle = -angle |
end |
return angle, distance |
end |
--- Get the current world position of the specified unit |
-- The position is transformed to the current continent, if applicable |
-- NOTE: The same restrictions as for the UnitPosition() API apply, |
-- which means a very limited set of unit ids will actually work. |
-- @param unitId Unit Id |
-- @return x, y, instanceID |
function HereBeDragons:GetUnitWorldPosition(unitId) |
-- get the current position |
local y, x, z, instanceID = UnitPosition(unitId) |
if not x or not y then return nil, nil, instanceIDOverrides[instanceID] or instanceID end |
-- return transformed coordinates |
return applyCoordinateTransforms(x, y, instanceID) |
end |
--- Get the current world position of the player |
-- The position is transformed to the current continent, if applicable |
-- @return x, y, instanceID |
function HereBeDragons:GetPlayerWorldPosition() |
-- get the current position |
local y, x, z, instanceID = UnitPosition("player") |
if not x or not y then return nil, nil, instanceIDOverrides[instanceID] or instanceID end |
-- return transformed coordinates |
return applyCoordinateTransforms(x, y, instanceID) |
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 mapID, level, mapFile, isMicroDungeon |
function HereBeDragons:GetPlayerZone() |
return currentPlayerZoneMapID, currentPlayerLevel, currentMapFile, currentMapIsMicroDungeon |
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, mapID, level, mapFile, isMicroDungeon |
function HereBeDragons:GetPlayerZonePosition(allowOutOfBounds) |
if not currentPlayerZoneMapID then return nil, nil, nil, nil end |
local x, y, instanceID = self:GetPlayerWorldPosition() |
if not x or not y then return nil, nil, nil, nil end |
x, y = self:GetZoneCoordinatesFromWorld(x, y, currentPlayerZoneMapID, currentPlayerLevel, allowOutOfBounds) |
if x and y then |
return x, y, currentPlayerZoneMapID, currentPlayerLevel, currentMapFile, currentMapIsMicroDungeon |
end |
return nil, nil, nil, nil |
end |
## Interface: 20400 |
## Title: Lib: LibStub |
## Notes: Universal Library Stub |
## Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel |
## X-Website: http://jira.wowace.com/browse/LS |
## X-Category: Library |
## X-License: Public Domain |
LibStub.lua |
-- LibStub is a simple versioning stub meant for use in Libraries. http://www.wowace.com/wiki/LibStub for more info |
-- LibStub is hereby placed in the Public Domain Credits: Kaelten, Cladhaire, ckknight, Mikk, Ammo, Nevcairiel, joshborke |
local LIBSTUB_MAJOR, LIBSTUB_MINOR = "LibStub", 2 -- NEVER MAKE THIS AN SVN REVISION! IT NEEDS TO BE USABLE IN ALL REPOS! |
local LibStub = _G[LIBSTUB_MAJOR] |
if not LibStub or LibStub.minor < LIBSTUB_MINOR then |
LibStub = LibStub or {libs = {}, minors = {} } |
_G[LIBSTUB_MAJOR] = LibStub |
LibStub.minor = LIBSTUB_MINOR |
function LibStub:NewLibrary(major, minor) |
assert(type(major) == "string", "Bad argument #2 to `NewLibrary' (string expected)") |
minor = assert(tonumber(strmatch(minor, "%d+")), "Minor version must either be a number or contain a number.") |
local oldminor = self.minors[major] |
if oldminor and oldminor >= minor then return nil end |
self.minors[major], self.libs[major] = minor, self.libs[major] or {} |
return self.libs[major], oldminor |
end |
function LibStub:GetLibrary(major, silent) |
if not self.libs[major] and not silent then |
error(("Cannot find a library instance of %q."):format(tostring(major)), 2) |
end |
return self.libs[major], self.minors[major] |
end |
function LibStub:IterateLibraries() return pairs(self.libs) end |
setmetatable(LibStub, { __call = LibStub.GetLibrary }) |
end |
-- HereBeDragons is a data API for the World of Warcraft mapping system |
-- HereBeDragons-2.0 is not supported on WoW 7.x or earlier |
if select(4, GetBuildInfo()) < 80000 then |
return |
end |
local MAJOR, MINOR = "HereBeDragons-2.0", 6 |
assert(LibStub, MAJOR .. " requires LibStub") |
local HereBeDragons, oldversion = LibStub:NewLibrary(MAJOR, MINOR) |
if not HereBeDragons then return end |
local CBH = LibStub("CallbackHandler-1.0") |
HereBeDragons.eventFrame = HereBeDragons.eventFrame or CreateFrame("Frame") |
HereBeDragons.mapData = HereBeDragons.mapData or {} |
HereBeDragons.worldMapData = HereBeDragons.worldMapData or {} |
HereBeDragons.transforms = HereBeDragons.transforms or {} |
HereBeDragons.callbacks = HereBeDragons.callbacks or CBH:New(HereBeDragons, nil, nil, false) |
-- Data Constants |
local COSMIC_MAP_ID = 946 |
local WORLD_MAP_ID = 947 |
-- Lua upvalues |
local PI2 = math.pi * 2 |
local atan2 = math.atan2 |
local pairs, ipairs = pairs, ipairs |
local type = type |
local band = bit.band |
-- WoW API upvalues |
local UnitPosition = UnitPosition |
local C_Map = C_Map |
-- data table upvalues |
local mapData = HereBeDragons.mapData -- table { width, height, left, top, .instance, .name, .mapType } |
local worldMapData = HereBeDragons.worldMapData -- table { width, height, left, top } |
local transforms = HereBeDragons.transforms |
local currentPlayerUIMapID, currentPlayerUIMapType |
-- Override instance ids for phased content |
local instanceIDOverrides = { |
-- Draenor |
[1152] = 1116, -- Horde Garrison 1 |
[1330] = 1116, -- Horde Garrison 2 |
[1153] = 1116, -- Horde Garrison 3 |
[1154] = 1116, -- Horde Garrison 4 (unused) |
[1158] = 1116, -- Alliance Garrison 1 |
[1331] = 1116, -- Alliance Garrison 2 |
[1159] = 1116, -- Alliance Garrison 3 |
[1160] = 1116, -- Alliance Garrison 4 (unused) |
[1191] = 1116, -- Ashran PvP Zone |
[1203] = 1116, -- Frostfire Finale Scenario |
[1207] = 1116, -- Talador Finale Scenario |
[1277] = 1116, -- Defense of Karabor Scenario (SMV) |
[1402] = 1116, -- Gorgrond Finale Scenario |
[1464] = 1116, -- Tanaan |
[1465] = 1116, -- Tanaan |
-- Legion |
[1478] = 1220, -- Temple of Elune Scenario (Val'Sharah) |
[1495] = 1220, -- Protection Paladin Artifact Scenario (Stormheim) |
[1498] = 1220, -- Havoc Demon Hunter Artifact Scenario (Suramar) |
[1502] = 1220, -- Dalaran Underbelly |
[1533] = 0, -- Karazhan Artifact Scenario |
[1612] = 1220, -- Feral Druid Artifact Scenario (Suramar) |
[1626] = 1220, -- Suramar Withered Scenario |
[1662] = 1220, -- Suramar Invasion Scenario |
} |
-- gather map info, but only if this isn't an upgrade (or the upgrade version forces a re-map) |
if not oldversion or oldversion < 3 then |
-- wipe old data, if required, otherwise the upgrade path isn't triggered |
if oldversion then |
wipe(mapData) |
wipe(worldMapData) |
wipe(transforms) |
end |
-- map transform data extracted from UIMapAssignment.db2 (see HereBeDragons-Scripts on GitHub) |
-- format: instanceID, newInstanceID, minY, maxY, minX, maxX, offsetY, offsetX |
local transformData = { |
{ 530, 0, 4800, 16000, -10133.3, -2666.67, -2400, 2400 }, |
{ 530, 1, -6933.33, 533.33, -16000, -8000, 10133.3, 17600 }, |
{ 732, 0, -3200, 533.3, -533.3, 2666.7, -611.8, 3904.3 }, |
{ 1064, 870, 5391, 8148, 3518, 7655, -2134.2, -2286.6 }, |
{ 1208, 1116, -2666, -2133, -2133, -1600, 10210, 2410 }, |
{ 1460, 1220, -1066.7, 2133.3, 0, 3200, -2333.9, 966.7 }, |
} |
local function processTransforms() |
for _, transform in pairs(transformData) do |
local instanceID, newInstanceID, minY, maxY, minX, maxX, offsetY, offsetX = unpack(transform) |
if not transforms[instanceID] then |
transforms[instanceID] = {} |
end |
table.insert(transforms[instanceID], { newInstanceID = newInstanceID, minY = minY, maxY = maxY, minX = minX, maxX = maxX, offsetY = offsetY, offsetX = offsetX }) |
end |
end |
local function applyMapTransforms(instanceID, left, right, top, bottom) |
if transforms[instanceID] then |
for _, transformData in ipairs(transforms[instanceID]) do |
if left <= transformData.maxX and right >= transformData.minX and top <= transformData.maxY and bottom >= transformData.minY then |
instanceID = transformData.newInstanceID |
left = left + transformData.offsetX |
right = right + transformData.offsetX |
top = top + transformData.offsetY |
bottom = bottom + transformData.offsetY |
break |
end |
end |
end |
return instanceID, left, right, top, bottom |
end |
local vector00, vector05 = CreateVector2D(0, 0), CreateVector2D(0.5, 0.5) |
-- gather the data of one map (by uiMapID) |
local function processMap(id, data) |
if not id or mapData[id] then return end |
-- get two positions from the map, we use 0/0 and 0.5/0.5 to avoid issues on some maps where 1/1 is translated inaccurately |
local instance, topLeft = C_Map.GetWorldPosFromMapPos(id, vector00) |
local _, bottomRight = C_Map.GetWorldPosFromMapPos(id, vector05) |
if topLeft and bottomRight then |
local top, left = topLeft:GetXY() |
local bottom, right = bottomRight:GetXY() |
bottom = top + (bottom - top) * 2 |
right = left + (right - left) * 2 |
instance, left, right, top, bottom = applyMapTransforms(instance, left, right, top, bottom) |
mapData[id] = {left - right, top - bottom, left, top, instance = instance, name = data.name, mapType = data.mapType, parent = data.parentMapID} |
else |
mapData[id] = {0, 0, 0, 0, instance = instance or -1, name = data.name, mapType = data.mapType, parent = data.parentMapID } |
end |
end |
local function processMapChildrenRecursive(id) |
local children = C_Map.GetMapChildrenInfo(id) |
if children and #children > 0 then |
for i = 1, #children do |
local id = children[i].mapID |
if id and not mapData[id] then |
processMap(id, children[i]) |
processMapChildrenRecursive(id) |
end |
end |
end |
end |
local function fixupZones() |
local cosmic = C_Map.GetMapInfo(COSMIC_MAP_ID) |
mapData[COSMIC_MAP_ID] = {0, 0, 0, 0} |
mapData[COSMIC_MAP_ID].instance = -1 |
mapData[COSMIC_MAP_ID].name = cosmic.name |
mapData[COSMIC_MAP_ID].mapType = cosmic.mapType |
-- data for the azeroth world map |
worldMapData[0] = { 76153.14, 50748.62, 65008.24, 23827.51 } |
worldMapData[1] = { 77803.77, 51854.98, 13157.6, 28030.61 } |
worldMapData[571] = { 71773.64, 50054.05, 36205.94, 12366.81 } |
worldMapData[870] = { 67710.54, 45118.08, 33565.89, 38020.67 } |
worldMapData[1220] = { 82758.64, 55151.28, 52943.46, 24484.72 } |
worldMapData[1642] = { 77933.3, 51988.91, 44262.36, 32835.1 } |
worldMapData[1643] = { 76060.47, 50696.96, 55384.8, 25774.35 } |
end |
local function gatherMapData() |
processTransforms() |
processMapChildrenRecursive(COSMIC_MAP_ID) |
fixupZones() |
end |
gatherMapData() |
end |
-- Transform a set of coordinates based on the defined map transformations |
local function applyCoordinateTransforms(x, y, instanceID) |
if transforms[instanceID] then |
for _, transformData in ipairs(transforms[instanceID]) do |
if transformData.minX <= x and transformData.maxX >= x and transformData.minY <= y and transformData.maxY >= y then |
instanceID = transformData.newInstanceID |
x = x + transformData.offsetX |
y = y + transformData.offsetY |
break |
end |
end |
end |
if instanceIDOverrides[instanceID] then |
instanceID = instanceIDOverrides[instanceID] |
end |
return x, y, instanceID |
end |
local StartUpdateTimer |
local function UpdateCurrentPosition() |
-- retrieve current zone |
local uiMapID = C_Map.GetBestMapForUnit("player") |
if uiMapID ~= currentPlayerUIMapID then |
-- update upvalues and signal callback |
currentPlayerUIMapID, currentPlayerUIMapType = uiMapID, mapData[uiMapID] and mapData[uiMapID].mapType or 0 |
HereBeDragons.callbacks:Fire("PlayerZoneChanged", currentPlayerUIMapID, currentPlayerUIMapType) |
end |
-- start a timer to update in micro dungeons since multi-level micro dungeons do not reliably fire events |
if currentPlayerUIMapType == Enum.UIMapType.Micro then |
StartUpdateTimer() |
end |
end |
-- upgradeable timer callback, don't want to keep calling the old function if the library is upgraded |
HereBeDragons.UpdateCurrentPosition = UpdateCurrentPosition |
local function UpdateTimerCallback() |
-- signal that the timer ran |
HereBeDragons.updateTimerActive = nil |
-- run update now |
HereBeDragons.UpdateCurrentPosition() |
end |
function StartUpdateTimer() |
if not HereBeDragons.updateTimerActive then |
-- prevent running multiple timers |
HereBeDragons.updateTimerActive = true |
-- and queue an update |
C_Timer.After(1, UpdateTimerCallback) |
end |
end |
local function OnEvent(frame, event, ...) |
UpdateCurrentPosition() |
end |
HereBeDragons.eventFrame:SetScript("OnEvent", OnEvent) |
HereBeDragons.eventFrame:UnregisterAllEvents() |
HereBeDragons.eventFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA") |
HereBeDragons.eventFrame:RegisterEvent("ZONE_CHANGED") |
HereBeDragons.eventFrame:RegisterEvent("ZONE_CHANGED_INDOORS") |
HereBeDragons.eventFrame:RegisterEvent("NEW_WMO_CHUNK") |
HereBeDragons.eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD") |
-- if we're loading after entering the world (ie. on demand), update position now |
if IsLoggedIn() then |
UpdateCurrentPosition() |
end |
--- Return the localized zone name for a given uiMapID |
-- @param uiMapID uiMapID of the zone |
function HereBeDragons:GetLocalizedMap(uiMapID) |
return mapData[uiMapID] and mapData[uiMapID].name or nil |
end |
--- Get the size of the zone |
-- @param uiMapID uiMapID of the zone |
-- @return width, height of the zone, in yards |
function HereBeDragons:GetZoneSize(uiMapID) |
local data = mapData[uiMapID] |
if not data then return 0, 0 end |
return data[1], data[2] |
end |
--- Get a list of all map IDs |
-- @return array-style table with all known/valid map IDs |
function HereBeDragons:GetAllMapIDs() |
local t = {} |
for id in pairs(mapData) do |
table.insert(t, id) |
end |
return t |
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 HereBeDragons:GetWorldCoordinatesFromZone(x, y, zone) |
local data = mapData[zone] |
if not data or data[1] == 0 or data[2] == 0 then return nil, nil, nil 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 local/point coordinates to world coordinates in yards. The coordinates have to come from the Azeroth World Map |
-- @param x X position in 0-1 point coordinates |
-- @param y Y position in 0-1 point coordinates |
-- @param instance Instance to use for the world coordinates |
function HereBeDragons:GetWorldCoordinatesFromAzerothWorldMap(x, y, instance) |
local data = worldMapData[instance] |
if not data or data[1] == 0 or data[2] == 0 then return nil, nil, nil 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, 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 HereBeDragons:GetZoneCoordinatesFromWorld(x, y, zone, allowOutOfBounds) |
local data = mapData[zone] |
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 |
--- 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 HereBeDragons: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 |
-- Helper function to handle world map coordinate translation |
local function TranslateAzerothWorldMapCoordinates(self, x, y, oZone, dZone, allowOutOfBounds) |
if (oZone ~= WORLD_MAP_ID and not mapData[oZone]) or (dZone ~= WORLD_MAP_ID and not mapData[dZone]) then return nil, nil end |
-- determine the instance we're working with |
local instance = (oZone == WORLD_MAP_ID) and mapData[dZone].instance or mapData[oZone].instance |
if not worldMapData[instance] then return nil, nil end |
local data = worldMapData[instance] |
local width, height, left, top = data[1], data[2], data[3], data[4] |
if oZone == WORLD_MAP_ID then |
x, y = self:GetWorldCoordinatesFromAzerothWorldMap(x, y, instance) |
return self:GetZoneCoordinatesFromWorld(x, y, dZone, allowOutOfBounds) |
else |
x, y = self:GetWorldCoordinatesFromZone(x, y, oZone) |
return self:GetAzerothWorldMapCoordinatesFromWorld(x, y, instance, allowOutOfBounds) |
end |
end |
--- Translate zone coordinates from one zone to another |
-- @param x X position in 0-1 point coordinates, relative to the origin zone |
-- @param y Y position in 0-1 point coordinates, relative to the origin zone |
-- @param oZone Origin Zone, uiMapID |
-- @param dZone Destination Zone, uiMapID |
-- @param allowOutOfBounds Allow coordinates to go beyond the current map (ie. outside of the 0-1 range), otherwise nil will be returned |
function HereBeDragons:TranslateZoneCoordinates(x, y, oZone, dZone, allowOutOfBounds) |
if oZone == dZone then return x, y end |
if oZone == WORLD_MAP_ID or dZone == WORLD_MAP_ID then |
return TranslateAzerothWorldMapCoordinates(self, x, y, oZone, dZone, allowOutOfBounds) |
end |
local xCoord, yCoord, instance = self:GetWorldCoordinatesFromZone(x, y, oZone) |
if not xCoord then return nil, nil end |
local data = mapData[dZone] |
if not data or data.instance ~= instance then return nil, nil end |
return self:GetZoneCoordinatesFromWorld(xCoord, yCoord, dZone, allowOutOfBounds) |
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 HereBeDragons: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 HereBeDragons:GetZoneDistance(oZone, oX, oY, dZone, dX, dY) |
local oX, oY, oInstance = self:GetWorldCoordinatesFromZone(oX, oY, oZone) |
if not oX then return nil, nil, nil end |
-- translate dX, dY to the origin zone |
local 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 |
--- Return the angle and 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 angle, distance where angle is in radians and distance in yards |
function HereBeDragons:GetWorldVector(instanceID, oX, oY, dX, dY) |
local distance, deltaX, deltaY = self:GetWorldDistance(instanceID, oX, oY, dX, dY) |
if not distance then return nil, nil end |
-- calculate the angle from deltaY and deltaX |
local angle = atan2(-deltaX, deltaY) |
-- normalize the angle |
if angle > 0 then |
angle = PI2 - angle |
else |
angle = -angle |
end |
return angle, distance |
end |
--- Get the current world position of the specified unit |
-- The position is transformed to the current continent, if applicable |
-- NOTE: The same restrictions as for the UnitPosition() API apply, |
-- which means a very limited set of unit ids will actually work. |
-- @param unitId Unit Id |
-- @return x, y, instanceID |
function HereBeDragons:GetUnitWorldPosition(unitId) |
-- get the current position |
local y, x, z, instanceID = UnitPosition(unitId) |
if not x or not y then return nil, nil, instanceIDOverrides[instanceID] or instanceID end |
-- return transformed coordinates |
return applyCoordinateTransforms(x, y, instanceID) |
end |
--- Get the current world position of the player |
-- The position is transformed to the current continent, if applicable |
-- @return x, y, instanceID |
function HereBeDragons:GetPlayerWorldPosition() |
-- get the current position |
local y, x, z, instanceID = UnitPosition("player") |
if not x or not y then return nil, nil, instanceIDOverrides[instanceID] or instanceID end |
-- return transformed coordinates |
return applyCoordinateTransforms(x, y, instanceID) |
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 HereBeDragons:GetPlayerZone() |
return currentPlayerUIMapID, currentPlayerUIMapType |
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 HereBeDragons:GetPlayerZonePosition(allowOutOfBounds) |
if not currentPlayerUIMapID then return nil, nil, nil, nil end |
local x, y, instanceID = self:GetPlayerWorldPosition() |
if not x or not y then return nil, nil, nil, nil end |
x, y = self:GetZoneCoordinatesFromWorld(x, y, currentPlayerUIMapID, allowOutOfBounds) |
if x and y then |
return x, y, currentPlayerUIMapID, currentPlayerUIMapType |
end |
return nil, nil, nil, nil |
end |
tag a0d9d7d23b61dfd50e81bb5d5008ed2a9168a001 2.00-release |
Author: Hendrik Leppkes <h.leppkes@gmail.com> |
Date: Tue Jul 17 14:46:43 2018 +0200 |
Tag as 2.00-release |
commit fbe9b033a29ba62bb525d65e9ed1197ea2738041 |
Author: Hendrik Leppkes <h.leppkes@gmail.com> |
Date: Tue Jul 17 14:46:33 2018 +0200 |
Update TOC for 8.0 |
commit d6e04fda96a84f89a43d248c3a6e195b4e414bcf |
Author: Hendrik Leppkes <h.leppkes@gmail.com> |
Date: Sun Jul 1 12:37:59 2018 +0200 |
Update transform data for the latest beta build |
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ |
..\FrameXML\UI.xsd"> |
<Script file="CallbackHandler-1.0.lua"/> |
</Ui> |
--[[ $Id: CallbackHandler-1.0.lua 18 2014-10-16 02:52:20Z mikk $ ]] |
local MAJOR, MINOR = "CallbackHandler-1.0", 6 |
local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR) |
if not CallbackHandler then return end -- No upgrade needed |
local meta = {__index = function(tbl, key) tbl[key] = {} return tbl[key] end} |
-- Lua APIs |
local tconcat = table.concat |
local assert, error, loadstring = assert, error, loadstring |
local setmetatable, rawset, rawget = setmetatable, rawset, rawget |
local next, select, pairs, type, tostring = next, select, pairs, type, tostring |
-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded |
-- List them here for Mikk's FindGlobals script |
-- GLOBALS: geterrorhandler |
local xpcall = xpcall |
local function errorhandler(err) |
return geterrorhandler()(err) |
end |
local function CreateDispatcher(argCount) |
local code = [[ |
local next, xpcall, eh = ... |
local method, ARGS |
local function call() method(ARGS) end |
local function dispatch(handlers, ...) |
local index |
index, method = next(handlers) |
if not method then return end |
local OLD_ARGS = ARGS |
ARGS = ... |
repeat |
xpcall(call, eh) |
index, method = next(handlers, index) |
until not method |
ARGS = OLD_ARGS |
end |
return dispatch |
]] |
local ARGS, OLD_ARGS = {}, {} |
for i = 1, argCount do ARGS[i], OLD_ARGS[i] = "arg"..i, "old_arg"..i end |
code = code:gsub("OLD_ARGS", tconcat(OLD_ARGS, ", ")):gsub("ARGS", tconcat(ARGS, ", ")) |
return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(next, xpcall, errorhandler) |
end |
local Dispatchers = setmetatable({}, {__index=function(self, argCount) |
local dispatcher = CreateDispatcher(argCount) |
rawset(self, argCount, dispatcher) |
return dispatcher |
end}) |
-------------------------------------------------------------------------- |
-- CallbackHandler:New |
-- |
-- target - target object to embed public APIs in |
-- RegisterName - name of the callback registration API, default "RegisterCallback" |
-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback" |
-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API. |
function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName) |
RegisterName = RegisterName or "RegisterCallback" |
UnregisterName = UnregisterName or "UnregisterCallback" |
if UnregisterAllName==nil then -- false is used to indicate "don't want this method" |
UnregisterAllName = "UnregisterAllCallbacks" |
end |
-- we declare all objects and exported APIs inside this closure to quickly gain access |
-- to e.g. function names, the "target" parameter, etc |
-- Create the registry object |
local events = setmetatable({}, meta) |
local registry = { recurse=0, events=events } |
-- registry:Fire() - fires the given event/message into the registry |
function registry:Fire(eventname, ...) |
if not rawget(events, eventname) or not next(events[eventname]) then return end |
local oldrecurse = registry.recurse |
registry.recurse = oldrecurse + 1 |
Dispatchers[select('#', ...) + 1](events[eventname], eventname, ...) |
registry.recurse = oldrecurse |
if registry.insertQueue and oldrecurse==0 then |
-- Something in one of our callbacks wanted to register more callbacks; they got queued |
for eventname,callbacks in pairs(registry.insertQueue) do |
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten. |
for self,func in pairs(callbacks) do |
events[eventname][self] = func |
-- fire OnUsed callback? |
if first and registry.OnUsed then |
registry.OnUsed(registry, target, eventname) |
first = nil |
end |
end |
end |
registry.insertQueue = nil |
end |
end |
-- Registration of a callback, handles: |
-- self["method"], leads to self["method"](self, ...) |
-- self with function ref, leads to functionref(...) |
-- "addonId" (instead of self) with function ref, leads to functionref(...) |
-- all with an optional arg, which, if present, gets passed as first argument (after self if present) |
target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]]) |
if type(eventname) ~= "string" then |
error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2) |
end |
method = method or eventname |
local first = not rawget(events, eventname) or not next(events[eventname]) -- test for empty before. not test for one member after. that one member may have been overwritten. |
if type(method) ~= "string" and type(method) ~= "function" then |
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2) |
end |
local regfunc |
if type(method) == "string" then |
-- self["method"] calling style |
if type(self) ~= "table" then |
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2) |
elseif self==target then |
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2) |
elseif type(self[method]) ~= "function" then |
error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2) |
end |
if select("#",...)>=1 then -- this is not the same as testing for arg==nil! |
local arg=select(1,...) |
regfunc = function(...) self[method](self,arg,...) end |
else |
regfunc = function(...) self[method](self,...) end |
end |
else |
-- function ref with self=object or self="addonId" or self=thread |
if type(self)~="table" and type(self)~="string" and type(self)~="thread" then |
error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string or thread expected.", 2) |
end |
if select("#",...)>=1 then -- this is not the same as testing for arg==nil! |
local arg=select(1,...) |
regfunc = function(...) method(arg,...) end |
else |
regfunc = method |
end |
end |
if events[eventname][self] or registry.recurse<1 then |
-- if registry.recurse<1 then |
-- we're overwriting an existing entry, or not currently recursing. just set it. |
events[eventname][self] = regfunc |
-- fire OnUsed callback? |
if registry.OnUsed and first then |
registry.OnUsed(registry, target, eventname) |
end |
else |
-- we're currently processing a callback in this registry, so delay the registration of this new entry! |
-- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency |
registry.insertQueue = registry.insertQueue or setmetatable({},meta) |
registry.insertQueue[eventname][self] = regfunc |
end |
end |
-- Unregister a callback |
target[UnregisterName] = function(self, eventname) |
if not self or self==target then |
error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2) |
end |
if type(eventname) ~= "string" then |
error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2) |
end |
if rawget(events, eventname) and events[eventname][self] then |
events[eventname][self] = nil |
-- Fire OnUnused callback? |
if registry.OnUnused and not next(events[eventname]) then |
registry.OnUnused(registry, target, eventname) |
end |
end |
if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then |
registry.insertQueue[eventname][self] = nil |
end |
end |
-- OPTIONAL: Unregister all callbacks for given selfs/addonIds |
if UnregisterAllName then |
target[UnregisterAllName] = function(...) |
if select("#",...)<1 then |
error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2) |
end |
if select("#",...)==1 and ...==target then |
error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2) |
end |
for i=1,select("#",...) do |
local self = select(i,...) |
if registry.insertQueue then |
for eventname, callbacks in pairs(registry.insertQueue) do |
if callbacks[self] then |
callbacks[self] = nil |
end |
end |
end |
for eventname, callbacks in pairs(events) do |
if callbacks[self] then |
callbacks[self] = nil |
-- Fire OnUnused callback? |
if registry.OnUnused and not next(callbacks) then |
registry.OnUnused(registry, target, eventname) |
end |
end |
end |
end |
end |
end |
return registry |
end |
-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it |
-- try to upgrade old implicit embeds since the system is selfcontained and |
-- relies on closures to work. |
-- HereBeDragons-Pins is a library to show pins/icons on the world map and minimap |
-- HereBeDragons-Pins-1.0 is not supported on WoW 8.0 |
if select(4, GetBuildInfo()) >= 80000 then |
return |
end |
local MAJOR, MINOR = "HereBeDragons-Pins-1.0", 16 |
assert(LibStub, MAJOR .. " requires LibStub") |
local pins, oldversion = LibStub:NewLibrary(MAJOR, MINOR) |
if not pins then return end |
local HBD = LibStub("HereBeDragons-1.0") |
pins.updateFrame = pins.updateFrame or CreateFrame("Frame") |
-- storage for minimap pins |
pins.minimapPins = pins.minimapPins or {} |
pins.activeMinimapPins = pins.activeMinimapPins or {} |
pins.minimapPinRegistry = pins.minimapPinRegistry or {} |
-- and worldmap pins |
pins.worldmapPins = pins.worldmapPins or {} |
pins.worldmapPinRegistry = pins.worldmapPinRegistry or {} |
-- store a reference to the active minimap object |
pins.Minimap = pins.Minimap or Minimap |
-- upvalue lua api |
local cos, sin, max = math.cos, math.sin, math.max |
local type, pairs = type, pairs |
-- upvalue wow api |
local GetPlayerFacing = GetPlayerFacing |
-- upvalue data tables |
local minimapPins = pins.minimapPins |
local activeMinimapPins = pins.activeMinimapPins |
local minimapPinRegistry = pins.minimapPinRegistry |
local worldmapPins = pins.worldmapPins |
local worldmapPinRegistry = pins.worldmapPinRegistry |
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 |
}, |
} |
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 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 |
local minimapScale, minimapShape, mapRadius, minimapWidth, minimapHeight, mapSin, mapCos |
local lastZoom, lastFacing, lastXY, lastYY |
local worldmapWidth, worldmapHeight = WorldMapButton:GetWidth(), WorldMapButton:GetHeight() |
local function drawMinimapPin(pin, data) |
local xDist, yDist = lastXY - data.x, lastYY - data.y |
-- handle rotation |
if rotateMinimap then |
local dx, dy = xDist, yDist |
xDist = dx*mapCos - dy*mapSin |
yDist = dx*mapSin + dy*mapCos |
end |
-- adapt delta position to the map radius |
local diffX = xDist / mapRadius |
local diffY = yDist / mapRadius |
-- different minimap shapes |
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 |
if dist <= 1 or data.floatOnEdge 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 UpdateMinimapPins(force) |
-- get the current player position |
local x, y, instanceID = HBD:GetPlayerWorldPosition() |
local mapID, mapFloor = HBD:GetPlayerZone() |
-- 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, data 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"] |
mapRadius = minimap_size[indoors][zoom] / 2 |
minimapWidth = pins.Minimap:GetWidth() / 2 |
minimapHeight = pins.Minimap:GetHeight() / 2 |
-- update upvalues for icon placement |
lastZoom = zoom |
lastFacing = facing |
lastXY, lastYY = x, y |
if rotateMinimap then |
mapSin = sin(facing) |
mapCos = cos(facing) |
end |
for pin, data in pairs(minimapPins) do |
if data.instanceID == instanceID and (not data.floor or (data.floor == mapFloor and (data.floor == 0 or data.mapID == mapID))) then |
activeMinimapPins[pin] = data |
data.keep = true |
-- draw the pin (this may reset data.keep if outside of the map) |
drawMinimapPin(pin, data) |
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 |
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 = HBD:GetPlayerWorldPosition() |
-- 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 |
mapRadius = minimap_size[indoors][zoom] / 2 |
-- 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() |
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 |
local function PositionWorldMapIcon(icon, data, currentMapID, currentMapFloor) |
-- special handling for the azeroth world map |
-- translating coordinates to the azeroth map requires passing the instance ID |
-- of the origin continent, so the appropriate coordinates can be calculated |
if currentMapID == WORLDMAP_AZEROTH_ID then |
currentMapFloor = data.instanceID |
end |
local x, y = HBD:GetZoneCoordinatesFromWorld(data.x, data.y, currentMapID, currentMapFloor) |
if x and y then |
icon:ClearAllPoints() |
icon:SetPoint("CENTER", WorldMapButton, "TOPLEFT", x * worldmapWidth, -y * worldmapHeight) |
icon:Show() |
else |
icon:Hide() |
end |
end |
local function GetWorldMapLocation() |
local mapID, mapFloor = GetCurrentMapAreaID(), GetCurrentMapDungeonLevel() |
-- override the mapID for the azeroth world map |
if mapID == -1 and GetCurrentMapContinent() == 0 and GetCurrentMapZone() == 0 then |
mapID = WORLDMAP_AZEROTH_ID |
mapFloor = 0 |
end |
return mapID, mapFloor |
end |
local function UpdateWorldMap() |
if not WorldMapButton:IsVisible() then return end |
local mapID, mapFloor = GetWorldMapLocation() |
-- not viewing a valid map |
if not mapID or mapID == -1 then |
for icon in pairs(worldmapPins) do |
icon:Hide() |
end |
return |
end |
local instanceID = HBD.mapData[mapID] and HBD.mapData[mapID].instance or -1 |
worldmapWidth = WorldMapButton:GetWidth() |
worldmapHeight = WorldMapButton:GetHeight() |
for icon, data in pairs(worldmapPins) do |
if (instanceID == data.instanceID or mapID == WORLDMAP_AZEROTH_ID) and (not data.floor or (data.floor == mapFloor and (data.floor == 0 or data.mapID == mapID))) then |
PositionWorldMapIcon(icon, data, mapID, mapFloor) |
else |
icon:Hide() |
end |
end |
end |
local function UpdateMaps() |
UpdateMinimapZoom() |
UpdateMinimapPins() |
UpdateWorldMap() |
end |
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 |
UpdateMinimapZoom() |
UpdateMinimapPins() |
elseif event == "PLAYER_LOGIN" then |
-- recheck cvars after login |
rotateMinimap = GetCVar("rotateMinimap") == "1" |
elseif event == "PLAYER_ENTERING_WORLD" then |
UpdateMaps() |
elseif event == "WORLD_MAP_UPDATE" then |
UpdateWorldMap() |
end |
end |
pins.updateFrame:SetScript("OnEvent", OnEventHandler) |
pins.updateFrame:UnregisterAllEvents() |
pins.updateFrame:RegisterEvent("CVAR_UPDATE") |
pins.updateFrame:RegisterEvent("MINIMAP_UPDATE_ZOOM") |
pins.updateFrame:RegisterEvent("PLAYER_LOGIN") |
pins.updateFrame:RegisterEvent("PLAYER_ENTERING_WORLD") |
pins.updateFrame:RegisterEvent("WORLD_MAP_UPDATE") |
HBD.RegisterCallback(pins, "PlayerZoneChanged", UpdateMaps) |
--- Add a icon to the minimap (x/y world coordinate version) |
-- Note: This API does not let you specify a floor, as floors are map-specific, not instance/world wide. Use the Map/Floor API to specify a floor. |
-- @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") |
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.mapID = nil |
t.floor = nil |
minimapPins[icon] = t |
queueFullUpdate = true |
icon:SetParent(pins.Minimap) |
end |
--- Add a icon to the minimap (mapid/floor coordinate version) |
-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier) |
-- @param icon Icon Frame |
-- @param mapID Map ID of the map to place the icon on |
-- @param mapFloor Floor to place the icon on (or nil for all floors) |
-- @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 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:AddMinimapIconMF(ref, icon, mapID, mapFloor, x, y, floatOnEdge) |
if not ref then |
error(MAJOR..": AddMinimapIconMF: 'ref' must not be nil") |
end |
if type(icon) ~= "table" or not icon.SetPoint then |
error(MAJOR..": AddMinimapIconMF: 'icon' must be a frame") |
end |
if type(mapID) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then |
error(MAJOR..": AddMinimapIconMF: 'mapID', 'x' and 'y' must be numbers") |
end |
-- convert to world coordinates and use our known adding function |
local xCoord, yCoord, instanceID = HBD:GetWorldCoordinatesFromZone(x, y, mapID, mapFloor) |
if not xCoord then return end |
self:AddMinimapIconWorld(ref, icon, instanceID, xCoord, yCoord, floatOnEdge) |
-- store extra information |
minimapPins[icon].mapID = mapID |
minimapPins[icon].floor = mapFloor |
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 |
--- Add a icon to the world map (x/y world coordinate version) |
-- Note: This API does not let you specify a floor, as floors are map-specific, not instance/world wide. Use the Map/Floor API to specify a floor. |
-- @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 |
function pins:AddWorldMapIconWorld(ref, icon, instanceID, x, y) |
if not ref then |
error(MAJOR..": AddWorldMapIconWorld: 'ref' must not be nil") |
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 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.mapID = nil |
t.floor = nil |
worldmapPins[icon] = t |
if WorldMapButton:IsVisible() then |
local currentMapID, currentMapFloor = GetWorldMapLocation() |
if currentMapID and HBD.mapData[currentMapID] and (HBD.mapData[currentMapID].instance == instanceID or currentMapID == WORLDMAP_AZEROTH_ID) then |
PositionWorldMapIcon(icon, t, currentMapID, currentMapFloor) |
else |
icon:Hide() |
end |
end |
end |
--- Add a icon to the world map (mapid/floor coordinate version) |
-- @param ref Reference to your addon to track the icon under (ie. your "self" or string identifier) |
-- @param icon Icon Frame |
-- @param mapID Map ID of the map to place the icon on |
-- @param mapFloor Floor to place the icon on (or nil for all floors) |
-- @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 |
function pins:AddWorldMapIconMF(ref, icon, mapID, mapFloor, x, y) |
if not ref then |
error(MAJOR..": AddWorldMapIconMF: 'ref' must not be nil") |
end |
if type(icon) ~= "table" or not icon.SetPoint then |
error(MAJOR..": AddWorldMapIconMF: 'icon' must be a frame") |
end |
if type(mapID) ~= "number" or type(x) ~= "number" or type(y) ~= "number" then |
error(MAJOR..": AddWorldMapIconMF: 'mapID', 'x' and 'y' must be numbers") |
end |
-- convert to world coordinates |
local xCoord, yCoord, instanceID = HBD:GetWorldCoordinatesFromZone(x, y, mapID, mapFloor) |
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.mapID = mapID |
t.floor = mapFloor |
worldmapPins[icon] = t |
if WorldMapButton:IsVisible() then |
local currentMapID, currentMapFloor = GetWorldMapLocation() |
if currentMapID and HBD.mapData[currentMapID] and (HBD.mapData[currentMapID].instance == instanceID or currentMapID == WORLDMAP_AZEROTH_ID) |
and (not mapFloor or (currentMapFloor == mapFloor and (mapFloor == 0 or currentMapID == mapID))) then |
PositionWorldMapIcon(icon, t, currentMapID, currentMapFloor) |
else |
icon:Hide() |
end |
end |
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() |
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() |
end |
wipe(worldmapPinRegistry[ref]) |
end |
--- Return the angle and distance from the player to the specified pin |
-- @param icon icon object (minimap or worldmap) |
-- @return angle, distance where angle is in radians and distance in yards |
function pins:GetVectorToIcon(icon) |
if not icon then return nil, nil end |
local data = minimapPins[icon] or worldmapPins[icon] |
if not data then return nil, nil end |
local x, y, instance = HBD:GetPlayerWorldPosition() |
if not x or not y or instance ~= data.instanceID then return nil end |
return HBD:GetWorldVector(instance, x, y, data.x, data.y) |
end |