Quantcast

Update to ACE r1175-alpha

Ludovicus [06-08-18 - 20:05]
Update to ACE r1175-alpha
Filename
libs/AceConfig-3.0/AceConfig-3.0.lua
libs/AceConfig-3.0/AceConfig-3.0.xml
libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.lua
libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.xml
libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua
libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.xml
libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.lua
libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.xml
libs/AceDB-3.0/AceDB-3.0.lua
libs/AceDB-3.0/AceDB-3.0.xml
libs/AceDBOptions-3.0/AceDBOptions-3.0.lua
libs/AceDBOptions-3.0/AceDBOptions-3.0.xml
libs/AceEvent-3.0/AceEvent-3.0.lua
libs/AceEvent-3.0/AceEvent-3.0.xml
libs/AceGUI-3.0/widgets/AceGUIContainer-Frame.lua
libs/AceGUI-3.0/widgets/AceGUIContainer-ScrollFrame.lua
libs/AceGUI-3.0/widgets/AceGUIContainer-TabGroup.lua
libs/AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua
libs/AceGUI-3.0/widgets/AceGUIContainer-Window.lua
libs/AceGUI-3.0/widgets/AceGUIWidget-Button.lua
libs/AceGUI-3.0/widgets/AceGUIWidget-CheckBox.lua
libs/AceGUI-3.0/widgets/AceGUIWidget-ColorPicker.lua
libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown-Items.lua
libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown.lua
libs/AceGUI-3.0/widgets/AceGUIWidget-EditBox.lua
libs/AceGUI-3.0/widgets/AceGUIWidget-InteractiveLabel.lua
libs/AceGUI-3.0/widgets/AceGUIWidget-Label.lua
libs/AceGUI-3.0/widgets/AceGUIWidget-Slider.lua
libs/CallbackHandler-1.0/CallbackHandler-1.0.lua
libs/CallbackHandler-1.0/CallbackHandler-1.0.xml
libs/HereBeDragons/CHANGES.txt
libs/HereBeDragons/HereBeDragons-1.0.lua
libs/HereBeDragons/HereBeDragons-2.0.lua
libs/HereBeDragons/HereBeDragons-Migrate.lua
libs/HereBeDragons/HereBeDragons-Pins-1.0.lua
libs/HereBeDragons/HereBeDragons-Pins-2.0.lua
diff --git a/libs/AceConfig-3.0/AceConfig-3.0.lua b/libs/AceConfig-3.0/AceConfig-3.0.lua
index 3bedf8c..a99ddf7 100755
--- a/libs/AceConfig-3.0/AceConfig-3.0.lua
+++ b/libs/AceConfig-3.0/AceConfig-3.0.lua
@@ -3,7 +3,7 @@
 -- as well as associate it with a slash command.
 -- @class file
 -- @name AceConfig-3.0
--- @release $Id: AceConfig-3.0.lua 969 2010-10-07 02:11:48Z shefki $
+-- @release $Id: AceConfig-3.0.lua 1161 2017-08-12 14:30:16Z funkydude $

 --[[
 AceConfig-3.0
@@ -12,13 +12,14 @@ Very light wrapper library that combines all the AceConfig subcomponents into on

 ]]

-local MAJOR, MINOR = "AceConfig-3.0", 2
+local cfgreg = LibStub("AceConfigRegistry-3.0")
+local cfgcmd = LibStub("AceConfigCmd-3.0")
+
+local MAJOR, MINOR = "AceConfig-3.0", 3
 local AceConfig = LibStub:NewLibrary(MAJOR, MINOR)

 if not AceConfig then return end

-local cfgreg = LibStub("AceConfigRegistry-3.0")
-local cfgcmd = LibStub("AceConfigCmd-3.0")
 --TODO: local cfgdlg = LibStub("AceConfigDialog-3.0", true)
 --TODO: local cfgdrp = LibStub("AceConfigDropdown-3.0", true)

diff --git a/libs/AceConfig-3.0/AceConfig-3.0.xml b/libs/AceConfig-3.0/AceConfig-3.0.xml
index 87972ad..a3569b7 100755
--- a/libs/AceConfig-3.0/AceConfig-3.0.xml
+++ b/libs/AceConfig-3.0/AceConfig-3.0.xml
@@ -5,4 +5,4 @@
 	<Include file="AceConfigDialog-3.0\AceConfigDialog-3.0.xml"/>
 	<!--<Include file="AceConfigDropdown-3.0\AceConfigDropdown-3.0.xml"/>-->
 	<Script file="AceConfig-3.0.lua"/>
-</Ui>
\ No newline at end of file
+</Ui>
diff --git a/libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.lua b/libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.lua
index 2023981..33f9fe1 100755
--- a/libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.lua
+++ b/libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.lua
@@ -1,7 +1,7 @@
 --- AceConfigCmd-3.0 handles access to an options table through the "command line" interface via the ChatFrames.
 -- @class file
 -- @name AceConfigCmd-3.0
--- @release $Id: AceConfigCmd-3.0.lua 1045 2011-12-09 17:58:40Z nevcairiel $
+-- @release $Id: AceConfigCmd-3.0.lua 1161 2017-08-12 14:30:16Z funkydude $

 --[[
 AceConfigCmd-3.0
@@ -14,8 +14,9 @@ REQUIRES: AceConsole-3.0 for command registration (loaded on demand)

 -- TODO: plugin args

+local cfgreg = LibStub("AceConfigRegistry-3.0")

-local MAJOR, MINOR = "AceConfigCmd-3.0", 13
+local MAJOR, MINOR = "AceConfigCmd-3.0", 14
 local AceConfigCmd = LibStub:NewLibrary(MAJOR, MINOR)

 if not AceConfigCmd then return end
@@ -23,7 +24,6 @@ if not AceConfigCmd then return end
 AceConfigCmd.commands = AceConfigCmd.commands or {}
 local commands = AceConfigCmd.commands

-local cfgreg = LibStub("AceConfigRegistry-3.0")
 local AceConsole -- LoD
 local AceConsoleName = "AceConsole-3.0"

diff --git a/libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.xml b/libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.xml
index 188d354..9e157b5 100755
--- a/libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.xml
+++ b/libs/AceConfig-3.0/AceConfigCmd-3.0/AceConfigCmd-3.0.xml
@@ -1,4 +1,4 @@
 <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="AceConfigCmd-3.0.lua"/>
-</Ui>
\ No newline at end of file
+</Ui>
diff --git a/libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua b/libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua
index 8dbd134..66416e8 100755
--- a/libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua
+++ b/libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua
@@ -1,10 +1,13 @@
 --- AceConfigDialog-3.0 generates AceGUI-3.0 based windows based on option tables.
 -- @class file
 -- @name AceConfigDialog-3.0
--- @release $Id: AceConfigDialog-3.0.lua 1113 2014-09-11 20:18:16Z nevcairiel $
+-- @release $Id: AceConfigDialog-3.0.lua 1169 2018-02-27 16:18:28Z nevcairiel $

 local LibStub = LibStub
-local MAJOR, MINOR = "AceConfigDialog-3.0", 59
+local gui = LibStub("AceGUI-3.0")
+local reg = LibStub("AceConfigRegistry-3.0")
+
+local MAJOR, MINOR = "AceConfigDialog-3.0", 66
 local AceConfigDialog, oldminor = LibStub:NewLibrary(MAJOR, MINOR)

 if not AceConfigDialog then return end
@@ -17,9 +20,6 @@ AceConfigDialog.frame.apps = AceConfigDialog.frame.apps or {}
 AceConfigDialog.frame.closing = AceConfigDialog.frame.closing or {}
 AceConfigDialog.frame.closeAllOverride = AceConfigDialog.frame.closeAllOverride or {}

-local gui = LibStub("AceGUI-3.0")
-local reg = LibStub("AceConfigRegistry-3.0")
-
 -- Lua APIs
 local tconcat, tinsert, tsort, tremove, tsort = table.concat, table.insert, table.sort, table.remove, table.sort
 local strmatch, format = string.match, string.format
@@ -542,16 +542,16 @@ local function OptionOnMouseOver(widget, event)

 	if descStyle and descStyle ~= "tooltip" then return end

-	GameTooltip:SetText(name, 1, .82, 0, 1)
+	GameTooltip:SetText(name, 1, .82, 0, true)

 	if opt.type == "multiselect" then
-		GameTooltip:AddLine(user.text,0.5, 0.5, 0.8, 1)
+		GameTooltip:AddLine(user.text, 0.5, 0.5, 0.8, true)
 	end
 	if type(desc) == "string" then
-		GameTooltip:AddLine(desc, 1, 1, 1, 1)
+		GameTooltip:AddLine(desc, 1, 1, 1, true)
 	end
 	if type(usage) == "string" then
-		GameTooltip:AddLine("Usage: "..usage, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, 1)
+		GameTooltip:AddLine("Usage: "..usage, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, true)
 	end

 	GameTooltip:Show()
@@ -611,6 +611,31 @@ local function confirmPopup(appName, rootframe, basepath, info, message, func, .
 	end
 end

+local function validationErrorPopup(message)
+	if not StaticPopupDialogs["ACECONFIGDIALOG30_VALIDATION_ERROR_DIALOG"] then
+		StaticPopupDialogs["ACECONFIGDIALOG30_VALIDATION_ERROR_DIALOG"] = {}
+	end
+	local t = StaticPopupDialogs["ACECONFIGDIALOG30_VALIDATION_ERROR_DIALOG"]
+	t.text = message
+	t.button1 = OKAY
+	t.preferredIndex = STATICPOPUP_NUMDIALOGS
+	local dialog, oldstrata
+	t.OnAccept = function()
+		if dialog and oldstrata then
+			dialog:SetFrameStrata(oldstrata)
+		end
+	end
+	t.timeout = 0
+	t.whileDead = 1
+	t.hideOnEscape = 1
+
+	dialog = StaticPopup_Show("ACECONFIGDIALOG30_VALIDATION_ERROR_DIALOG")
+	if dialog then
+		oldstrata = dialog:GetFrameStrata()
+		dialog:SetFrameStrata("TOOLTIP")
+	end
+end
+
 local function ActivateControl(widget, event, ...)
 	--This function will call the set / execute handler for the widget
 	--widget:GetUserDataTable() contains the needed info
@@ -696,32 +721,26 @@ local function ActivateControl(widget, event, ...)
 	end

 	local rootframe = user.rootframe
-	if type(validated) == "string" then
-		--validate function returned a message to display
-		if rootframe.SetStatusText then
-			rootframe:SetStatusText(validated)
-		else
-			-- TODO: do something else.
-		end
-		PlaySound("igPlayerInviteDecline")
-		del(info)
-		return true
-	elseif not validated then
-		--validate returned false
-		if rootframe.SetStatusText then
+	if not validated or type(validated) == "string" then
+		if not validated then
 			if usage then
-				rootframe:SetStatusText(name..": "..usage)
+				validated = name..": "..usage
 			else
 				if pattern then
-					rootframe:SetStatusText(name..": Expected "..pattern)
+					validated = name..": Expected "..pattern
 				else
-					rootframe:SetStatusText(name..": Invalid Value")
+					validated = name..": Invalid Value"
 				end
 			end
+		end
+
+		-- show validate message
+		if rootframe.SetStatusText then
+			rootframe:SetStatusText(validated)
 		else
-			-- TODO: do something else
+			validationErrorPopup(validated)
 		end
-		PlaySound("igPlayerInviteDecline")
+		PlaySound(882) -- SOUNDKIT.IG_PLAYER_INVITE_DECLINE || _DECLINE is actually missing from the table
 		del(info)
 		return true
 	else
@@ -1015,6 +1034,7 @@ local function BuildGroups(group, options, path, appName, recurse)
 				entry.value = k
 				entry.text = GetOptionsMemberValue("name", v, options, path, appName)
 				entry.icon = GetOptionsMemberValue("icon", v, options, path, appName)
+				entry.iconCoords = GetOptionsMemberValue("iconCoords", v, options, path, appName)
 				entry.disabled = CheckOptionDisabled(v, options, path, appName)
 				tinsert(tree,entry)
 				if recurse and (v.childGroups or "tree") == "tree" then
@@ -1092,7 +1112,7 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
 					local imageCoords = GetOptionsMemberValue("imageCoords",v, options, path, appName)
 					local image, width, height = GetOptionsMemberValue("image",v, options, path, appName)

-					if type(image) == "string" then
+					if type(image) == "string" or type(image) == "number" then
 						control = gui:Create("Icon")
 						if not width then
 							width = GetOptionsMemberValue("imageWidth",v, options, path, appName)
@@ -1154,7 +1174,7 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
 					local image = GetOptionsMemberValue("image", v, options, path, appName)
 					local imageCoords = GetOptionsMemberValue("imageCoords", v, options, path, appName)

-					if type(image) == "string" then
+					if type(image) == "string" or type(image) == "number" then
 						if type(imageCoords) == "table" then
 							control:SetImage(image, unpack(imageCoords))
 						else
@@ -1207,6 +1227,8 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
 								radio:SetWidth(width_multiplier * 2)
 							elseif width == "half" then
 								radio:SetWidth(width_multiplier / 2)
+							elseif (type(width) == "number") then
+								radio:SetWidth(width_multiplier * width)
 							elseif width == "full" then
 								radio.width = "fill"
 							else
@@ -1269,6 +1291,8 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
 							control:SetWidth(width_multiplier * 2)
 						elseif width == "half" then
 							control:SetWidth(width_multiplier / 2)
+						elseif (type(width) == "number") then
+							control:SetWidth(width_multiplier * width)
 						elseif width == "full" then
 							control.width = "fill"
 						else
@@ -1305,6 +1329,8 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
 								check:SetWidth(width_multiplier * 2)
 							elseif width == "half" then
 								check:SetWidth(width_multiplier / 2)
+							elseif (type(width) == "number") then
+								control:SetWidth(width_multiplier * width)
 							elseif width == "full" then
 								check.width = "fill"
 							else
@@ -1354,7 +1380,7 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
 					local imageCoords = GetOptionsMemberValue("imageCoords",v, options, path, appName)
 					local image, width, height = GetOptionsMemberValue("image",v, options, path, appName)

-					if type(image) == "string" then
+					if type(image) == "string" or type(image) == "number" then
 						if not width then
 							width = GetOptionsMemberValue("imageWidth",v, options, path, appName)
 						end
@@ -1386,6 +1412,8 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin
 							control:SetWidth(width_multiplier * 2)
 						elseif width == "half" then
 							control:SetWidth(width_multiplier / 2)
+						elseif (type(width) == "number") then
+							control:SetWidth(width_multiplier * width)
 						elseif width == "full" then
 							control.width = "fill"
 						else
@@ -1448,10 +1476,10 @@ local function TreeOnButtonEnter(widget, event, uniquevalue, button)
 		GameTooltip:SetPoint("LEFT",button,"RIGHT")
 	end

-	GameTooltip:SetText(name, 1, .82, 0, 1)
+	GameTooltip:SetText(name, 1, .82, 0, true)

 	if type(desc) == "string" then
-		GameTooltip:AddLine(desc, 1, 1, 1, 1)
+		GameTooltip:AddLine(desc, 1, 1, 1, true)
 	end

 	GameTooltip:Show()
diff --git a/libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.xml b/libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.xml
index 86ce057..8e1e606 100755
--- a/libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.xml
+++ b/libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.xml
@@ -1,4 +1,4 @@
 <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="AceConfigDialog-3.0.lua"/>
-</Ui>
\ No newline at end of file
+</Ui>
diff --git a/libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.lua b/libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.lua
index d684d66..f8ac3f9 100755
--- a/libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.lua
+++ b/libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.lua
@@ -8,16 +8,16 @@
 -- :IterateOptionsTables() (and :GetOptionsTable() if only given one argument) return a function reference that the requesting config handling addon must call with valid "uiType", "uiName".
 -- @class file
 -- @name AceConfigRegistry-3.0
--- @release $Id: AceConfigRegistry-3.0.lua 1105 2013-12-08 22:11:58Z nevcairiel $
-local MAJOR, MINOR = "AceConfigRegistry-3.0", 15
+-- @release $Id: AceConfigRegistry-3.0.lua 1169 2018-02-27 16:18:28Z nevcairiel $
+local CallbackHandler = LibStub("CallbackHandler-1.0")
+
+local MAJOR, MINOR = "AceConfigRegistry-3.0", 18
 local AceConfigRegistry = LibStub:NewLibrary(MAJOR, MINOR)

 if not AceConfigRegistry then return end

 AceConfigRegistry.tables = AceConfigRegistry.tables or {}

-local CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0")
-
 if not AceConfigRegistry.callbacks then
 	AceConfigRegistry.callbacks = CallbackHandler:New(AceConfigRegistry)
 end
@@ -57,6 +57,7 @@ local istable={["table"]=true,   _="table"}
 local ismethodtable={["table"]=true,["string"]=true,["function"]=true,   _="methodname, funcref or table"}
 local optstring={["nil"]=true,["string"]=true, _="string"}
 local optstringfunc={["nil"]=true,["string"]=true,["function"]=true, _="string or funcref"}
+local optstringnumberfunc={["nil"]=true,["string"]=true,["number"]=true,["function"]=true, _="string, number or funcref"}
 local optnumber={["nil"]=true,["number"]=true, _="number"}
 local optmethod={["nil"]=true,["string"]=true,["function"]=true, _="methodname or funcref"}
 local optmethodfalse={["nil"]=true,["string"]=true,["function"]=true,["boolean"]={[false]=true},  _="methodname, funcref or false"}
@@ -66,6 +67,7 @@ local optmethodbool={["nil"]=true,["string"]=true,["function"]=true,["boolean"]=
 local opttable={["nil"]=true,["table"]=true,  _="table"}
 local optbool={["nil"]=true,["boolean"]=true,  _="boolean"}
 local optboolnumber={["nil"]=true,["boolean"]=true,["number"]=true,  _="boolean or number"}
+local optstringnumber={["nil"]=true,["string"]=true,["number"]=true, _="string or number"}

 local basekeys={
 	type=isstring,
@@ -82,20 +84,20 @@ local basekeys={
 		dialogHidden=optmethodbool,
 		dropdownHidden=optmethodbool,
 	cmdHidden=optmethodbool,
-	icon=optstringfunc,
+	icon=optstringnumberfunc,
 	iconCoords=optmethodtable,
 	handler=opttable,
 	get=optmethodfalse,
 	set=optmethodfalse,
 	func=optmethodfalse,
 	arg={["*"]=true},
-	width=optstring,
+	width=optstringnumber,
 }

 local typedkeys={
 	header={},
 	description={
-		image=optstringfunc,
+		image=optstringnumberfunc,
 		imageCoords=optmethodtable,
 		imageHeight=optnumber,
 		imageWidth=optnumber,
@@ -112,7 +114,7 @@ local typedkeys={
 		childGroups=optstring,
 	},
 	execute={
-		image=optstringfunc,
+		image=optstringnumberfunc,
 		imageCoords=optmethodtable,
 		imageHeight=optnumber,
 		imageWidth=optnumber,
@@ -127,7 +129,7 @@ local typedkeys={
 	},
 	toggle={
 		tristate=optbool,
-		image=optstringfunc,
+		image=optstringnumberfunc,
 		imageCoords=optmethodtable,
 	},
 	tristate={
diff --git a/libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.xml b/libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.xml
index 101bfda..4ea69ca 100755
--- a/libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.xml
+++ b/libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.xml
@@ -1,4 +1,4 @@
 <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="AceConfigRegistry-3.0.lua"/>
-</Ui>
\ No newline at end of file
+</Ui>
diff --git a/libs/AceDB-3.0/AceDB-3.0.lua b/libs/AceDB-3.0/AceDB-3.0.lua
index 79f4b39..b42b442 100755
--- a/libs/AceDB-3.0/AceDB-3.0.lua
+++ b/libs/AceDB-3.0/AceDB-3.0.lua
@@ -10,6 +10,7 @@
 -- * **race** Race-specific data. All of the players characters of the same race share this database.
 -- * **faction** Faction-specific data. All of the players characters of the same faction share this database.
 -- * **factionrealm** Faction and realm specific data. All of the players characters on the same realm and of the same faction share this database.
+-- * **locale** Locale specific data, based on the locale of the players game client.
 -- * **global** Global Data. All characters on the same account share this database.
 -- * **profile** Profile-specific data. All characters using the same profile share this database. The user can control which profile should be used.
 --
@@ -39,8 +40,8 @@
 -- end
 -- @class file
 -- @name AceDB-3.0.lua
--- @release $Id: AceDB-3.0.lua 1115 2014-09-21 11:52:35Z kaelten $
-local ACEDB_MAJOR, ACEDB_MINOR = "AceDB-3.0", 25
+-- @release $Id: AceDB-3.0.lua 1142 2016-07-11 08:36:19Z nevcairiel $
+local ACEDB_MAJOR, ACEDB_MINOR = "AceDB-3.0", 26
 local AceDB, oldminor = LibStub:NewLibrary(ACEDB_MAJOR, ACEDB_MINOR)

 if not AceDB then return end -- No upgrade needed
@@ -263,7 +264,7 @@ local factionrealmKey = factionKey .. " - " .. realmKey
 local localeKey = GetLocale():lower()

 local regionTable = { "US", "KR", "EU", "TW", "CN" }
-local regionKey = _G["GetCurrentRegion"] and regionTable[GetCurrentRegion()] or string.sub(GetCVar("realmList"), 1, 2):upper()
+local regionKey = regionTable[GetCurrentRegion()]
 local factionrealmregionKey = factionrealmKey .. " - " .. regionKey

 -- Actual database initialization function
@@ -303,7 +304,7 @@ local function initdb(sv, defaults, defaultProfile, olddb, parent)
 		["factionrealm"] = factionrealmKey,
 		["factionrealmregion"] = factionrealmregionKey,
 		["profile"] = profileKey,
-        ["locale"] = localeKey,
+		["locale"] = localeKey,
 		["global"] = true,
 		["profiles"] = true,
 	}
diff --git a/libs/AceDB-3.0/AceDB-3.0.xml b/libs/AceDB-3.0/AceDB-3.0.xml
index 46b20ba..108fc70 100755
--- a/libs/AceDB-3.0/AceDB-3.0.xml
+++ b/libs/AceDB-3.0/AceDB-3.0.xml
@@ -1,4 +1,4 @@
 <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="AceDB-3.0.lua"/>
-</Ui>
\ No newline at end of file
+</Ui>
diff --git a/libs/AceDBOptions-3.0/AceDBOptions-3.0.lua b/libs/AceDBOptions-3.0/AceDBOptions-3.0.lua
index 616f35e..5028fef 100755
--- a/libs/AceDBOptions-3.0/AceDBOptions-3.0.lua
+++ b/libs/AceDBOptions-3.0/AceDBOptions-3.0.lua
@@ -1,8 +1,8 @@
 --- AceDBOptions-3.0 provides a universal AceConfig options screen for managing AceDB-3.0 profiles.
 -- @class file
 -- @name AceDBOptions-3.0
--- @release $Id: AceDBOptions-3.0.lua 1066 2012-09-18 14:36:49Z nevcairiel $
-local ACEDBO_MAJOR, ACEDBO_MINOR = "AceDBOptions-3.0", 14
+-- @release $Id: AceDBOptions-3.0.lua 1140 2016-07-03 07:53:29Z nevcairiel $
+local ACEDBO_MAJOR, ACEDBO_MINOR = "AceDBOptions-3.0", 15
 local AceDBOptions, oldminor = LibStub:NewLibrary(ACEDBO_MAJOR, ACEDBO_MINOR)

 if not AceDBOptions then return end -- No upgrade needed
@@ -53,19 +53,19 @@ if LOCALE == "deDE" then
 	L["choose_sub"] = "Wählt ein bereits vorhandenes Profil aus."
 	L["copy"] = "Kopieren von..."
 	L["copy_desc"] = "Kopiere die Einstellungen von einem vorhandenen Profil in das aktive Profil."
-	-- L["current"] = "Current Profile:"
+	L["current"] = "Aktuelles Profil:"
 	L["default"] = "Standard"
 	L["delete"] = "Profil löschen"
 	L["delete_confirm"] = "Willst du das ausgewählte Profil wirklich löschen?"
-	L["delete_desc"] = "Lösche vorhandene oder unbenutzte Profile aus der Datenbank um Platz zu sparen und um die SavedVariables Datei 'sauber' zu halten."
+	L["delete_desc"] = "Lösche vorhandene oder unbenutzte Profile aus der Datenbank, um Platz zu sparen und die SavedVariables-Datei 'sauber' zu halten."
 	L["delete_sub"] = "Löscht ein Profil aus der Datenbank."
-	L["intro"] = "Hier kannst du das aktive Datenbankprofile ändern, damit du verschiedene Einstellungen für jeden Charakter erstellen kannst, wodurch eine sehr flexible Konfiguration möglich wird."
+	L["intro"] = "Hier kannst du das aktive Datenbankprofil ändern, damit du verschiedene Einstellungen für jeden Charakter erstellen kannst, wodurch eine sehr flexible Konfiguration möglich wird."
 	L["new"] = "Neu"
 	L["new_sub"] = "Ein neues Profil erstellen."
 	L["profiles"] = "Profile"
 	L["profiles_sub"] = "Profile verwalten"
 	L["reset"] = "Profil zurücksetzen"
-	L["reset_desc"] = "Setzt das momentane Profil auf Standardwerte zurück, für den Fall das mit der Konfiguration etwas schief lief oder weil du einfach neu starten willst."
+	L["reset_desc"] = "Setzt das momentane Profil auf Standardwerte zurück, für den Fall, dass mit der Konfiguration etwas schief lief oder weil du einfach neu starten willst."
 	L["reset_sub"] = "Das aktuelle Profil auf Standard zurücksetzen."
 elseif LOCALE == "frFR" then
 	L["choose"] = "Profils existants"
@@ -73,7 +73,7 @@ elseif LOCALE == "frFR" then
 	L["choose_sub"] = "Permet de choisir un des profils déjà disponibles."
 	L["copy"] = "Copier à partir de"
 	L["copy_desc"] = "Copie les paramètres d'un profil déjà existant dans le profil actuellement actif."
-	-- L["current"] = "Current Profile:"
+	L["current"] = "Profil actuel :"
 	L["default"] = "Défaut"
 	L["delete"] = "Supprimer un profil"
 	L["delete_confirm"] = "Etes-vous sûr de vouloir supprimer le profil sélectionné ?"
@@ -88,32 +88,32 @@ elseif LOCALE == "frFR" then
 	L["reset_desc"] = "Réinitialise le profil actuel au cas où votre configuration est corrompue ou si vous voulez tout simplement faire table rase."
 	L["reset_sub"] = "Réinitialise le profil actuel avec les paramètres par défaut."
 elseif LOCALE == "koKR" then
-	L["choose"] = "프로필 선택"
-	L["choose_desc"] = "새로운 이름을 입력하거나, 이미 있는 프로필중 하나를 선택하여 새로운 프로필을 만들 수 있습니다."
-	L["choose_sub"] = "당신이 현재 이용할수 있는 프로필을 선택합니다."
-	L["copy"] = "복사"
-	L["copy_desc"] = "현재 사용중인 프로필에, 선택한 프로필의 설정을 복사합니다."
-	-- L["current"] = "Current Profile:"
+	L["choose"] = "저장 중인 프로필"
+	L["choose_desc"] = "입력창에 새로운 이름을 입력하거나 저장 중인 프로필 중 하나를 선택하여 새로운 프로필을 만들 수 있습니다."
+	L["choose_sub"] = "현재 이용할 수 있는 프로필 중 하나를 선택합니다."
+	L["copy"] = "복사해오기"
+	L["copy_desc"] = "현재 사용 중인 프로필에 선택한 프로필의 설정을 복사합니다."
+	L["current"] = "현재 프로필:"
 	L["default"] = "기본값"
 	L["delete"] = "프로필 삭제"
-	L["delete_confirm"] = "정말로 선택한 프로필의 삭제를 원하십니까?"
-	L["delete_desc"] = "데이터베이스에 사용중이거나 저장된 프로파일 삭제로 SavedVariables 파일의 정리와 공간 절약이 됩니다."
+	L["delete_confirm"] = "정말로 선택한 프로필을 삭제할까요?"
+	L["delete_desc"] = "저장 공간 절약과 SavedVariables 파일의 정리를 위해 데이터베이스에서 사용하지 않는 프로필을 삭제하세요."
 	L["delete_sub"] = "데이터베이스의 프로필을 삭제합니다."
-	L["intro"] = "모든 캐릭터의 다양한 설정과 사용중인 데이터베이스 프로필, 어느것이던지 매우 다루기 쉽게 바꿀수 있습니다."
+	L["intro"] = "활성 데이터베이스 프로필을 변경할 수 있고, 각 캐릭터 별로 다른 설정을 할 수 있습니다."
 	L["new"] = "새로운 프로필"
 	L["new_sub"] = "새로운 프로필을 만듭니다."
 	L["profiles"] = "프로필"
-	L["profiles_sub"] = "프로필 설정"
+	L["profiles_sub"] = "프로필 관리"
 	L["reset"] = "프로필 초기화"
-	L["reset_desc"] = "단순히 다시 새롭게 구성을 원하는 경우, 현재 프로필을 기본값으로 초기화 합니다."
-	L["reset_sub"] = "현재의 프로필을 기본값으로 초기화 합니다"
+	L["reset_desc"] = "설정이 깨졌거나 처음부터 다시 설정을 원하는 경우, 현재 프로필을 기본값으로 초기화하세요."
+	L["reset_sub"] = "현재 프로필을 기본값으로 초기화합니다"
 elseif LOCALE == "esES" or LOCALE == "esMX" then
 	L["choose"] = "Perfiles existentes"
 	L["choose_desc"] = "Puedes crear un nuevo perfil introduciendo un nombre en el recuadro o puedes seleccionar un perfil de los ya existentes."
 	L["choose_sub"] = "Selecciona uno de los perfiles disponibles."
 	L["copy"] = "Copiar de"
 	L["copy_desc"] = "Copia los ajustes de un perfil existente al perfil actual."
-	-- L["current"] = "Current Profile:"
+	L["current"] = "Perfil actual:"
 	L["default"] = "Por defecto"
 	L["delete"] = "Borrar un Perfil"
 	L["delete_confirm"] = "¿Estas seguro que quieres borrar el perfil seleccionado?"
@@ -129,31 +129,31 @@ elseif LOCALE == "esES" or LOCALE == "esMX" then
 	L["reset_sub"] = "Reinicar el perfil actual al de por defecto"
 elseif LOCALE == "zhTW" then
 	L["choose"] = "現有的設定檔"
-	L["choose_desc"] = "你可以通過在文本框內輸入一個名字創立一個新的設定檔,也可以選擇一個已經存在的設定檔。"
-	L["choose_sub"] = "從當前可用的設定檔裏面選擇一個。"
+	L["choose_desc"] = "您可以在文字方塊內輸入名字以建立新的設定檔,或是選擇一個現有的設定檔使用。"
+	L["choose_sub"] = "從當前可用的設定檔裡面選擇一個。"
 	L["copy"] = "複製自"
-	L["copy_desc"] = "從當前某個已保存的設定檔複製到當前正使用的設定檔。"
-	-- L["current"] = "Current Profile:"
+	L["copy_desc"] = "從一個現有的設定檔,將設定複製到現在使用中的設定檔。"
+	L["current"] = "目前設定檔:"
 	L["default"] = "預設"
 	L["delete"] = "刪除一個設定檔"
-	L["delete_confirm"] = "你確定要刪除所選擇的設定檔嗎?"
-	L["delete_desc"] = "從資料庫裏刪除不再使用的設定檔,以節省空間,並且清理SavedVariables檔。"
-	L["delete_sub"] = "從資料庫裏刪除一個設定檔。"
-	L["intro"] = "你可以選擇一個活動的資料設定檔,這樣你的每個角色就可以擁有不同的設定值,可以給你的插件設定帶來極大的靈活性。"
+	L["delete_confirm"] = "確定要刪除所選擇的設定檔嗎?"
+	L["delete_desc"] = "從資料庫裡刪除不再使用的設定檔,以節省空間,並且清理 SavedVariables 檔案。"
+	L["delete_sub"] = "從資料庫裡刪除一個設定檔。"
+	L["intro"] = "您可以從資料庫中選擇一個設定檔來使用,如此就可以讓每個角色使用不同的設定。"
 	L["new"] = "新建"
 	L["new_sub"] = "新建一個空的設定檔。"
 	L["profiles"] = "設定檔"
 	L["profiles_sub"] = "管理設定檔"
 	L["reset"] = "重置設定檔"
-	L["reset_desc"] = "將當前的設定檔恢復到它的預設值,用於你的設定檔損壞,或者你只是想重來的情況。"
-	L["reset_sub"] = "將當前的設定檔恢復為預設值"
+	L["reset_desc"] = "將現用的設定檔重置為預設值;用於設定檔損壞,或者單純想要重來的情況。"
+	L["reset_sub"] = "將目前的設定檔重置為預設值"
 elseif LOCALE == "zhCN" then
 	L["choose"] = "现有的配置文件"
 	L["choose_desc"] = "你可以通过在文本框内输入一个名字创立一个新的配置文件,也可以选择一个已经存在的配置文件。"
 	L["choose_sub"] = "从当前可用的配置文件里面选择一个。"
 	L["copy"] = "复制自"
 	L["copy_desc"] = "从当前某个已保存的配置文件复制到当前正使用的配置文件。"
-	-- L["current"] = "Current Profile:"
+	L["current"] = "当前配置文件:"
 	L["default"] = "默认"
 	L["delete"] = "删除一个配置文件"
 	L["delete_confirm"] = "你确定要删除所选择的配置文件么?"
@@ -173,7 +173,7 @@ elseif LOCALE == "ruRU" then
 	L["choose_sub"] = "Выбор одиного из уже доступных профилей"
 	L["copy"] = "Скопировать из"
 	L["copy_desc"] = "Скопировать настройки из выбранного профиля в активный."
-	-- L["current"] = "Current Profile:"
+	L["current"] = "Текущий профиль:"
 	L["default"] = "По умолчанию"
 	L["delete"] = "Удалить профиль"
 	L["delete_confirm"] = "Вы уверены, что вы хотите удалить выбранный профиль?"
@@ -185,17 +185,17 @@ elseif LOCALE == "ruRU" then
 	L["profiles"] = "Профили"
 	L["profiles_sub"] = "Управление профилями"
 	L["reset"] = "Сброс профиля"
-	L["reset_desc"] = "Если ваша конфигурации испорчена или если вы хотите настроить всё заново - сбросьте текущий профиль на стандартные значения."
+	L["reset_desc"] = "Сбросить текущий профиль к стандартным настройкам, если ваша конфигурация испорчена или вы хотите настроить всё заново."
 	L["reset_sub"] = "Сброс текущего профиля на стандартный"
 elseif LOCALE == "itIT" then
-	L["choose"] = "Profili esistenti"
-	L["choose_desc"] = "Puoi creare un nuovo profilo digitando il nome della casella di testo, oppure scegliendone uno tra i profili gia' esistenti."
-	L["choose_sub"] = "Seleziona uno dei profili disponibili."
+	L["choose"] = "Profili Esistenti"
+	L["choose_desc"] = "Puoi creare un nuovo profilo digitando il nome della casella di testo, oppure scegliendone uno tra i profili già esistenti."
+	L["choose_sub"] = "Seleziona uno dei profili attualmente disponibili."
 	L["copy"] = "Copia Da"
 	L["copy_desc"] = "Copia le impostazioni da un profilo esistente, nel profilo attivo in questo momento."
 	L["current"] = "Profilo Attivo:"
 	L["default"] = "Standard"
-	L["delete"] = "Cancella un profilo"
+	L["delete"] = "Cancella un Profilo"
 	L["delete_confirm"] = "Sei sicuro di voler cancellare il profilo selezionato?"
 	L["delete_desc"] = "Cancella i profili non utilizzati dal database per risparmiare spazio e mantenere puliti i file di configurazione SavedVariables."
 	L["delete_sub"] = "Cancella un profilo dal Database."
@@ -205,8 +205,28 @@ elseif LOCALE == "itIT" then
 	L["profiles"] = "Profili"
 	L["profiles_sub"] = "Gestisci Profili"
 	L["reset"] = "Reimposta Profilo"
-	L["reset_desc"] = "Riporta il tuo profilo attivo alle sue impostazioni di default, nel caso in cui la tua configurazione si sia corrotta, o semplicemente tu voglia re-inizializzarla."
-	L["reset_sub"] = "Reimposta il profilo ai suoi valori di default."
+	L["reset_desc"] = "Riporta il tuo profilo attivo alle sue impostazioni predefinite, nel caso in cui la tua configurazione si sia corrotta, o semplicemente tu voglia re-inizializzarla."
+	L["reset_sub"] = "Reimposta il profilo ai suoi valori predefiniti."
+elseif LOCALE == "ptBR" then
+	L["choose"] = "Perfis Existentes"
+	L["choose_desc"] = "Você pode tanto criar um perfil novo tanto digitando um nome na caixa de texto, quanto escolher um dos perfis já existentes."
+	L["choose_sub"] = "Selecione um de seus perfis atualmente disponíveis."
+	L["copy"] = "Copiar De"
+	L["copy_desc"] = "Copia as definições de um perfil existente no perfil atualmente ativo."
+	L["current"] = "Perfil Autal:"
+	L["default"] = "Padrão"
+	L["delete"] = "Remover um Perfil"
+	L["delete_confirm"] = "Tem certeza que deseja remover o perfil selecionado?"
+	L["delete_desc"] = "Remove perfis existentes e inutilizados do banco de dados para economizar espaço, e limpar o arquivo SavedVariables."
+	L["delete_sub"] = "Remove um perfil do banco de dados."
+	L["intro"] = "Você pode alterar o perfil do banco de dados ativo, para que possa ter definições diferentes para cada personagem."
+	L["new"] = "Novo"
+	L["new_sub"] = "Cria um novo perfil vazio."
+	L["profiles"] = "Perfis"
+	L["profiles_sub"] = "Gerenciar Perfis"
+	L["reset"] = "Resetar Perfil"
+	L["reset_desc"] = "Reseta o perfil atual para os valores padrões, no caso de sua configuração estar quebrada, ou simplesmente se deseja começar novamente."
+	L["reset_sub"] = "Resetar o perfil atual ao padrão"
 end

 local defaultProfiles
diff --git a/libs/AceDBOptions-3.0/AceDBOptions-3.0.xml b/libs/AceDBOptions-3.0/AceDBOptions-3.0.xml
index 2668fb0..51305f9 100755
--- a/libs/AceDBOptions-3.0/AceDBOptions-3.0.xml
+++ b/libs/AceDBOptions-3.0/AceDBOptions-3.0.xml
@@ -1,4 +1,4 @@
 <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="AceDBOptions-3.0.lua"/>
-</Ui>
\ No newline at end of file
+</Ui>
diff --git a/libs/AceEvent-3.0/AceEvent-3.0.lua b/libs/AceEvent-3.0/AceEvent-3.0.lua
index 578ae25..bbf55c2 100755
--- a/libs/AceEvent-3.0/AceEvent-3.0.lua
+++ b/libs/AceEvent-3.0/AceEvent-3.0.lua
@@ -9,8 +9,10 @@
 -- make into AceEvent.
 -- @class file
 -- @name AceEvent-3.0
--- @release $Id: AceEvent-3.0.lua 975 2010-10-23 11:26:18Z nevcairiel $
-local MAJOR, MINOR = "AceEvent-3.0", 3
+-- @release $Id: AceEvent-3.0.lua 1161 2017-08-12 14:30:16Z funkydude $
+local CallbackHandler = LibStub("CallbackHandler-1.0")
+
+local MAJOR, MINOR = "AceEvent-3.0", 4
 local AceEvent = LibStub:NewLibrary(MAJOR, MINOR)

 if not AceEvent then return end
@@ -18,8 +20,6 @@ if not AceEvent then return end
 -- Lua APIs
 local pairs = pairs

-local CallbackHandler = LibStub:GetLibrary("CallbackHandler-1.0")
-
 AceEvent.frame = AceEvent.frame or CreateFrame("Frame", "AceEvent30Frame") -- our event frame
 AceEvent.embeds = AceEvent.embeds or {} -- what objects embed this lib

diff --git a/libs/AceEvent-3.0/AceEvent-3.0.xml b/libs/AceEvent-3.0/AceEvent-3.0.xml
index 313ef4d..41ef791 100755
--- a/libs/AceEvent-3.0/AceEvent-3.0.xml
+++ b/libs/AceEvent-3.0/AceEvent-3.0.xml
@@ -1,4 +1,4 @@
 <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="AceEvent-3.0.lua"/>
-</Ui>
\ No newline at end of file
+</Ui>
diff --git a/libs/AceGUI-3.0/widgets/AceGUIContainer-Frame.lua b/libs/AceGUI-3.0/widgets/AceGUIContainer-Frame.lua
index 0dae68c..80fd582 100755
--- a/libs/AceGUI-3.0/widgets/AceGUIContainer-Frame.lua
+++ b/libs/AceGUI-3.0/widgets/AceGUIContainer-Frame.lua
@@ -1,7 +1,7 @@
 --[[-----------------------------------------------------------------------------
 Frame Container
 -------------------------------------------------------------------------------]]
-local Type, Version = "Frame", 24
+local Type, Version = "Frame", 26
 local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
 if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end

@@ -21,10 +21,14 @@ local CreateFrame, UIParent = CreateFrame, UIParent
 Scripts
 -------------------------------------------------------------------------------]]
 local function Button_OnClick(frame)
-	PlaySound("gsTitleOptionExit")
+	PlaySound(799) -- SOUNDKIT.GS_TITLE_OPTION_EXIT
 	frame.obj:Hide()
 end

+local function Frame_OnShow(frame)
+	frame.obj:Fire("OnShow")
+end
+
 local function Frame_OnClose(frame)
 	frame.obj:Fire("OnClose")
 end
@@ -186,6 +190,7 @@ local function Constructor()
 	frame:SetBackdropColor(0, 0, 0, 1)
 	frame:SetMinResize(400, 200)
 	frame:SetToplevel(true)
+	frame:SetScript("OnShow", Frame_OnShow)
 	frame:SetScript("OnHide", Frame_OnClose)
 	frame:SetScript("OnMouseDown", Frame_OnMouseDown)

diff --git a/libs/AceGUI-3.0/widgets/AceGUIContainer-ScrollFrame.lua b/libs/AceGUI-3.0/widgets/AceGUIContainer-ScrollFrame.lua
index 6dd0c4d..9afb54b 100755
--- a/libs/AceGUI-3.0/widgets/AceGUIContainer-ScrollFrame.lua
+++ b/libs/AceGUI-3.0/widgets/AceGUIContainer-ScrollFrame.lua
@@ -6,8 +6,6 @@ local Type, Version = "ScrollFrame", 24
 local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
 if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end

-local IsLegion = select(4, GetBuildInfo()) >= 70000
-
 -- Lua APIs
 local pairs, assert, type = pairs, assert, type
 local min, max, floor, abs = math.min, math.max, math.floor, math.abs
@@ -178,11 +176,7 @@ local function Constructor()

 	local scrollbg = scrollbar:CreateTexture(nil, "BACKGROUND")
 	scrollbg:SetAllPoints(scrollbar)
-	if IsLegion then
-		scrollbg:SetColorTexture(0, 0, 0, 0.4)
-	else
-		scrollbg:SetTexture(0, 0, 0, 0.4)
-	end
+	scrollbg:SetColorTexture(0, 0, 0, 0.4)

 	--Container Support
 	local content = CreateFrame("Frame", nil, scrollframe)
diff --git a/libs/AceGUI-3.0/widgets/AceGUIContainer-TabGroup.lua b/libs/AceGUI-3.0/widgets/AceGUIContainer-TabGroup.lua
index 00be129..95544c5 100755
--- a/libs/AceGUI-3.0/widgets/AceGUIContainer-TabGroup.lua
+++ b/libs/AceGUI-3.0/widgets/AceGUIContainer-TabGroup.lua
@@ -2,7 +2,7 @@
 TabGroup Container
 Container that uses tabs on top to switch between groups.
 -------------------------------------------------------------------------------]]
-local Type, Version = "TabGroup", 35
+local Type, Version = "TabGroup", 36
 local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
 if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end

@@ -63,7 +63,7 @@ Scripts
 -------------------------------------------------------------------------------]]
 local function Tab_OnClick(frame)
 	if not (frame.selected or frame.disabled) then
-		PlaySound("igCharacterInfoTab")
+		PlaySound(841) -- SOUNDKIT.IG_CHARACTER_INFO_TAB
 		frame.obj:SelectTab(frame.value)
 	end
 end
diff --git a/libs/AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua b/libs/AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua
index 9bf17d8..236f633 100755
--- a/libs/AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua
+++ b/libs/AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua
@@ -2,11 +2,11 @@
 TreeGroup Container
 Container that uses a tree control to switch between groups.
 -------------------------------------------------------------------------------]]
-local Type, Version = "TreeGroup", 40
+local Type, Version = "TreeGroup", 41
 local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
 if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end

-local IsLegion = select(4, GetBuildInfo()) >= 70000
+local WoW80 = select(4, GetBuildInfo()) >= 80000

 -- Lua APIs
 local next, pairs, ipairs, assert, type = next, pairs, ipairs, assert, type
@@ -164,7 +164,7 @@ end
 local function FirstFrameUpdate(frame)
 	local self = frame.obj
 	frame:SetScript("OnUpdate", nil)
-	self:RefreshTree()
+	self:RefreshTree(nil, true)
 end

 local function BuildUniqueValue(...)
@@ -302,6 +302,8 @@ local methods = {

 	["OnRelease"] = function(self)
 		self.status = nil
+		self.tree = nil
+		self.frame:SetScript("OnUpdate", nil)
 		for k, v in pairs(self.localstatus) do
 			if k == "groups" then
 				for k2 in pairs(v) do
@@ -390,8 +392,8 @@ local methods = {
 		end
 	end,

-	["RefreshTree"] = function(self,scrollToSelection)
-		local buttons = self.buttons
+	["RefreshTree"] = function(self,scrollToSelection,fromOnUpdate)
+		local buttons = self.buttons
 		local lines = self.lines

 		for i, v in ipairs(buttons) do
@@ -422,6 +424,12 @@ local methods = {
 		local maxlines = (floor(((self.treeframe:GetHeight()or 0) - 20 ) / 18))
 		if maxlines <= 0 then return end

+		-- workaround for lag spikes on WoW 8.0
+		if WoW80 and self.frame:GetParent() == UIParent and not fromOnUpdate then
+			self.frame:SetScript("OnUpdate", FirstFrameUpdate)
+			return
+		end
+
 		local first, last

 		scrollToSelection = status.scrollToSelection
@@ -672,12 +680,7 @@ local function Constructor()

 	local scrollbg = scrollbar:CreateTexture(nil, "BACKGROUND")
 	scrollbg:SetAllPoints(scrollbar)
-
-	if IsLegion then
-		scrollbg:SetColorTexture(0,0,0,0.4)
-	else
-		scrollbg:SetTexture(0,0,0,0.4)
-	end
+	scrollbg:SetColorTexture(0,0,0,0.4)

 	local border = CreateFrame("Frame",nil,frame)
 	border:SetPoint("TOPLEFT", treeframe, "TOPRIGHT")
diff --git a/libs/AceGUI-3.0/widgets/AceGUIContainer-Window.lua b/libs/AceGUI-3.0/widgets/AceGUIContainer-Window.lua
index bb0a2a2..6825420 100755
--- a/libs/AceGUI-3.0/widgets/AceGUIContainer-Window.lua
+++ b/libs/AceGUI-3.0/widgets/AceGUIContainer-Window.lua
@@ -21,14 +21,18 @@ local CreateFrame, UIParent = CreateFrame, UIParent
 ]]
 do
 	local Type = "Window"
-	local Version = 4
+	local Version = 6
+
+	local function frameOnShow(this)
+		this.obj:Fire("OnShow")
+	end

 	local function frameOnClose(this)
 		this.obj:Fire("OnClose")
 	end

 	local function closeOnClick(this)
-		PlaySound("gsTitleOptionExit")
+		PlaySound(799) -- SOUNDKIT.GS_TITLE_OPTION_EXIT
 		this.obj:Hide()
 	end

@@ -180,6 +184,7 @@ do
 		frame:SetFrameStrata("FULLSCREEN_DIALOG")
 		frame:SetScript("OnMouseDown", frameOnMouseDown)

+		frame:SetScript("OnShow",frameOnShow)
 		frame:SetScript("OnHide",frameOnClose)
 		frame:SetMinResize(240,240)
 		frame:SetToplevel(true)
diff --git a/libs/AceGUI-3.0/widgets/AceGUIWidget-Button.lua b/libs/AceGUI-3.0/widgets/AceGUIWidget-Button.lua
index c7c72c1..0a23be4 100755
--- a/libs/AceGUI-3.0/widgets/AceGUIWidget-Button.lua
+++ b/libs/AceGUI-3.0/widgets/AceGUIWidget-Button.lua
@@ -2,7 +2,7 @@
 Button Widget
 Graphical Button.
 -------------------------------------------------------------------------------]]
-local Type, Version = "Button", 23
+local Type, Version = "Button", 24
 local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
 if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end

@@ -18,7 +18,7 @@ Scripts
 -------------------------------------------------------------------------------]]
 local function Button_OnClick(frame, ...)
 	AceGUI:ClearFocus()
-	PlaySound("igMainMenuOption")
+	PlaySound(852) -- SOUNDKIT.IG_MAINMENU_OPTION
 	frame.obj:Fire("OnClick", ...)
 end

diff --git a/libs/AceGUI-3.0/widgets/AceGUIWidget-CheckBox.lua b/libs/AceGUI-3.0/widgets/AceGUIWidget-CheckBox.lua
index 8847ebc..b96ac59 100755
--- a/libs/AceGUI-3.0/widgets/AceGUIWidget-CheckBox.lua
+++ b/libs/AceGUI-3.0/widgets/AceGUIWidget-CheckBox.lua
@@ -1,7 +1,7 @@
 --[[-----------------------------------------------------------------------------
 Checkbox Widget
 -------------------------------------------------------------------------------]]
-local Type, Version = "CheckBox", 22
+local Type, Version = "CheckBox", 23
 local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
 if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end

@@ -60,9 +60,9 @@ local function CheckBox_OnMouseUp(frame)
 		self:ToggleChecked()

 		if self.checked then
-			PlaySound("igMainMenuOptionCheckBoxOn")
+			PlaySound(856) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON
 		else -- for both nil and false (tristate)
-			PlaySound("igMainMenuOptionCheckBoxOff")
+			PlaySound(857) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF
 		end

 		self:Fire("OnValueChanged", self.checked)
diff --git a/libs/AceGUI-3.0/widgets/AceGUIWidget-ColorPicker.lua b/libs/AceGUI-3.0/widgets/AceGUIWidget-ColorPicker.lua
index 740a467..05e2b57 100755
--- a/libs/AceGUI-3.0/widgets/AceGUIWidget-ColorPicker.lua
+++ b/libs/AceGUI-3.0/widgets/AceGUIWidget-ColorPicker.lua
@@ -5,8 +5,6 @@ local Type, Version = "ColorPicker", 23
 local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
 if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end

-local IsLegion = select(4, GetBuildInfo()) >= 70000
-
 -- Lua APIs
 local pairs = pairs

@@ -148,11 +146,7 @@ local function Constructor()
 	local texture = frame:CreateTexture(nil, "BACKGROUND")
 	texture:SetWidth(16)
 	texture:SetHeight(16)
-	if IsLegion then
-		texture:SetColorTexture(1, 1, 1)
-	else
-		texture:SetTexture(1, 1, 1)
-	end
+	texture:SetColorTexture(1, 1, 1)
 	texture:SetPoint("CENTER", colorSwatch)
 	texture:Show()

diff --git a/libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown-Items.lua b/libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown-Items.lua
index 5ea840f..5748e4f 100755
--- a/libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown-Items.lua
+++ b/libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown-Items.lua
@@ -1,9 +1,7 @@
---[[ $Id: AceGUIWidget-DropDown-Items.lua 1137 2016-05-15 10:57:36Z nevcairiel $ ]]--
+--[[ $Id: AceGUIWidget-DropDown-Items.lua 1167 2017-08-29 22:08:48Z funkydude $ ]]--

 local AceGUI = LibStub("AceGUI-3.0")

-local IsLegion = select(4, GetBuildInfo()) >= 70000
-
 -- Lua APIs
 local select, assert = select, assert

@@ -325,7 +323,7 @@ end
 -- Does not close the pullout on click.
 do
 	local widgetType = "Dropdown-Item-Toggle"
-	local widgetVersion = 3
+	local widgetVersion = 4

 	local function UpdateToggle(self)
 		if self.value then
@@ -345,9 +343,9 @@ do
 		if self.disabled then return end
 		self.value = not self.value
 		if self.value then
-			PlaySound("igMainMenuOptionCheckBoxOn")
+			PlaySound(856) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON
 		else
-			PlaySound("igMainMenuOptionCheckBoxOff")
+			PlaySound(857) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_OFF
 		end
 		UpdateToggle(self)
 		self:Fire("OnValueChanged", self.value)
@@ -457,11 +455,7 @@ do

 		local line = self.frame:CreateTexture(nil, "OVERLAY")
 		line:SetHeight(1)
-		if IsLegion then
-			line:SetColorTexture(.5, .5, .5)
-		else
-			line:SetTexture(.5, .5, .5)
-		end
+		line:SetColorTexture(.5, .5, .5)
 		line:SetPoint("LEFT", self.frame, "LEFT", 10, 0)
 		line:SetPoint("RIGHT", self.frame, "RIGHT", -10, 0)

diff --git a/libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown.lua b/libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown.lua
index 0dd3bff..cf0b0aa 100755
--- a/libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown.lua
+++ b/libs/AceGUI-3.0/widgets/AceGUIWidget-DropDown.lua
@@ -1,4 +1,4 @@
---[[ $Id: AceGUIWidget-DropDown.lua 1116 2014-10-12 08:15:46Z nevcairiel $ ]]--
+--[[ $Id: AceGUIWidget-DropDown.lua 1167 2017-08-29 22:08:48Z funkydude $ ]]--
 local AceGUI = LibStub("AceGUI-3.0")

 -- Lua APIs
@@ -356,7 +356,7 @@ end

 do
 	local widgetType = "Dropdown"
-	local widgetVersion = 30
+	local widgetVersion = 31

 	--[[ Static data ]]--

@@ -381,7 +381,7 @@ do

 	local function Dropdown_TogglePullout(this)
 		local self = this.obj
-		PlaySound("igMainMenuOptionCheckBoxOn") -- missleading name, but the Blizzard code uses this sound
+		PlaySound(856) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON
 		if self.open then
 			self.open = nil
 			self.pullout:Close()
diff --git a/libs/AceGUI-3.0/widgets/AceGUIWidget-EditBox.lua b/libs/AceGUI-3.0/widgets/AceGUIWidget-EditBox.lua
index d039026..b0b00f9 100755
--- a/libs/AceGUI-3.0/widgets/AceGUIWidget-EditBox.lua
+++ b/libs/AceGUI-3.0/widgets/AceGUIWidget-EditBox.lua
@@ -1,7 +1,7 @@
 --[[-----------------------------------------------------------------------------
 EditBox Widget
 -------------------------------------------------------------------------------]]
-local Type, Version = "EditBox", 26
+local Type, Version = "EditBox", 27
 local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
 if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end

@@ -73,7 +73,7 @@ local function EditBox_OnEnterPressed(frame)
 	local value = frame:GetText()
 	local cancel = self:Fire("OnEnterPressed", value)
 	if not cancel then
-		PlaySound("igMainMenuOptionCheckBoxOn")
+		PlaySound(856) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON
 		HideButton(self)
 	end
 end
diff --git a/libs/AceGUI-3.0/widgets/AceGUIWidget-InteractiveLabel.lua b/libs/AceGUI-3.0/widgets/AceGUIWidget-InteractiveLabel.lua
index 9e06049..036efee 100755
--- a/libs/AceGUI-3.0/widgets/AceGUIWidget-InteractiveLabel.lua
+++ b/libs/AceGUI-3.0/widgets/AceGUIWidget-InteractiveLabel.lua
@@ -1,7 +1,7 @@
 --[[-----------------------------------------------------------------------------
 InteractiveLabel Widget
 -------------------------------------------------------------------------------]]
-local Type, Version = "InteractiveLabel", 20
+local Type, Version = "InteractiveLabel", 21
 local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
 if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end

diff --git a/libs/AceGUI-3.0/widgets/AceGUIWidget-Label.lua b/libs/AceGUI-3.0/widgets/AceGUIWidget-Label.lua
index 23897d5..75817a0 100755
--- a/libs/AceGUI-3.0/widgets/AceGUIWidget-Label.lua
+++ b/libs/AceGUI-3.0/widgets/AceGUIWidget-Label.lua
@@ -2,7 +2,7 @@
 Label Widget
 Displays text and optionally an icon.
 -------------------------------------------------------------------------------]]
-local Type, Version = "Label", 23
+local Type, Version = "Label", 24
 local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
 if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end

@@ -78,6 +78,8 @@ local methods = {
 		self:SetImageSize(16, 16)
 		self:SetColor()
 		self:SetFontObject()
+		self:SetJustifyH("LEFT")
+		self:SetJustifyV("TOP")

 		-- reset the flag
 		self.resizing = nil
@@ -134,6 +136,14 @@ local methods = {
 		self.image:SetHeight(height)
 		UpdateImageAnchor(self)
 	end,
+
+	["SetJustifyH"] = function(self, justifyH)
+		self.label:SetJustifyH(justifyH)
+	end,
+
+	["SetJustifyV"] = function(self, justifyV)
+		self.label:SetJustifyV(justifyV)
+	end,
 }

 --[[-----------------------------------------------------------------------------
@@ -144,9 +154,6 @@ local function Constructor()
 	frame:Hide()

 	local label = frame:CreateFontString(nil, "BACKGROUND", "GameFontHighlightSmall")
-	label:SetJustifyH("LEFT")
-	label:SetJustifyV("TOP")
-
 	local image = frame:CreateTexture(nil, "BACKGROUND")

 	-- create widget
diff --git a/libs/AceGUI-3.0/widgets/AceGUIWidget-Slider.lua b/libs/AceGUI-3.0/widgets/AceGUIWidget-Slider.lua
index 583f29d..20d0887 100755
--- a/libs/AceGUI-3.0/widgets/AceGUIWidget-Slider.lua
+++ b/libs/AceGUI-3.0/widgets/AceGUIWidget-Slider.lua
@@ -2,7 +2,7 @@
 Slider Widget
 Graphical Slider, like, for Range values.
 -------------------------------------------------------------------------------]]
-local Type, Version = "Slider", 21
+local Type, Version = "Slider", 22
 local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
 if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end

@@ -108,7 +108,7 @@ local function EditBox_OnEnterPressed(frame)
 	end

 	if value then
-		PlaySound("igMainMenuOptionCheckBoxOn")
+		PlaySound(856) -- SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON
 		self.slider:SetValue(value)
 		self:Fire("OnMouseUp", value)
 	end
diff --git a/libs/CallbackHandler-1.0/CallbackHandler-1.0.lua b/libs/CallbackHandler-1.0/CallbackHandler-1.0.lua
index a127301..675d7b0 100755
--- a/libs/CallbackHandler-1.0/CallbackHandler-1.0.lua
+++ b/libs/CallbackHandler-1.0/CallbackHandler-1.0.lua
@@ -1,4 +1,4 @@
---[[ $Id: CallbackHandler-1.0.lua 965 2010-08-09 00:47:52Z mikk $ ]]
+--[[ $Id: CallbackHandler-1.0.lua 1131 2015-06-04 07:29:24Z nevcairiel $ ]]
 local MAJOR, MINOR = "CallbackHandler-1.0", 6
 local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR)

@@ -65,9 +65,7 @@ end})
 --   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, OnUsed, OnUnused)
-	-- TODO: Remove this after beta has gone out
-	assert(not OnUsed and not OnUnused, "ACE-80: OnUsed/OnUnused are deprecated. Callbacks are now done to registry.OnUsed and registry.OnUnused")
+function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName)

 	RegisterName = RegisterName or "RegisterCallback"
 	UnregisterName = UnregisterName or "UnregisterCallback"
diff --git a/libs/CallbackHandler-1.0/CallbackHandler-1.0.xml b/libs/CallbackHandler-1.0/CallbackHandler-1.0.xml
index 876df83..c107f88 100755
--- a/libs/CallbackHandler-1.0/CallbackHandler-1.0.xml
+++ b/libs/CallbackHandler-1.0/CallbackHandler-1.0.xml
@@ -1,4 +1,4 @@
 <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>
\ No newline at end of file
+</Ui>
diff --git a/libs/HereBeDragons/CHANGES.txt b/libs/HereBeDragons/CHANGES.txt
index 1be2f23..3ffe873 100755
--- a/libs/HereBeDragons/CHANGES.txt
+++ b/libs/HereBeDragons/CHANGES.txt
@@ -1,8 +1,8 @@
 Changes since tag 1.91-beta

-commit af699f6637a2d07ba3000273534116aefaffc679
-Author: Hendrik Leppkes <h.leppkes@gmail.com>
-Date:   Sat May 26 19:15:44 2018 +0200
-
-    Optimize transform storage for faster lookups
-
+commit af699f6637a2d07ba3000273534116aefaffc679
+Author: Hendrik Leppkes <h.leppkes@gmail.com>
+Date:   Sat May 26 19:15:44 2018 +0200
+
+    Optimize transform storage for faster lookups
+
diff --git a/libs/HereBeDragons/HereBeDragons-1.0.lua b/libs/HereBeDragons/HereBeDragons-1.0.lua
index 97e54ff..78faf34 100755
--- a/libs/HereBeDragons/HereBeDragons-1.0.lua
+++ b/libs/HereBeDragons/HereBeDragons-1.0.lua
@@ -1,776 +1,776 @@
--- 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
+-- 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
diff --git a/libs/HereBeDragons/HereBeDragons-2.0.lua b/libs/HereBeDragons/HereBeDragons-2.0.lua
index bee78ea..b9e333e 100755
--- a/libs/HereBeDragons/HereBeDragons-2.0.lua
+++ b/libs/HereBeDragons/HereBeDragons-2.0.lua
@@ -1,496 +1,496 @@
--- 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", 4
-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, 1, -6933.33, 533.33, -16000, -8000, 10133.3, 17600 },
-        { 530, 0, 4800, 16000, -10133.3, -2666.67, -2400, 2400 },
-        { 732, 0, -20000, 20000, -20000, 20000, -1600, 2800 },
-        { 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}
-        else
-            mapData[id] = {0, 0, 0, 0, instance = instance or -1, name = data.name, mapType = data.mapType}
-        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
+-- 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", 4
+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, 1, -6933.33, 533.33, -16000, -8000, 10133.3, 17600 },
+        { 530, 0, 4800, 16000, -10133.3, -2666.67, -2400, 2400 },
+        { 732, 0, -20000, 20000, -20000, 20000, -1600, 2800 },
+        { 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}
+        else
+            mapData[id] = {0, 0, 0, 0, instance = instance or -1, name = data.name, mapType = data.mapType}
+        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
diff --git a/libs/HereBeDragons/HereBeDragons-Migrate.lua b/libs/HereBeDragons/HereBeDragons-Migrate.lua
index e56d9e9..b6efc1b 100755
--- a/libs/HereBeDragons/HereBeDragons-Migrate.lua
+++ b/libs/HereBeDragons/HereBeDragons-Migrate.lua
@@ -1,529 +1,529 @@
--- 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-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
diff --git a/libs/HereBeDragons/HereBeDragons-Pins-1.0.lua b/libs/HereBeDragons/HereBeDragons-Pins-1.0.lua
index d92c2b5..d0a79c5 100755
--- a/libs/HereBeDragons/HereBeDragons-Pins-1.0.lua
+++ b/libs/HereBeDragons/HereBeDragons-Pins-1.0.lua
@@ -1,651 +1,651 @@
--- 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
+-- 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
diff --git a/libs/HereBeDragons/HereBeDragons-Pins-2.0.lua b/libs/HereBeDragons/HereBeDragons-Pins-2.0.lua
index 381dfc7..7aee844 100755
--- a/libs/HereBeDragons/HereBeDragons-Pins-2.0.lua
+++ b/libs/HereBeDragons/HereBeDragons-Pins-2.0.lua
@@ -1,716 +1,716 @@
--- 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", 1
-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
-
--- 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 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
-
--------------------------------------------------------------------------------------------
--- 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
-
--- 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 == WORLDMAP_AZEROTH_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 info = C_Map.GetMapInfo(data.uiMapID)
-                while info and info.parentMapID do
-                    if info.parentMapID == uiMapID then
-                        local mapType = HBD.mapData[info.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
-                        info = C_Map.GetMapInfo(info.parentMapID)
-                    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")
-end
-
-function worldmapProviderPin:OnAcquired(icon, x, y)
-    self:SetPosition(x, y)
-
-    self.icon = icon
-    icon:SetParent(self)
-    icon:ClearAllPoints()
-    icon:SetPoint("CENTER", self, "CENTER")
-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
+-- 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", 1
+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
+
+-- 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 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
+
+-------------------------------------------------------------------------------------------
+-- 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
+
+-- 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 == WORLDMAP_AZEROTH_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 info = C_Map.GetMapInfo(data.uiMapID)
+                while info and info.parentMapID do
+                    if info.parentMapID == uiMapID then
+                        local mapType = HBD.mapData[info.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
+                        info = C_Map.GetMapInfo(info.parentMapID)
+                    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")
+end
+
+function worldmapProviderPin:OnAcquired(icon, x, y)
+    self:SetPosition(x, y)
+
+    self.icon = icon
+    icon:SetParent(self)
+    icon:ClearAllPoints()
+    icon:SetPoint("CENTER", self, "CENTER")
+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