1
0
Bifurcation 0
Ce dépôt a été archivé le 2020-03-15. Vous pouvez voir ses fichiers ou le cloner, mais pas ouvrir de ticket ou de demandes d'ajout, ni soumettre de changements.
questhelperredux/QuestHelper/libs/Astrolabe/Astrolabe.lua

1462 lignes
47 KiB
Lua

--[[
Name: Astrolabe
Revision: $Rev: 147 $
$Date: 2012-08-28 09:45:40 -0700 (Tue, 28 Aug 2012) $
Author(s): Esamynn (esamynn at wowinterface.com)
Inspired By: Gatherer by Norganna
MapLibrary by Kristofer Karlsson (krka at kth.se)
Documentation: http://wiki.esamynn.org/Astrolabe
SVN: http://svn.esamynn.org/astrolabe/
Description:
This is a library for the World of Warcraft UI system to place
icons accurately on both the Minimap and on Worldmaps.
This library also manages and updates the position of Minimap icons
automatically.
Copyright (C) 2006-2012 James Carrothers
License:
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Note:
This library's source code is specifically designed to work with
World of Warcraft's interpreted AddOn system. You have an implicit
licence to use this library with these facilities since that is its
designated purpose as per:
http://www.fsf.org/licensing/licenses/gpl-faq.html#InterpreterIncompat
]]
-- WARNING!!!
-- DO NOT MAKE CHANGES TO THIS LIBRARY WITHOUT FIRST CHANGING THE LIBRARY_VERSION_MAJOR
-- STRING (to something unique) OR ELSE YOU MAY BREAK OTHER ADDONS THAT USE THIS LIBRARY!!!
local LIBRARY_VERSION_MAJOR = "Astrolabe-1.0"
local LIBRARY_VERSION_MINOR = tonumber(string.match("$Revision: 147 $", "(%d+)") or 1)
if not DongleStub then error(LIBRARY_VERSION_MAJOR .. " requires DongleStub.") end
if not DongleStub:IsNewerVersion(LIBRARY_VERSION_MAJOR, LIBRARY_VERSION_MINOR) then return end
local Astrolabe = {};
-- define local variables for Data Tables (defined at the end of this file)
local WorldMapSize, MicroDungeonSize, MinimapSize, ValidMinimapShapes, zeroData;
function Astrolabe:GetVersion()
return LIBRARY_VERSION_MAJOR, LIBRARY_VERSION_MINOR;
end
--------------------------------------------------------------------------------------------------------------
-- Config Constants
--------------------------------------------------------------------------------------------------------------
local configConstants = {
MinimapUpdateMultiplier = true,
}
-- this constant is multiplied by the current framerate to determine
-- how many icons are updated each frame
Astrolabe.MinimapUpdateMultiplier = 1;
--------------------------------------------------------------------------------------------------------------
-- Working Tables
--------------------------------------------------------------------------------------------------------------
Astrolabe.LastPlayerPosition = { 0, 0, 0, 0 };
Astrolabe.MinimapIcons = {};
Astrolabe.IconsOnEdge = {};
Astrolabe.IconsOnEdge_GroupChangeCallbacks = {};
Astrolabe.MinimapIconCount = 0
Astrolabe.ForceNextUpdate = false;
Astrolabe.IconsOnEdgeChanged = false;
-- This variable indicates whether we know of a visible World Map or not.
-- The state of this variable is controlled by the AstrolabeMapMonitor library.
Astrolabe.WorldMapVisible = false;
local AddedOrUpdatedIcons = {}
local MinimapIconsMetatable = { __index = AddedOrUpdatedIcons }
--------------------------------------------------------------------------------------------------------------
-- Local Pointers for often used API functions
--------------------------------------------------------------------------------------------------------------
local twoPi = math.pi * 2;
local atan2 = math.atan2;
local sin = math.sin;
local cos = math.cos;
local abs = math.abs;
local sqrt = math.sqrt;
local min = math.min
local max = math.max
local yield = coroutine.yield
local next = next
local GetFramerate = GetFramerate
local real_GetCurrentMapAreaID = GetCurrentMapAreaID
local function GetCurrentMapAreaID()
local id = real_GetCurrentMapAreaID();
if ( id < 0 and GetCurrentMapContinent() == WORLDMAP_WORLD_ID ) then
return 0;
end
return id;
end
--------------------------------------------------------------------------------------------------------------
-- Internal Utility Functions
--------------------------------------------------------------------------------------------------------------
local function assert(level,condition,message)
if not condition then
error(message,level)
end
end
local function argcheck(value, num, ...)
assert(1, type(num) == "number", "Bad argument #2 to 'argcheck' (number expected, got " .. type(level) .. ")")
for i=1,select("#", ...) do
if type(value) == select(i, ...) then return end
end
local types = strjoin(", ", ...)
local name = string.match(debugstack(2,2,0), ": in function [`<](.-)['>]")
error(string.format("Bad argument #%d to 'Astrolabe.%s' (%s expected, got %s)", num, name, types, type(value)), 3)
end
local function getSystemPosition( mapData, f, x, y )
if ( f ~= 0 ) then
mapData = rawget(mapData, f) or MicroDungeonSize[mapData.system][f];
end
x = x * mapData.width + mapData.xOffset;
y = y * mapData.height + mapData.yOffset;
return x, y;
end
local function printError( ... )
if ( ASTROLABE_VERBOSE) then
print(...)
end
end
--------------------------------------------------------------------------------------------------------------
-- General Utility Functions
--------------------------------------------------------------------------------------------------------------
function Astrolabe:ComputeDistance( m1, f1, x1, y1, m2, f2, x2, y2 )
--[[
argcheck(m1, 2, "number");
assert(3, m1 >= 0, "ComputeDistance: Illegal map id to m1: "..m1);
argcheck(f1, 3, "number", "nil");
argcheck(x1, 4, "number");
argcheck(y1, 5, "number");
argcheck(m2, 6, "number");
assert(3, m2 >= 0, "ComputeDistance: Illegal map id to m2: "..m2);
argcheck(f2, 7, "number", "nil");
argcheck(x2, 8, "number");
argcheck(y2, 9, "number");
--]]
if not ( m1 and m2 ) then return end;
f1 = f1 or min(#WorldMapSize[m1], 1);
f2 = f2 or min(#WorldMapSize[m2], 1);
local dist, xDelta, yDelta;
if ( m1 == m2 and f1 == f2 ) then
-- points in the same zone on the same floor
local mapData = WorldMapSize[m1];
if ( f1 ~= 0 ) then
mapData = rawget(mapData, f1) or MicroDungeonSize[mapData.system][f1];
end
xDelta = (x2 - x1) * mapData.width;
yDelta = (y2 - y1) * mapData.height;
else
local map1 = WorldMapSize[m1];
local map2 = WorldMapSize[m2];
if ( map1.system == map2.system ) then
-- points within the same system (continent)
x1, y1 = getSystemPosition(map1, f1, x1, y1);
x2, y2 = getSystemPosition(map2, f2, x2, y2);
xDelta = (x2 - x1);
yDelta = (y2 - y1);
else
local s1 = map1.system;
local s2 = map2.system;
if ( (m1==0 or WorldMapSize[0][s1]) and (m2==0 or WorldMapSize[0][s2]) ) then
x1, y1 = getSystemPosition(map1, f1, x1, y1);
x2, y2 = getSystemPosition(map2, f2, x2, y2);
if ( m1 ~= 0 ) then
-- translate up from system 1
local cont1 = WorldMapSize[0][s1];
x1 = (x1 - cont1.xOffset) * cont1.scale;
y1 = (y1 - cont1.yOffset) * cont1.scale;
end
if ( m2 ~= 0 ) then
-- translate up from system 2
local cont2 = WorldMapSize[0][s2];
x2 = (x2 - cont2.xOffset) * cont2.scale;
y2 = (y2 - cont2.yOffset) * cont2.scale;
end
xDelta = x2 - x1;
yDelta = y2 - y1;
end
end
end
if ( xDelta and yDelta ) then
dist = sqrt(xDelta*xDelta + yDelta*yDelta);
end
return dist, xDelta, yDelta;
end
function Astrolabe:TranslateWorldMapPosition( M, F, xPos, yPos, nM, nF )
--[[
argcheck(M, 2, "number");
argcheck(F, 3, "number", "nil");
argcheck(xPos, 4, "number");
argcheck(yPos, 5, "number");
argcheck(nM, 6, "number");
argcheck(nF, 7, "number", "nil");
--]]
if not ( M and nM ) then return end;
F = F or min(#WorldMapSize[M], 1);
nF = nF or min(#WorldMapSize[nM], 1);
if ( nM < 0 ) then
return;
end
local mapData;
if ( M == nM and F == nF ) then
return xPos, yPos;
else
local map = WorldMapSize[M];
local nMap = WorldMapSize[nM];
if ( map.system == nMap.system ) then
-- points within the same system (continent)
xPos, yPos = getSystemPosition(map, F, xPos, yPos);
mapData = WorldMapSize[nM];
if ( nF ~= 0 ) then
mapData = rawget(mapData, nF) or MicroDungeonSize[mapData.system][nF];
end
else
-- different continents, same world
local S = map.system;
local nS = nMap.system;
if ( (M==0 or WorldMapSize[0][S]) and (nM==0 or WorldMapSize[0][nS]) ) then
mapData = WorldMapSize[M];
xPos, yPos = getSystemPosition(mapData, F, xPos, yPos);
if ( M ~= 0 ) then
-- translate up to world map if we aren't there already
local cont = WorldMapSize[0][S];
xPos = (xPos - cont.xOffset) * cont.scale;
yPos = (yPos - cont.yOffset) * cont.scale;
mapData = WorldMapSize[0];
end
if ( nM ~= 0 ) then
-- translate down to the new continent
local nCont = WorldMapSize[0][nS];
xPos = (xPos / nCont.scale) + nCont.xOffset;
yPos = (yPos / nCont.scale) + nCont.yOffset;
mapData = WorldMapSize[nM];
if ( nF ~= 0 ) then
mapData = rawget(mapData, nF) or MicroDungeonSize[mapData.system][nF];
end
end
else
return;
end
end
-- need to account for the offset in the new system so we can
-- correctly translate into 0-1 style coordinates
xPos = xPos - mapData.xOffset;
yPos = yPos - mapData.yOffset;
end
return (xPos / mapData.width), (yPos / mapData.height);
end
--*****************************************************************************
-- This function will do its utmost to retrieve some sort of valid position
-- for the specified unit, including changing the current map zoom (if needed).
-- Map Zoom is returned to its previous setting before this function returns.
--*****************************************************************************
function Astrolabe:GetUnitPosition( unit, noMapChange )
local x, y = GetPlayerMapPosition(unit);
if ( x <= 0 and y <= 0 ) then
if ( noMapChange ) then
-- no valid position on the current map, and we aren't allowed
-- to change map zoom, so return
return;
end
local lastMapID, lastFloor = GetCurrentMapAreaID(), GetCurrentMapDungeonLevel();
SetMapToCurrentZone();
x, y = GetPlayerMapPosition(unit);
if ( x <= 0 and y <= 0 ) then
WorldMapZoomOutButton_OnClick();
x, y = GetPlayerMapPosition(unit);
if ( x <= 0 and y <= 0 ) then
-- we are in an instance without a map or otherwise off map
return;
end
end
local M, F = GetCurrentMapAreaID(), GetCurrentMapDungeonLevel();
if ( M ~= lastMapID or F ~= lastFloor ) then
-- set map zoom back to what it was before
SetMapByID(lastMapID);
SetDungeonMapLevel(lastFloor);
end
return M, F, x, y;
end
return GetCurrentMapAreaID(), GetCurrentMapDungeonLevel(), x, y;
end
--*****************************************************************************
-- This function will do its utmost to retrieve some sort of valid position
-- for the specified unit, including changing the current map zoom (if needed).
-- However, if a monitored WorldMapFrame (See AstrolabeMapMonitor.lua) is
-- visible, then will simply return nil if the current zoom does not provide
-- a valid position for the player unit. Map Zoom is NOT returned to its previous
-- setting before this function returns, in order to provide better performance.
--*****************************************************************************
function Astrolabe:GetCurrentPlayerPosition()
local x, y = GetPlayerMapPosition("player");
if ( x <= 0 and y <= 0 ) then
if ( self.WorldMapVisible ) then
-- we know there is a visible world map, so don't cause
-- WORLD_MAP_UPDATE events by changing map zoom
return;
end
SetMapToCurrentZone();
x, y = GetPlayerMapPosition("player");
if ( x <= 0 and y <= 0 ) then
WorldMapZoomOutButton_OnClick();
x, y = GetPlayerMapPosition("player");
if ( x <= 0 and y <= 0 ) then
-- we are in an instance without a map or otherwise off map
return;
end
end
end
return GetCurrentMapAreaID(), GetCurrentMapDungeonLevel(), x, y;
end
function Astrolabe:GetMapID(continent, zone)
zone = zone or 0;
local ret = self.ContinentList[continent];
if ( ret ) then
return ret[zone];
end
if ( continent == 0 and zone == 0 ) then
return 0;
end
end
function Astrolabe:GetNumFloors( mapID )
if ( type(mapID) == "number" ) then
local mapData = WorldMapSize[mapID]
return #mapData
end
end
function Astrolabe:GetMapInfo( mapID, mapFloor )
argcheck(mapID, 2, "number");
assert(3, mapID >= 0, "GetMapInfo: Illegal map id to mapID: "..mapID);
argcheck(mapFloor, 3, "number", "nil");
mapFloor = mapFloor or min(#WorldMapSize[mapID], 1);
local mapData = WorldMapSize[mapID];
local system, systemParent = mapData.system, WorldMapSize[0][mapData.system] and true or false
if ( mapFloor ~= 0 ) then
mapData = rawget(mapData, mapFloor) or MicroDungeonSize[mapData.system][mapFloor];
end
if ( mapData ~= zeroData ) then
return system, systemParent, mapData.width, mapData.height, mapData.xOffset, mapData.yOffset;
end
end
--------------------------------------------------------------------------------------------------------------
-- Working Table Cache System
--------------------------------------------------------------------------------------------------------------
local tableCache = {};
tableCache["__mode"] = "v";
setmetatable(tableCache, tableCache);
local function GetWorkingTable( icon )
if ( tableCache[icon] ) then
return tableCache[icon];
else
local T = {};
tableCache[icon] = T;
return T;
end
end
--------------------------------------------------------------------------------------------------------------
-- Minimap Icon Placement
--------------------------------------------------------------------------------------------------------------
--*****************************************************************************
-- local variables specifically for use in this section
--*****************************************************************************
local minimapRotationEnabled = false;
local minimapShape = false;
local minimapRotationOffset = GetPlayerFacing();
local function placeIconOnMinimap( minimap, minimapZoom, mapWidth, mapHeight, icon, dist, xDist, yDist )
local mapDiameter;
if ( Astrolabe.minimapOutside ) then
mapDiameter = MinimapSize.outdoor[minimapZoom];
else
mapDiameter = MinimapSize.indoor[minimapZoom];
end
local mapRadius = mapDiameter / 2;
local xScale = mapDiameter / mapWidth;
local yScale = mapDiameter / mapHeight;
local iconDiameter = ((icon:GetWidth() / 2) + 3) * xScale;
local iconOnEdge = nil;
local isRound = true;
if ( minimapRotationEnabled ) then
local sinTheta = sin(minimapRotationOffset)
local cosTheta = cos(minimapRotationOffset)
--[[
Math Note
The math that is acutally going on in the next 3 lines is:
local dx, dy = xDist, -yDist
xDist = (dx * cosTheta) + (dy * sinTheta)
yDist = -((-dx * sinTheta) + (dy * cosTheta))
This is because the origin for map coordinates is the top left corner
of the map, not the bottom left, and so we have to reverse the vertical
distance when doing the our rotation, and then reverse the result vertical
distance because this rotation formula gives us a result with the origin based
in the bottom left corner (of the (+, +) quadrant).
The actual code is a simplification of the above.
]]
local dx, dy = xDist, yDist
xDist = (dx * cosTheta) - (dy * sinTheta)
yDist = (dx * sinTheta) + (dy * cosTheta)
end
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
-- for non-circular portions of the Minimap edge
if not ( isRound ) then
dist = max(abs(xDist), abs(yDist))
end
if ( (dist + iconDiameter) > mapRadius ) then
-- position along the outside of the Minimap
iconOnEdge = true;
local factor = (mapRadius - iconDiameter) / dist;
xDist = xDist * factor;
yDist = yDist * factor;
end
if ( Astrolabe.IconsOnEdge[icon] ~= iconOnEdge ) then
Astrolabe.IconsOnEdge[icon] = iconOnEdge;
Astrolabe.IconsOnEdgeChanged = true;
end
icon:ClearAllPoints();
icon:SetPoint("CENTER", minimap, "CENTER", xDist/xScale, -yDist/yScale);
end
function Astrolabe:PlaceIconOnMinimap( icon, mapID, mapFloor, xPos, yPos )
-- check argument types
argcheck(icon, 2, "table");
assert(3, icon.SetPoint and icon.ClearAllPoints, "Usage Message");
argcheck(mapID, 3, "number");
argcheck(mapFloor, 4, "number", "nil");
argcheck(xPos, 5, "number");
argcheck(yPos, 6, "number");
-- if the positining system is currently active, just use the player position used by the last incremental (or full) update
-- otherwise, make sure we base our calculations off of the most recent player position (if one is available)
local lM, lF, lx, ly;
if ( self.processingFrame:IsShown() ) then
lM, lF, lx, ly = unpack(self.LastPlayerPosition);
else
lM, lF, lx, ly = self:GetCurrentPlayerPosition();
if ( lM and lM >= 0 ) then
local lastPosition = self.LastPlayerPosition;
lastPosition[1] = lM;
lastPosition[2] = lF;
lastPosition[3] = lx;
lastPosition[4] = ly;
else
lM, lF, lx, ly = unpack(self.LastPlayerPosition);
end
end
local dist, xDist, yDist = self:ComputeDistance(lM, lF, lx, ly, mapID, mapFloor, xPos, yPos);
if not ( dist ) then
--icon's position has no meaningful position relative to the player's current location
return -1;
end
local iconData = GetWorkingTable(icon);
if ( self.MinimapIcons[icon] ) then
self.MinimapIcons[icon] = nil;
else
self.MinimapIconCount = self.MinimapIconCount + 1
end
AddedOrUpdatedIcons[icon] = iconData
iconData.mapID = mapID;
iconData.mapFloor = mapFloor;
iconData.xPos = xPos;
iconData.yPos = yPos;
iconData.dist = dist;
iconData.xDist = xDist;
iconData.yDist = yDist;
minimapRotationEnabled = GetCVar("rotateMinimap") ~= "0"
if ( minimapRotationEnabled ) then
minimapRotationOffset = GetPlayerFacing();
end
-- check Minimap Shape
minimapShape = GetMinimapShape and ValidMinimapShapes[GetMinimapShape()];
-- place the icon on the Minimap and :Show() it
local map = Minimap
placeIconOnMinimap(map, map:GetZoom(), map:GetWidth(), map:GetHeight(), icon, dist, xDist, yDist);
icon:Show()
-- We know this icon's position is valid, so we need to make sure the icon placement system is active.
self.processingFrame:Show()
return 0;
end
function Astrolabe:RemoveIconFromMinimap( icon )
if not ( self.MinimapIcons[icon] ) then
return 1;
end
AddedOrUpdatedIcons[icon] = nil
self.MinimapIcons[icon] = nil;
self.IconsOnEdge[icon] = nil;
icon:Hide();
local MinimapIconCount = self.MinimapIconCount - 1
if ( MinimapIconCount <= 0 ) then
-- no icons left to manage
self.processingFrame:Hide()
MinimapIconCount = 0 -- because I'm paranoid
end
self.MinimapIconCount = MinimapIconCount
return 0;
end
function Astrolabe:RemoveAllMinimapIcons()
self:DumpNewIconsCache()
local MinimapIcons = self.MinimapIcons;
local IconsOnEdge = self.IconsOnEdge;
for k, v in pairs(MinimapIcons) do
MinimapIcons[k] = nil;
IconsOnEdge[k] = nil;
k:Hide();
end
self.MinimapIconCount = 0
self.processingFrame:Hide()
end
local lastZoom; -- to remember the last seen Minimap zoom level
-- local variables to track the status of the two update coroutines
local fullUpdateInProgress = true
local resetIncrementalUpdate = false
local resetFullUpdate = false
-- Incremental Update Code
do
-- local variables to track the incremental update coroutine
local incrementalUpdateCrashed = true
local incrementalUpdateThread
local function UpdateMinimapIconPositions( self )
yield()
while ( true ) do
self:DumpNewIconsCache() -- put new/updated icons into the main datacache
resetIncrementalUpdate = false -- by definition, the incremental update is reset if it is here
local M, F, x, y = self:GetCurrentPlayerPosition();
if ( M and M >= 0 ) then
local Minimap = Minimap;
local lastPosition = self.LastPlayerPosition;
local lM, lF, lx, ly = unpack(lastPosition);
minimapRotationEnabled = GetCVar("rotateMinimap") ~= "0"
if ( minimapRotationEnabled ) then
minimapRotationOffset = GetPlayerFacing();
end
-- check current frame rate
local numPerCycle = min(50, GetFramerate() * (self.MinimapUpdateMultiplier or 1))
-- check Minimap Shape
minimapShape = GetMinimapShape and ValidMinimapShapes[GetMinimapShape()];
if ( lM == M and lF == F and lx == x and ly == y ) then
-- player has not moved since the last update
if ( lastZoom ~= Minimap:GetZoom() or self.ForceNextUpdate or minimapRotationEnabled ) then
local currentZoom = Minimap:GetZoom();
lastZoom = currentZoom;
local mapWidth = Minimap:GetWidth();
local mapHeight = Minimap:GetHeight();
numPerCycle = numPerCycle * 2
local count = 0
for icon, data in pairs(self.MinimapIcons) do
placeIconOnMinimap(Minimap, currentZoom, mapWidth, mapHeight, icon, data.dist, data.xDist, data.yDist);
count = count + 1
if ( count > numPerCycle ) then
count = 0
yield()
-- check if the incremental update cycle needs to be reset
-- because a full update has been run
if ( resetIncrementalUpdate ) then
break;
end
end
end
self.ForceNextUpdate = false;
end
else
local dist, xDelta, yDelta = self:ComputeDistance(lM, lF, lx, ly, M, F, x, y);
if ( dist ) then
local currentZoom = Minimap:GetZoom();
lastZoom = currentZoom;
local mapWidth = Minimap:GetWidth();
local mapHeight = Minimap:GetHeight();
local count = 0
for icon, data in pairs(self.MinimapIcons) do
local xDist = data.xDist - xDelta;
local yDist = data.yDist - yDelta;
local dist = sqrt(xDist*xDist + yDist*yDist);
placeIconOnMinimap(Minimap, currentZoom, mapWidth, mapHeight, icon, dist, xDist, yDist);
data.dist = dist;
data.xDist = xDist;
data.yDist = yDist;
count = count + 1
if ( count >= numPerCycle ) then
count = 0
yield()
-- check if the incremental update cycle needs to be reset
-- because a full update has been run
if ( resetIncrementalUpdate ) then
break;
end
end
end
if not ( resetIncrementalUpdate ) then
lastPosition[1] = M;
lastPosition[2] = F;
lastPosition[3] = x;
lastPosition[4] = y;
end
else
self:RemoveAllMinimapIcons()
lastPosition[1] = M;
lastPosition[2] = F;
lastPosition[3] = x;
lastPosition[4] = y;
end
end
else
if not ( self.WorldMapVisible ) then
self.processingFrame:Hide();
end
end
-- if we've been reset, then we want to start the new cycle immediately
if not ( resetIncrementalUpdate ) then
yield()
end
end
end
function Astrolabe:UpdateMinimapIconPositions()
if ( fullUpdateInProgress ) then
-- if we're in the middle a a full update, we want to finish that first
self:CalculateMinimapIconPositions()
else
if ( incrementalUpdateCrashed ) then
incrementalUpdateThread = coroutine.wrap(UpdateMinimapIconPositions)
incrementalUpdateThread(self) --initialize the thread
end
incrementalUpdateCrashed = true
incrementalUpdateThread()
incrementalUpdateCrashed = false
end
end
end
-- Full Update Code
do
-- local variables to track the full update coroutine
local fullUpdateCrashed = true
local fullUpdateThread
local function CalculateMinimapIconPositions( self )
yield()
while ( true ) do
self:DumpNewIconsCache() -- put new/updated icons into the main datacache
resetFullUpdate = false -- by definition, the full update is reset if it is here
fullUpdateInProgress = true -- set the flag the says a full update is in progress
local M, F, x, y = self:GetCurrentPlayerPosition();
if ( M and M >= 0 ) then
minimapRotationEnabled = GetCVar("rotateMinimap") ~= "0"
if ( minimapRotationEnabled ) then
minimapRotationOffset = GetPlayerFacing();
end
-- check current frame rate
local numPerCycle = GetFramerate() * (self.MinimapUpdateMultiplier or 1) * 2
-- check Minimap Shape
minimapShape = GetMinimapShape and ValidMinimapShapes[GetMinimapShape()];
local currentZoom = Minimap:GetZoom();
lastZoom = currentZoom;
local Minimap = Minimap;
local mapWidth = Minimap:GetWidth();
local mapHeight = Minimap:GetHeight();
local count = 0
for icon, data in pairs(self.MinimapIcons) do
local dist, xDist, yDist = self:ComputeDistance(M, F, x, y, data.mapID, data.mapFloor, data.xPos, data.yPos);
if ( dist ) then
placeIconOnMinimap(Minimap, currentZoom, mapWidth, mapHeight, icon, dist, xDist, yDist);
data.dist = dist;
data.xDist = xDist;
data.yDist = yDist;
else
self:RemoveIconFromMinimap(icon)
end
count = count + 1
if ( count >= numPerCycle ) then
count = 0
yield()
-- check if we need to restart due to the full update being reset
if ( resetFullUpdate ) then
break;
end
end
end
if not ( resetFullUpdate ) then
local lastPosition = self.LastPlayerPosition;
lastPosition[1] = M;
lastPosition[2] = F;
lastPosition[3] = x;
lastPosition[4] = y;
resetIncrementalUpdate = true
end
else
if not ( self.WorldMapVisible ) then
self.processingFrame:Hide();
end
end
-- if we've been reset, then we want to start the new cycle immediately
if not ( resetFullUpdate ) then
fullUpdateInProgress = false
yield()
end
end
end
function Astrolabe:CalculateMinimapIconPositions( reset )
if ( fullUpdateCrashed ) then
fullUpdateThread = coroutine.wrap(CalculateMinimapIconPositions)
fullUpdateThread(self) --initialize the thread
elseif ( reset ) then
resetFullUpdate = true
end
fullUpdateCrashed = true
fullUpdateThread()
fullUpdateCrashed = false
-- return result flag
if ( fullUpdateInProgress ) then
return 1 -- full update started, but did not complete on this cycle
else
if ( resetIncrementalUpdate ) then
return 0 -- update completed
else
return -1 -- full update did no occur for some reason
end
end
end
end
function Astrolabe:GetDistanceToIcon( icon )
local data = self.MinimapIcons[icon];
if ( data ) then
return data.dist, data.xDist, data.yDist;
end
end
function Astrolabe:IsIconOnEdge( icon )
return self.IconsOnEdge[icon];
end
function Astrolabe:GetDirectionToIcon( icon )
local data = self.MinimapIcons[icon];
if ( data ) then
local dir = atan2(data.xDist, -(data.yDist))
if ( dir > 0 ) then
return twoPi - dir;
else
return -dir;
end
end
end
function Astrolabe:Register_OnEdgeChanged_Callback( func, ident )
-- check argument types
argcheck(func, 2, "function");
self.IconsOnEdge_GroupChangeCallbacks[func] = ident;
end
--*****************************************************************************
-- INTERNAL USE ONLY PLEASE!!!
-- Calling this function at the wrong time can cause errors
--*****************************************************************************
function Astrolabe:DumpNewIconsCache()
local MinimapIcons = self.MinimapIcons
for icon, data in pairs(AddedOrUpdatedIcons) do
MinimapIcons[icon] = data
AddedOrUpdatedIcons[icon] = nil
end
-- we now need to restart any updates that were in progress
resetIncrementalUpdate = true
resetFullUpdate = true
end
--------------------------------------------------------------------------------------------------------------
-- World Map Icon Placement
--------------------------------------------------------------------------------------------------------------
function Astrolabe:PlaceIconOnWorldMap( worldMapFrame, icon, mapID, mapFloor, xPos, yPos )
-- check argument types
argcheck(worldMapFrame, 2, "table");
assert(3, worldMapFrame.GetWidth and worldMapFrame.GetHeight, "Usage Message");
argcheck(icon, 3, "table");
assert(3, icon.SetPoint and icon.ClearAllPoints, "Usage Message");
argcheck(mapID, 4, "number");
argcheck(mapFloor, 5, "number", "nil");
argcheck(xPos, 6, "number");
argcheck(yPos, 7, "number");
local M, F = GetCurrentMapAreaID(), GetCurrentMapDungeonLevel();
local nX, nY = self:TranslateWorldMapPosition(mapID, mapFloor, xPos, yPos, M, F);
-- anchor and :Show() the icon if it is within the boundry of the current map, :Hide() it otherwise
if ( nX and nY and (0 < nX and nX <= 1) and (0 < nY and nY <= 1) ) then
icon:ClearAllPoints();
icon:SetPoint("CENTER", worldMapFrame, "TOPLEFT", nX * worldMapFrame:GetWidth(), -nY * worldMapFrame:GetHeight());
icon:Show();
else
icon:Hide();
end
return nX, nY;
end
--------------------------------------------------------------------------------------------------------------
-- Handler Scripts
--------------------------------------------------------------------------------------------------------------
function Astrolabe:OnEvent( frame, event )
if ( event == "MINIMAP_UPDATE_ZOOM" ) then
-- update minimap zoom scale
local Minimap = Minimap;
local curZoom = Minimap:GetZoom();
if ( GetCVar("minimapZoom") == GetCVar("minimapInsideZoom") ) then
if ( curZoom < 2 ) then
Minimap:SetZoom(curZoom + 1);
else
Minimap:SetZoom(curZoom - 1);
end
end
if ( GetCVar("minimapZoom")+0 == Minimap:GetZoom() ) then
self.minimapOutside = true;
else
self.minimapOutside = false;
end
Minimap:SetZoom(curZoom);
-- re-calculate all Minimap Icon positions
if ( frame:IsVisible() ) then
self:CalculateMinimapIconPositions(true);
end
elseif ( event == "PLAYER_LEAVING_WORLD" ) then
frame:Hide(); -- yes, I know this is redunant
self:RemoveAllMinimapIcons(); --dump all minimap icons
-- TODO: when I uncouple the point buffer from Minimap drawing,
-- I should consider updating LastPlayerPosition here
elseif ( event == "PLAYER_ENTERING_WORLD" ) then
frame:Show();
if not ( frame:IsVisible() ) then
-- do the minimap recalculation anyways if the OnShow script didn't execute
-- this is done to ensure the accuracy of information about icons that were
-- inserted while the Player was in the process of zoning
self:CalculateMinimapIconPositions(true);
end
elseif ( event == "ZONE_CHANGED_NEW_AREA" ) then
frame:Hide();
frame:Show();
end
end
function Astrolabe:OnUpdate( frame, elapsed )
-- on-edge group changed call-backs
if ( self.IconsOnEdgeChanged ) then
self.IconsOnEdgeChanged = false;
for func in pairs(self.IconsOnEdge_GroupChangeCallbacks) do
pcall(func);
end
end
self:UpdateMinimapIconPositions();
end
function Astrolabe:OnShow( frame )
-- set the world map to a zoom with a valid player position
if not ( self.WorldMapVisible ) then
SetMapToCurrentZone();
end
local M, F = Astrolabe:GetCurrentPlayerPosition();
if ( M and M >= 0 ) then
SetMapByID(M);
SetDungeonMapLevel(F);
else
frame:Hide();
return
end
-- re-calculate minimap icon positions (if needed)
if ( next(self.MinimapIcons) ) then
self:CalculateMinimapIconPositions(true);
else
-- needed so that the cycle doesn't overwrite an updated LastPlayerPosition
resetIncrementalUpdate = true;
end
if ( self.MinimapIconCount <= 0 ) then
-- no icons left to manage
frame:Hide();
end
end
function Astrolabe:OnHide( frame )
-- dump the new icons cache here
-- a full update will performed the next time processing is re-actived
self:DumpNewIconsCache()
end
-- called by AstrolabMapMonitor when all world maps are hidden
function Astrolabe:AllWorldMapsHidden()
if ( IsLoggedIn() ) then
self.processingFrame:Hide();
self.processingFrame:Show();
end
end
--------------------------------------------------------------------------------------------------------------
-- Library Registration
--------------------------------------------------------------------------------------------------------------
local HARVESTED_DATA_VERSION = 3; -- increment this when the format of the harvested data has to change
local function harvestMapData( HarvestedMapData )
local mapData = {}
local mapName = GetMapInfo();
local mapID = GetCurrentMapAreaID();
local numFloors = GetNumDungeonMapLevels();
mapData.mapName = mapName;
mapData.cont = (GetCurrentMapContinent()) or -100;
mapData.zone = (GetCurrentMapZone()) or -100;
mapData.numFloors = numFloors;
local _, TLx, TLy, BRx, BRy = GetCurrentMapZone();
if ( TLx and TLy and BRx and BRy and (TLx~=0 or TLy~=0 or BRx~=0 or BRy~=0) ) then
mapData[0] = {};
mapData[0].TLx = TLx;
mapData[0].TLy = TLy;
mapData[0].BRx = BRx;
mapData[0].BRy = BRy;
end
if ( not mapData[0] and numFloors == 0 and (GetCurrentMapDungeonLevel()) == 1 ) then
numFloors = 1;
mapData.hiddenFloor = true;
end
if ( numFloors > 0 ) then
for f = 1, numFloors do
SetDungeonMapLevel(f);
local _, TLx, TLy, BRx, BRy = GetCurrentMapDungeonLevel();
if ( TLx and TLy and BRx and BRy ) then
mapData[f] = {};
mapData[f].TLx = TLx;
mapData[f].TLy = TLy;
mapData[f].BRx = BRx;
mapData[f].BRy = BRy;
end
end
end
HarvestedMapData[mapID] = mapData;
end
local function activate( newInstance, oldInstance )
if ( oldInstance ) then -- this is an upgrade activate
-- print upgrade debug info
local _, oldVersion = oldInstance:GetVersion();
printError("Upgrading "..LIBRARY_VERSION_MAJOR.." from version "..oldVersion.." to version "..LIBRARY_VERSION_MINOR);
if ( oldInstance.DumpNewIconsCache ) then
oldInstance:DumpNewIconsCache()
end
for k, v in pairs(oldInstance) do
if ( type(v) ~= "function" and (not configConstants[k]) ) then
newInstance[k] = v;
end
end
-- sync up the current MinimapIconCount value
local iconCount = 0
for _ in pairs(newInstance.MinimapIcons) do
iconCount = iconCount + 1
end
newInstance.MinimapIconCount = iconCount
Astrolabe = oldInstance;
else
local frame = CreateFrame("Frame");
newInstance.processingFrame = frame;
end
configConstants = nil -- we don't need this anymore
if not ( oldInstance and oldInstance.HarvestedMapData.VERSION == HARVESTED_DATA_VERSION ) then
newInstance.HarvestedMapData = { VERSION = HARVESTED_DATA_VERSION };
local HarvestedMapData = newInstance.HarvestedMapData;
newInstance.ContinentList = { GetMapContinents() };
for C in pairs(newInstance.ContinentList) do
local zones = { GetMapZones(C) };
newInstance.ContinentList[C] = zones;
SetMapZoom(C, 0);
zones[0] = GetCurrentMapAreaID();
harvestMapData(HarvestedMapData);
for Z in ipairs(zones) do
SetMapZoom(C, Z);
zones[Z] = GetCurrentMapAreaID();
harvestMapData(HarvestedMapData);
end
end
for _, id in ipairs(GetAreaMaps()) do
if not ( HarvestedMapData[id] ) then
if ( SetMapByID(id) ) then
harvestMapData(HarvestedMapData);
end
end
end
end
local frame = newInstance.processingFrame;
frame:Hide();
frame:SetParent("Minimap");
frame:UnregisterAllEvents();
frame:RegisterEvent("MINIMAP_UPDATE_ZOOM");
frame:RegisterEvent("PLAYER_LEAVING_WORLD");
frame:RegisterEvent("PLAYER_ENTERING_WORLD");
frame:RegisterEvent("ZONE_CHANGED_NEW_AREA");
frame:SetScript("OnEvent",
function( frame, event, ... )
Astrolabe:OnEvent(frame, event, ...);
end
);
frame:SetScript("OnUpdate",
function( frame, elapsed )
Astrolabe:OnUpdate(frame, elapsed);
end
);
frame:SetScript("OnShow",
function( frame )
Astrolabe:OnShow(frame);
end
);
frame:SetScript("OnHide",
function( frame )
Astrolabe:OnHide(frame);
end
);
setmetatable(Astrolabe.MinimapIcons, MinimapIconsMetatable)
end
DongleStub:Register(Astrolabe, activate)
--------------------------------------------------------------------------------------------------------------
-- Data
--------------------------------------------------------------------------------------------------------------
-- diameter of the Minimap in game yards at
-- the various possible zoom levels
MinimapSize = {
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
},
}
ValidMinimapShapes = {
-- { 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 },
}
-- distances across and offsets of the world maps
-- in game yards
WorldMapSize = {
[0] = {
height = 22266.74312,
system = -1,
width = 33400.121,
xOffset = 0,
yOffset = 0,
[1] = {
xOffset = -10311.71318,
yOffset = -19819.33898,
scale = 0.56089997291565,
},
[0] = {
xOffset = -48226.86993,
yOffset = -16433.90283,
scale = 0.56300002336502,
},
[571] = {
xOffset = -29750.89905,
yOffset = -11454.50802,
scale = 0.5949000120163,
},
[870] = {
xOffset = -27693.71178,
yOffset = -29720.0585,
scale = 0.65140002965927,
},
},
}
MicroDungeonSize = {}
local function zeroDataFunc(tbl, key)
if ( type(key) == "number" ) then
return zeroData;
else
return rawget(zeroData, key);
end
end
zeroData = { xOffset = 0, height = 1, yOffset = 0, width = 1, __index = zeroDataFunc };
setmetatable(zeroData, zeroData);
-- get data on useful transforms
local TRANSFORMS = {}
for _, ID in ipairs(GetWorldMapTransforms()) do
local terrainMapID, newTerrainMapID, _, _, transformMinY, transformMaxY, transformMinX, transformMaxX, offsetY, offsetX = GetWorldMapTransformInfo(ID)
if ( offsetX ~= 0 or offsetY ~= 0 ) then
TRANSFORMS[ID] = {
terrainMapID = terrainMapID,
newTerrainMapID = newTerrainMapID,
BRy = -transformMinY,
TLy = -transformMaxY,
BRx = -transformMinX,
TLx = -transformMaxX,
offsetY = offsetY,
offsetX = offsetX,
}
end
end
--remove this temporarily
local harvestedDataVersion = Astrolabe.HarvestedMapData.VERSION
Astrolabe.HarvestedMapData.VERSION = nil
for mapID, harvestedData in pairs(Astrolabe.HarvestedMapData) do
local terrainMapID = GetAreaMapInfo(mapID)
local mapData = WorldMapSize[mapID];
if not ( mapData ) then mapData = {}; end
if ( harvestedData.numFloors > 0 or harvestedData.hiddenFloor ) then
for f, harvData in pairs(harvestedData) do
if ( type(f) == "number" and f > 0 ) then
if not ( mapData[f] ) then
mapData[f] = {};
end
local floorData = mapData[f]
local TLx, TLy, BRx, BRy = -harvData.BRx, -harvData.BRy, -harvData.TLx, -harvData.TLy
if not ( TLx < BRx ) then
printError("Bad x-axis Orientation (Floor): ", mapID, f, TLx, BRx);
end
if not ( TLy < BRy) then
printError("Bad y-axis Orientation (Floor): ", mapID, f, TLy, BRy);
end
if not ( floorData.width ) then
floorData.width = BRx - TLx
end
if not ( floorData.height ) then
floorData.height = BRy - TLy
end
if not ( floorData.xOffset ) then
floorData.xOffset = TLx
end
if not ( floorData.yOffset ) then
floorData.yOffset = TLy
end
end
end
for f = 1, harvestedData.numFloors do
if not ( mapData[f] ) then
if ( f == 1 and harvestedData[0] and harvestedData[0].TLx and harvestedData[0].TLy and harvestedData[0].BRx and harvestedData[0].BRy ) then
-- handle dungeon maps which use zone level data for the first floor
mapData[f] = {};
local floorData = mapData[f]
local harvData = harvestedData[0]
local TLx, TLy, BRx, BRy = -harvData.TLx, -harvData.TLy, -harvData.BRx, -harvData.BRy
if not ( TLx < BRx ) then
printError("Bad x-axis Orientation (Floor from Zone): ", mapID, f, TLx, BRx);
end
if not ( TLy < BRy) then
printError("Bad y-axis Orientation (Floor from Zone): ", mapID, f, TLy, BRy);
end
floorData.width = BRx - TLx
floorData.height = BRy - TLy
floorData.xOffset = TLx
floorData.yOffset = TLy
else
printError(("Astrolabe is missing data for %s [%d], floor %d."):format(harvestedData.mapName, mapID, f));
end
end
end
if ( harvestedData.hiddenFloor ) then
mapData.width = mapData[1].width
mapData.height = mapData[1].height
mapData.xOffset = mapData[1].xOffset
mapData.yOffset = mapData[1].yOffset
end
else
local harvData = harvestedData[0]
if ( harvData ~= nil ) then
local TLx, TLy, BRx, BRy = -harvData.TLx, -harvData.TLy, -harvData.BRx, -harvData.BRy
-- apply any necessary transforms
for transformID, transformData in pairs(TRANSFORMS) do
if ( transformData.terrainMapID == terrainMapID ) then
if ( (transformData.TLx < TLx and BRx < transformData.BRx) and (transformData.TLy < TLy and BRy < transformData.BRy) ) then
TLx = TLx - transformData.offsetX;
BRx = BRx - transformData.offsetX;
BRy = BRy - transformData.offsetY;
TLy = TLy - transformData.offsetY;
terrainMapID = transformData.newTerrainMapID;
break;
end
end
end
if not ( TLx==0 and TLy==0 and BRx==0 and BRy==0 ) then
if not ( TLx < BRx ) then
printError("Bad x-axis Orientation (Zone): ", mapID, TLx, BRx);
end
if not ( TLy < BRy) then
printError("Bad y-axis Orientation (Zone): ", mapID, TLy, BRy);
end
end
if not ( mapData.width ) then
mapData.width = BRx - TLx
end
if not ( mapData.height ) then
mapData.height = BRy - TLy
end
if not ( mapData.xOffset ) then
mapData.xOffset = TLx
end
if not ( mapData.yOffset ) then
mapData.yOffset = TLy
end
else
if ( mapID == 751 ) then -- okay, this is Maelstrom continent
else
printError("Astrolabe harvested a map with no data at all: ", mapID)
end
end
end
-- if we don't have any data, we're gonna use zeroData, but we also need to
-- setup the system and systemParent IDs so things don't get confused
if not ( next(mapData, nil) ) then
mapData = { xOffset = 0, height = 1, yOffset = 0, width = 1 };
-- if this is an outside continent level or world map then throw up an extra warning
if ( harvestedData.cont > 0 and harvestedData.zone == 0 ) then
printError(("Astrolabe is missing data for world map %s [%d] (%d, %d)."):format(harvestedData.mapName, mapID, harvestedData.cont, harvestedData.zone));
end
end
-- store the data in the WorldMapSize DB
WorldMapSize[mapID] = mapData;
if ( mapData and mapData ~= zeroData ) then
-- setup system IDs
if not ( mapData.system ) then
mapData.system = terrainMapID;
end
-- determine terrainMapID for micro-dungeons
if ( harvestedData.cont > 0 and harvestedData.zone > 0 ) then
MicroDungeonSize[terrainMapID] = {}
end
setmetatable(mapData, zeroData);
end
end
-- put the version back
Astrolabe.HarvestedMapData.VERSION = harvestedDataVersion
-- micro dungeons
for _, ID in ipairs(GetDungeonMaps()) do
local floorIndex, minX, maxX, minY, maxY, terrainMapID, parentWorldMapID = GetDungeonMapInfo(ID);
local TLx, TLy, BRx, BRy = -maxX, -maxY, -minX, -minY
-- apply any necessary transforms
local transformApplied = false
for transformID, transformData in pairs(TRANSFORMS) do
if ( transformData.terrainMapID == terrainMapID ) then
if ( (transformData.TLx < TLx and BRx < transformData.BRx) and (transformData.TLy < TLy and BRy < transformData.BRy) ) then
TLx = TLx - transformData.offsetX;
BRx = BRx - transformData.offsetX;
BRy = BRy - transformData.offsetY;
TLy = TLy - transformData.offsetY;
terrainMapID = transformData.newTerrainMapID;
transformApplied = true;
break;
end
end
end
if ( MicroDungeonSize[terrainMapID] ) then
-- only consider systems that can have micro dungeons
if ( MicroDungeonSize[terrainMapID][floorIndex] and not transformApplied ) then
printError("Astrolabe detected a duplicate microdungeon floor!", terrainMapID, ID);
end
MicroDungeonSize[terrainMapID][floorIndex] = {
width = BRx - TLx,
height = BRy - TLy,
xOffset = TLx,
yOffset = TLy,
};
end
end
-- done with Transforms data
TRANSFORMS = nil
for _, data in pairs(MicroDungeonSize) do
setmetatable(data, zeroData);
end
setmetatable(MicroDungeonSize, zeroData);
-- make sure we don't have any EXTRA data hanging around
for mapID, mapData in pairs(WorldMapSize) do
if ( mapID ~= 0 and getmetatable(mapData) ~= zeroData ) then
printError("Astrolabe has hard coded data for an invalid map ID", mapID);
end
end
setmetatable(WorldMapSize, zeroData); -- setup the metatable so that invalid map IDs don't cause Lua errors
-- register this library with AstrolabeMapMonitor, this will cause a full update if PLAYER_LOGIN has already fired
local AstrolabeMapMonitor = DongleStub("AstrolabeMapMonitor");
AstrolabeMapMonitor:RegisterAstrolabeLibrary(Astrolabe, LIBRARY_VERSION_MAJOR);