diff --git a/libs/HereBeDragons-1.0/HereBeDragons-1.0.lua b/libs/HereBeDragons-1.0/HereBeDragons-1.0.lua deleted file mode 100755 index 6db3fca..0000000 --- a/libs/HereBeDragons-1.0/HereBeDragons-1.0.lua +++ /dev/null @@ -1,771 +0,0 @@ --- HereBeDragons is a data API for the World of Warcraft mapping system - -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 = 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[0] == 0 or data[1] == 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[0] == 0 or data[1] == 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-1.0/HereBeDragons-Pins-1.0.lua b/libs/HereBeDragons-1.0/HereBeDragons-Pins-1.0.lua deleted file mode 100755 index 58b706c..0000000 --- a/libs/HereBeDragons-1.0/HereBeDragons-Pins-1.0.lua +++ /dev/null @@ -1,645 +0,0 @@ --- HereBeDragons-Pins is a library to show pins/icons on the world map and minimap - -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/CHANGES.txt b/libs/HereBeDragons/CHANGES.txt new file mode 100755 index 0000000..1be2f23 --- /dev/null +++ b/libs/HereBeDragons/CHANGES.txt @@ -0,0 +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 + diff --git a/libs/HereBeDragons/HereBeDragons-1.0.lua b/libs/HereBeDragons/HereBeDragons-1.0.lua new file mode 100755 index 0000000..97e54ff --- /dev/null +++ b/libs/HereBeDragons/HereBeDragons-1.0.lua @@ -0,0 +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 diff --git a/libs/HereBeDragons/HereBeDragons-2.0.lua b/libs/HereBeDragons/HereBeDragons-2.0.lua new file mode 100755 index 0000000..bee78ea --- /dev/null +++ b/libs/HereBeDragons/HereBeDragons-2.0.lua @@ -0,0 +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 diff --git a/libs/HereBeDragons/HereBeDragons-Migrate.lua b/libs/HereBeDragons/HereBeDragons-Migrate.lua new file mode 100755 index 0000000..e56d9e9 --- /dev/null +++ b/libs/HereBeDragons/HereBeDragons-Migrate.lua @@ -0,0 +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 diff --git a/libs/HereBeDragons/HereBeDragons-Pins-1.0.lua b/libs/HereBeDragons/HereBeDragons-Pins-1.0.lua new file mode 100755 index 0000000..d92c2b5 --- /dev/null +++ b/libs/HereBeDragons/HereBeDragons-Pins-1.0.lua @@ -0,0 +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 diff --git a/libs/HereBeDragons/HereBeDragons-Pins-2.0.lua b/libs/HereBeDragons/HereBeDragons-Pins-2.0.lua new file mode 100755 index 0000000..381dfc7 --- /dev/null +++ b/libs/HereBeDragons/HereBeDragons-Pins-2.0.lua @@ -0,0 +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