You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1604 lines
50 KiB

local _, addon = ...
local API = addon.API;
local IS_TWW = addon.IsGame_11_0_0;
local tonumber = tonumber;
local match = string.match;
local format = string.format;
local gsub = string.gsub;
local tinsert = table.insert;
local tremove = table.remove;
local floor = math.floor;
local sqrt = math.sqrt;
local time = time;
local unpack = unpack;
local GetCVarBool = C_CVar.GetCVarBool;
local CreateFrame = CreateFrame;
local securecallfunction = securecallfunction;
local function Nop(...)
end
do -- Table
local function Mixin(object, ...)
for i = 1, select("#", ...) do
local mixin = select(i, ...)
for k, v in pairs(mixin) do
object[k] = v;
end
end
return object
end
API.Mixin = Mixin;
local function CreateFromMixins(...)
return Mixin({}, ...)
end
API.CreateFromMixins = CreateFromMixins;
local function RemoveValueFromList(tbl, v)
for i = 1, #tbl do
if tbl[i] == v then
tremove(tbl, i);
return true
end
end
end
API.RemoveValueFromList = RemoveValueFromList;
local function ReverseList(list)
if not list then return end;
local tbl = {};
local n = 0;
for i = #list, 1, -1 do
n = n + 1;
tbl[n] = list[i];
end
return tbl
end
API.ReverseList = ReverseList;
end
do -- String
local function GetCreatureIDFromGUID(guid)
local id = match(guid, "Creature%-0%-%d*%-%d*%-%d*%-(%d*)");
if id then
return tonumber(id)
end
end
API.GetCreatureIDFromGUID = GetCreatureIDFromGUID;
local function GetVignetteIDFromGUID(guid)
local id = match(guid, "Vignette%-0%-%d*%-%d*%-%d*%-(%d*)");
if id then
return tonumber(id)
end
end
API.GetVignetteIDFromGUID = GetVignetteIDFromGUID;
local function GetWaypointFromText(text)
local uiMapID, x, y = match(text, "|Hworldmap:(%d*):(%d*):(%d*)|h");
if uiMapID and x and y then
return tonumber(uiMapID), tonumber(x), tonumber(y)
end
end
API.GetWaypointFromText = GetWaypointFromText;
local UnitGUID = UnitGUID;
local function GetUnitCreatureID(unit)
local guid = UnitGUID(unit);
if guid then
return GetCreatureIDFromGUID(guid)
end
end
API.GetUnitCreatureID = GetUnitCreatureID;
local function GetGlobalObject(objNameKey)
--Get object via string "FrameName.Key1.Key2"
local obj = _G;
for k in string.gmatch(objNameKey, "%w+") do
obj = obj[k];
if not obj then
return
end
end
return obj
end
API.GetGlobalObject = GetGlobalObject;
end
do -- DEBUG
local function CreateSaveDB(key)
if not PlumberDevOutput then
PlumberDevOutput = {};
end
if not PlumberDevOutput[key] then
PlumberDevOutput[key] = {};
end
end
local function SaveLocalizedText(localizedText, englishText)
local locale = GetLocale();
CreateSaveDB(locale);
PlumberDevOutput[locale][localizedText] = englishText or true;
end
API.SaveLocalizedText = SaveLocalizedText;
local function SaveDataUnderKey(key, ...)
CreateSaveDB(key);
PlumberDevOutput[key] = {...}
end
API.SaveDataUnderKey = SaveDataUnderKey;
end
do --Math
local function Clamp(value, min, max)
if value > max then
return max
elseif value < min then
return min
end
return value
end
API.Clamp = Clamp;
local function Lerp(startValue, endValue, amount)
return (1 - amount) * startValue + amount * endValue;
end
API.Lerp = Lerp;
local function GetPointsDistance2D(x1, y1, x2, y2)
return sqrt( (x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2))
end
API.GetPointsDistance2D = GetPointsDistance2D;
local function Round(n)
return floor(n + 0.5);
end
API.Round = Round;
end
do -- Color
local ColorSwatches = {
SelectionBlue = {12, 105, 216},
SmoothGreen = {124, 197, 118},
WarningRed = {212, 100, 28}, --228, 13, 14 248, 81, 73
};
for _, swatch in pairs(ColorSwatches) do
swatch[1] = swatch[1]/255;
swatch[2] = swatch[2]/255;
swatch[3] = swatch[3]/255;
end
local function GetColorByName(colorName)
if ColorSwatches[colorName] then
return unpack(ColorSwatches[colorName])
else
return 1, 1, 1
end
end
API.GetColorByName = GetColorByName;
-- Make Rare and Epic brighter (use the color in Narcissus)
local ITEM_QUALITY_COLORS = ITEM_QUALITY_COLORS;
local QualityColors = {};
QualityColors[3] = CreateColor(105/255, 158/255, 255/255, 1);
QualityColors[4] = CreateColor(185/255, 83/255, 255/255, 1);
local function GetItemQualityColor(quality)
if QualityColors[quality] then
return QualityColors[quality]
else
return ITEM_QUALITY_COLORS[quality].color
end
end
API.GetItemQualityColor = GetItemQualityColor;
local function IsWarningColor(r, g, b)
--Used to determine if the tooltip fontstring is red, which indicates there is a requirement you don't meet
return (r > 0.99 and r <= 1) and (g > 0.1254 and g < 0.1255) and (b > 0.1254 and b < 0.1255)
end
API.IsWarningColor = IsWarningColor;
local function SetTextColorByGlobal(fontString, colorMixin)
local r, g, b;
if colorMixin then
r, g, b = colorMixin:GetRGB();
else
r, g, b = 1, 1, 1;
end
fontString:SetTextColor(r, g, b);
end
API.SetTextColorByGlobal = SetTextColorByGlobal;
end
do -- Time
local D_DAYS = D_DAYS or "%d |4Day:Days;";
local D_HOURS = D_HOURS or "%d |4Hour:Hours;";
local D_MINUTES = D_MINUTES or "%d |4Minute:Minutes;";
local D_SECONDS = D_SECONDS or "%d |4Second:Seconds;";
local DAYS_ABBR = DAYS_ABBR or "%d |4Day:Days;"
local HOURS_ABBR = HOURS_ABBR or "%d |4Hr:Hr;";
local MINUTES_ABBR = MINUTES_ABBR or "%d |4Min:Min;";
local SECONDS_ABBR = SECONDS_ABBR or "%d |4Sec:Sec;";
local SHOW_HOUR_BELOW_DAYS = 3;
local SHOW_MINUTE_BELOW_HOURS = 12;
local SHOW_SECOND_BELOW_MINUTES = 10;
local COLOR_RED_BELOW_SECONDS = 172800;
local function BakePlural(number, singularPlural)
singularPlural = gsub(singularPlural, ";", "");
if number > 1 then
return format(gsub(singularPlural, "|4[^:]*:", ""), number);
else
singularPlural = gsub(singularPlural, ":.*", "");
singularPlural = gsub(singularPlural, "|4", "");
return format(singularPlural, number);
end
end
local function FormatTime(t, pattern, bakePluralEscapeSequence)
if bakePluralEscapeSequence then
return BakePlural(t, pattern);
else
return format(pattern, t);
end
end
local function SecondsToTime(seconds, abbreviated, partialTime, bakePluralEscapeSequence)
--partialTime: Stop processing if the remaining units don't really matter. e.g. to display the remaining time of an event when there are still days left
--bakePluralEscapeSequence: Convert EcsapeSequence like "|4Sec:Sec;" to its result so it can be sent to chat
local intialSeconds = seconds;
local timeString = "";
local isComplete = false;
local days = 0;
local hours = 0;
local minutes = 0;
if seconds >= 86400 then
days = floor(seconds / 86400);
seconds = seconds - days * 86400;
local dayText = FormatTime(days, (abbreviated and DAYS_ABBR) or D_DAYS, bakePluralEscapeSequence);
timeString = dayText;
if partialTime and days >= SHOW_HOUR_BELOW_DAYS then
isComplete = true;
end
end
if not isComplete then
hours = floor(seconds / 3600);
seconds = seconds - hours * 3600;
if hours > 0 then
local hourText = FormatTime(hours, (abbreviated and HOURS_ABBR) or D_HOURS, bakePluralEscapeSequence);
if timeString == "" then
timeString = hourText;
else
timeString = timeString.." "..hourText;
end
if partialTime and hours >= SHOW_MINUTE_BELOW_HOURS then
isComplete = true;
end
else
if timeString ~= "" and partialTime then
isComplete = true;
end
end
end
if partialTime and days > 0 then
isComplete = true;
end
if not isComplete then
minutes = floor(seconds / 60);
seconds = seconds - minutes * 60;
if minutes > 0 then
local minuteText = FormatTime(minutes, (abbreviated and MINUTES_ABBR) or D_MINUTES, bakePluralEscapeSequence);
if timeString == "" then
timeString = minuteText;
else
timeString = timeString.." "..minuteText;
end
if partialTime and minutes >= SHOW_SECOND_BELOW_MINUTES then
isComplete = true;
end
else
if timeString ~= "" and partialTime then
isComplete = true;
end
end
end
if (not isComplete) and seconds > 0 then
seconds = floor(seconds);
local secondText = FormatTime(seconds, (abbreviated and SECONDS_ABBR) or D_SECONDS, bakePluralEscapeSequence);
if timeString == "" then
timeString = secondText;
else
timeString = timeString.." "..secondText;
end
end
if partialTime and intialSeconds < COLOR_RED_BELOW_SECONDS and not bakePluralEscapeSequence then
--WARNING_FONT_COLOR
timeString = "|cffff4800"..timeString.."|r";
end
return timeString
end
API.SecondsToTime = SecondsToTime;
local function SecondsToClock(seconds)
--Clock: 00:00
return format("%s:%02d", math.floor(seconds / 60), math.floor(seconds % 60))
end
API.SecondsToClock = SecondsToClock;
--Unix Epoch is in UTC
local MonthDays = {
31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
};
local function IsLeapYear(year)
return year % 400 == 0 or (year % 4 == 0 and year % 100 ~= 0)
end
local function GetFebruaryDays(year)
if IsLeapYear(year) then
return 29
else
return 28
end
end
local function IsLeftTimeFuture(time1, time2, i)
if not time1[i] then return false end;
if time1[i] > time2[i] then
return true;
elseif time1[i] == time2[i] then
return IsLeftTimeFuture(time1, time2, i + 1);
else
return false
end
end
local function GetNumDaysToDate(year, month, day)
local numDays = day;
for yr = 1, (year -1) do
if IsLeapYear(yr) then
numDays = numDays + 366;
else
numDays = numDays + 365;
end
end
for m = 1, (month - 1) do
if m == 2 then
numDays = numDays + GetFebruaryDays(year);
else
numDays = numDays + MonthDays[m];
end
end
return numDays
end
local function GetNumSecondsToDate(year, month, day, hour, minute, second)
hour = hour or 0;
minute = minute or 0;
second = second or 0;
local numDays = GetNumDaysToDate(year, month, day);
local numSeconds = second;
numSeconds = numSeconds + numDays * 86400;
numSeconds = numSeconds + hour * 3600 + minute * 60;
return numSeconds
end
local function ConvertCalendarTime(calendarTime)
--WoW's CalendarTime See https://warcraft.wiki.gg/wiki/API_C_DateAndTime.GetCurrentCalendarTime
local year = calendarTime.year;
local month = calendarTime.month;
local day = calendarTime.monthDay;
local hour = calendarTime.hour;
local minute = calendarTime.minute;
local second = calendarTime.second or 0; --the original calendarTime does not contain second
return {year, month, day, hour, minute, second}
end
local function GetCalendarTimeDifference(lhsCalendarTime, rhsCalendarTime)
--time = {year, month, day, hour, minute, second}
local time1 = ConvertCalendarTime(lhsCalendarTime);
local time2 = ConvertCalendarTime(rhsCalendarTime);
local second1 = GetNumSecondsToDate(unpack(time1));
local second2 = GetNumSecondsToDate(unpack(time2));
return second2 - second1
end
API.GetCalendarTimeDifference = GetCalendarTimeDifference;
local function WrapNumberWithBrackets(text)
text = gsub(text, "%%d%+", "%%d");
text = gsub(text, "%%d", "%(%%d%+%)");
return text
end
local PATTERN_DAYS = WrapNumberWithBrackets(DAYS_ABBR);
local PATTERN_HOURS = WrapNumberWithBrackets(HOURS_ABBR);
local PATTERN_MINUTES = WrapNumberWithBrackets(MINUTES_ABBR);
local PATTERN_SECONDS = WrapNumberWithBrackets(SECONDS_ABBR);
local function ConvertTextToSeconds(durationText)
if not durationText then return 0 end;
local hours = tonumber(match(durationText, PATTERN_HOURS) or 0);
local minutes = tonumber(match(durationText, PATTERN_MINUTES) or 0);
local seconds = tonumber(match(durationText, PATTERN_SECONDS) or 0);
return 3600 * hours + 60 * minutes + seconds;
end
API.TimeLeftTextToSeconds = ConvertTextToSeconds;
end
do -- Item
local C_Item = C_Item;
local GetItemSpell = GetItemSpell;
local function ColorizeTextByQuality(text, quality, allowColorBlind)
if not (text and quality) then
return text
end
local color = API.GetItemQualityColor(quality);
text = color:WrapTextInColorCode(text);
if allowColorBlind and GetCVarBool("colorblindMode") then
text = text.." |cffffffff[".._G[format("ITEM_QUALITY%s_DESC", quality)].."]|r";
end
return text
end
API.ColorizeTextByQuality = ColorizeTextByQuality;
local function GetColorizedItemName(itemID)
local name = C_Item.GetItemNameByID(itemID);
local quality = C_Item.GetItemQualityByID(itemID);
return ColorizeTextByQuality(name, quality, true);
end
API.GetColorizedItemName = GetColorizedItemName;
local function GetItemSpellID(item)
local spellName, spellID = GetItemSpell(item);
return spellID
end
API.GetItemSpellID = GetItemSpellID;
end
do -- Tooltip Parser
local GetInfoByHyperlink = C_TooltipInfo.GetHyperlink;
local function GetLineText(lines, index)
if lines[index] then
return lines[index].leftText;
end
end
local function GetCreatureName(creatureID)
if not creatureID then return end;
local tooltipData = GetInfoByHyperlink("unit:Creature-0-0-0-0-"..creatureID);
if tooltipData then
return GetLineText(tooltipData.lines, 1);
end
end
API.GetCreatureName = GetCreatureName;
end
do -- Holiday
local CalendarTextureXHolidayKey = {
--Only the important ones :)
[235469] = "lunarfestival",
[235470] = "lunarfestival",
[235471] = "lunarfestival",
[235466] = "loveintheair",
[235467] = "loveintheair",
[235468] = "loveintheair",
[235475] = "noblegarden",
[235476] = "noblegarden",
[235477] = "noblegarden",
[235443] = "childrensweek",
[235444] = "childrensweek",
[235445] = "childrensweek",
[235472] = "midsummer",
[235473] = "midsummer",
[235474] = "midsummer",
[235439] = "brewfest",
[235440] = "brewfest",
[235441] = "brewfest",
[235442] = "brewfest",
[235460] = "hallowsendend",
[235461] = "hallowsendend",
[235462] = "hallowsendend",
[235482] = "winterveil",
[235483] = "winterveil",
[235484] = "winterveil",
[235485] = "winterveil",
};
local HolidayInfoMixin = {};
function HolidayInfoMixin:GetRemainingSeconds()
if self.endTime then
local presentTime = time();
return self.endTime - presentTime
else
return 0
end
end
function HolidayInfoMixin:GetEndTimeString()
--MM/DD 00:00
return self.endTimeString
end
function HolidayInfoMixin:GetRemainingTimeString()
--DD/HH/MM/SS
local seconds = self:GetRemainingSeconds();
return API.SecondsToTime(seconds, false, true);
end
function HolidayInfoMixin:GetName()
return self.name
end
function HolidayInfoMixin:GetKey()
return self.key
end
local function GetActiveMajorHolidayInfo()
local currentCalendarTime = C_DateAndTime.GetCurrentCalendarTime();
local presentDay = currentCalendarTime.monthDay;
local monthOffset = 0;
local holidayInfo;
local holidayKey, holidayName;
local endTimeString;
local eventEndTimeMixin; --{}
local endTime; --number time()
local activeHolidayData; --{ {holiday1}, {holiday2} }
for i = 1, C_Calendar.GetNumDayEvents(monthOffset, presentDay) do --Need to request data first with C_Calendar.OpenCalendar()
holidayInfo = C_Calendar.GetHolidayInfo(monthOffset, presentDay, i);
--print(i, holidayInfo.name)
if holidayInfo and holidayInfo.texture and CalendarTextureXHolidayKey[holidayInfo.texture] then
holidayKey = CalendarTextureXHolidayKey[holidayInfo.texture];
holidayName = holidayInfo.name;
if holidayInfo.startTime and holidayInfo.endTime then
endTimeString = FormatShortDate(holidayInfo.endTime.monthDay, holidayInfo.endTime.month) .." ".. GameTime_GetFormattedTime(holidayInfo.endTime.hour, holidayInfo.endTime.minute, true);
eventEndTimeMixin = holidayInfo.endTime;
end
local isEventActive = true;
if eventEndTimeMixin then
local presentTime = time();
local remainingSeconds = API.GetCalendarTimeDifference(currentCalendarTime, eventEndTimeMixin);
endTime = presentTime + remainingSeconds;
if remainingSeconds <= 0 then
isEventActive = false;
end
end
if isEventActive and holidayName then
local mixin = API.CreateFromMixins(HolidayInfoMixin);
mixin.name = holidayName;
mixin.key = holidayKey;
mixin.endTimeString = endTimeString;
mixin.endTime = endTime;
if not activeHolidayData then
activeHolidayData = {};
end
tinsert(activeHolidayData, mixin);
end
end
end
return activeHolidayData
end
API.GetActiveMajorHolidayInfo = GetActiveMajorHolidayInfo;
end
do --Fade Frame
local abs = math.abs;
local tinsert = table.insert;
local wipe = wipe;
local fadeInfo = {};
local fadingFrames = {};
local f = CreateFrame("Frame");
local function OnUpdate(self, elpased)
local i = 1;
local frame, info, timer, alpha;
local isComplete = true;
while fadingFrames[i] do
frame = fadingFrames[i];
info = fadeInfo[frame];
if info then
timer = info.timer + elpased;
if timer >= info.duration then
alpha = info.toAlpha;
fadeInfo[frame] = nil;
if info.alterShownState and alpha <= 0 then
frame:Hide();
end
else
alpha = info.fromAlpha + (info.toAlpha - info.fromAlpha) * timer/info.duration;
info.timer = timer;
end
frame:SetAlpha(alpha);
isComplete = false;
end
i = i + 1;
end
if isComplete then
f:Clear();
end
end
function f:Clear()
self:SetScript("OnUpdate", nil);
wipe(fadingFrames);
wipe(fadeInfo);
end
function f:Add(frame, fullDuration, fromAlpha, toAlpha, alterShownState, useConstantDuration)
local alpha = frame:GetAlpha();
if alterShownState then
if toAlpha > 0 then
frame:Show();
end
if toAlpha == 0 then
if not frame:IsShown() then
frame:SetAlpha(0);
alpha = 0;
end
if alpha == 0 then
frame:Hide();
end
end
end
if fromAlpha == toAlpha or alpha == toAlpha then
if fadeInfo[frame] then
fadeInfo[frame] = nil;
end
return;
end
local duration;
if useConstantDuration then
duration = fullDuration;
else
if fromAlpha then
duration = fullDuration * (alpha - toAlpha)/(fromAlpha - toAlpha);
else
duration = fullDuration * abs(alpha - toAlpha);
end
end
if duration <= 0 then
frame:SetAlpha(toAlpha);
if toAlpha == 0 then
frame:Hide();
end
return;
end
fadeInfo[frame] = {
fromAlpha = alpha,
toAlpha = toAlpha,
duration = duration,
timer = 0,
alterShownState = alterShownState,
};
for i = 1, #fadingFrames do
if fadingFrames[i] == frame then
return;
end
end
tinsert(fadingFrames, frame);
self:SetScript("OnUpdate", OnUpdate);
end
function f:SimpleFade(frame, toAlpha, alterShownState, speedMultiplier)
--Use a constant fading speed: 1.0 in 0.25s
--alterShownState: if true, run Frame:Hide() when alpha reaches zero / run Frame:Show() at the beginning
speedMultiplier = speedMultiplier or 1;
local alpha = frame:GetAlpha();
local duration = abs(alpha - toAlpha) * 0.25 * speedMultiplier;
if duration <= 0 then
return;
end
self:Add(frame, duration, alpha, toAlpha, alterShownState, true);
end
function f:Snap()
local i = 1;
local frame, info;
while fadingFrames[i] do
frame = fadingFrames[i];
info = fadeInfo[frame];
if info then
frame:SetAlpha(info.toAlpha);
end
i = i + 1;
end
self:Clear();
end
local function UIFrameFade(frame, duration, toAlpha, initialAlpha)
if initialAlpha then
frame:SetAlpha(initialAlpha);
f:Add(frame, duration, initialAlpha, toAlpha, true, false);
else
f:Add(frame, duration, nil, toAlpha, true, false);
end
end
local function UIFrameFadeIn(frame, duration)
frame:SetAlpha(0);
f:Add(frame, duration, 0, 1, true, false);
end
API.UIFrameFade = UIFrameFade; --from current alpha
API.UIFrameFadeIn = UIFrameFadeIn; --from 0 to 1
end
do -- Map
---- Create Module that will be activated in specific zones:
---- 1. Soridormi Auto-report
---- 2. Dreamseed
local C_Map = C_Map;
local GetMapInfo = C_Map.GetMapInfo;
local GetBestMapForUnit = C_Map.GetBestMapForUnit;
local CreateVector2D = CreateVector2D;
local controller;
local modules;
local lastMapID, total;
local ZoneTriggeredModuleMixin = {};
ZoneTriggeredModuleMixin.validMaps = {};
ZoneTriggeredModuleMixin.inZone = false;
ZoneTriggeredModuleMixin.enabled = true;
local function DoNothing(arg)
end
ZoneTriggeredModuleMixin.enterZoneCallback = DoNothing;
ZoneTriggeredModuleMixin.leaveZoneCallback = DoNothing;
function ZoneTriggeredModuleMixin:IsZoneValid(uiMapID)
return self.validMaps[uiMapID]
end
function ZoneTriggeredModuleMixin:SetValidZones(...)
self.validMaps = {};
local map;
for i = 1, select("#", ...) do
map = select(i, ...);
if type(map) == "table" then
for _, uiMapID in ipairs(map) do
self.validMaps[uiMapID] = true;
end
else
self.validMaps[map] = true;
end
end
end
function ZoneTriggeredModuleMixin:PlayerEnterZone(mapID)
if not self.inZone then
self.inZone = true;
self.enterZoneCallback(mapID);
end
if mapID ~= self.currentMapID then
self.currentMapID = mapID;
self:OnCurrentMapChanged(mapID);
end
end
function ZoneTriggeredModuleMixin:PlayerLeaveZone()
if self.inZone then
self.inZone = false;
self.currentMapID = nil;
self.leaveZoneCallback();
end
end
function ZoneTriggeredModuleMixin:SetEnterZoneCallback(callback)
self.enterZoneCallback = callback;
end
function ZoneTriggeredModuleMixin:SetLeaveZoneCallback(callback)
self.leaveZoneCallback = callback;
end
function ZoneTriggeredModuleMixin:OnCurrentMapChanged(newMapID)
end
function ZoneTriggeredModuleMixin:SetEnabled(state)
self.enabled = state or false;
if not self.enabled then
self:PlayerLeaveZone();
end
end
function ZoneTriggeredModuleMixin:Update()
if not self.enabled then return end;
local mapID = GetBestMapForUnit("player");
if self:IsZoneValid(mapID) then
self:PlayerEnterZone(mapID);
else
self:PlayerLeaveZone();
end
end
local function AddZoneModules(module)
if not controller then
controller = CreateFrame("Frame");
modules = {};
total = 0;
controller:SetScript("OnEvent", function(f, event, ...)
local mapID = GetBestMapForUnit("player");
if mapID and mapID ~= lastMapID then
lastMapID = mapID;
else
return
end
for i = 1, total do
if modules[i].enabled then
if modules[i]:IsZoneValid(mapID) then
modules[i]:PlayerEnterZone(mapID);
else
modules[i]:PlayerLeaveZone();
end
end
end
end);
controller:RegisterEvent("ZONE_CHANGED_NEW_AREA");
controller:RegisterEvent("PLAYER_ENTERING_WORLD");
end
table.insert(modules, module);
total = total + 1;
end
local function CreateZoneTriggeredModule(tag)
local module = {
tag = tag,
validMaps = {},
};
for k, v in pairs(ZoneTriggeredModuleMixin) do
module[k] = v;
end
AddZoneModules(module);
return module
end
API.CreateZoneTriggeredModule = CreateZoneTriggeredModule;
--Get Player Coord Less RAM cost
local UnitPosition = UnitPosition;
local GetPlayerMapPosition = C_Map.GetPlayerMapPosition;
local _posY, _posX, _data;
local lastUiMapID;
local MapData = {};
local function CacheMapData(uiMapID)
if MapData[uiMapID] then return end;
local instance, topLeft = C_Map.GetWorldPosFromMapPos(uiMapID, {x=0, y=0});
local width, height = C_Map.GetMapWorldSize(uiMapID);
if topLeft then
local top, left = topLeft:GetXY()
MapData[uiMapID] = {width, height, left, top};
end
end
local function GetPlayerMapCoord_Fallback(uiMapID)
local position = GetPlayerMapPosition(uiMapID, "player");
if position then
return position.x, position.Y
end
end
local function GetPlayerMapCoord(uiMapID)
_posY, _posX = UnitPosition("player");
if not (_posX and _posY) then return GetPlayerMapCoord_Fallback(uiMapID) end;
if uiMapID ~= lastUiMapID then
lastUiMapID = uiMapID;
CacheMapData(uiMapID);
end
_data = MapData[uiMapID]
if not _data or _data[1] == 0 or _data[2] == 0 then return GetPlayerMapCoord_Fallback(uiMapID) end;
return (_data[3] - _posX) / _data[1], (_data[4] - _posY) / _data[2]
end
API.GetPlayerMapCoord = GetPlayerMapCoord;
local function ConvertMapPositionToContinentPosition(uiMapID, x, y, poiID)
local info = GetMapInfo(uiMapID);
if not info then return end;
local continentMapID; --uiMapID
while info do
if info.mapType == Enum.UIMapType.Continent then
continentMapID = info.mapID;
break
elseif info.parentMapID then
info = GetMapInfo(info.parentMapID);
else
return
end
end
if not continentMapID then
print(string.format("Map %s doesn't belong to any continent.", uiMapID));
end
local point = {
uiMapID = uiMapID,
position = CreateVector2D(x, y);
};
C_Map.SetUserWaypoint(point);
C_Timer.After(0, function()
local posVector = C_Map.GetUserWaypointPositionForMap(continentMapID);
if posVector then
x, y = posVector:GetXY();
print(continentMapID, x, y);
if not PlumberDevData then
PlumberDevData = {};
end
if not PlumberDevData.POIPositions then
PlumberDevData.POIPositions = {};
end
if poiID then
x = floor(x*10000 + 0.5)/10000;
y = floor(y*10000 + 0.5)/10000;
PlumberDevData.POIPositions[poiID] = {
id = poiID,
mapID = uiMapID,
continent = continentMapID,
cx = x,
cy = y,
};
end
C_Map.ClearUserWaypoint();
else
print("No user waypoint found.")
end
end);
end
API.ConvertMapPositionToContinentPosition = ConvertMapPositionToContinentPosition;
--Calculate a list of map positions (cache data) and run callback
local Converter;
local function Converter_OnUpdate(self, elapsed)
self.t = self.t + elapsed;
if self.t > self.delay then
if self.t > 1 then --The delay is always much shorter than 1s, thie line is to prevent error looping
self.t = nil;
self:SetScript("OnUpdate", nil);
return
end
self.t = 0;
else
return
end
self.index = self.index + 1;
if self.calls[self.index] then
self.calls[self.index]();
else
self:SetScript("OnUpdate", nil);
self.t = nil;
self.calls = nil;
self.index = nil;
self.oldWaypoint = nil;
if self.onFinished then
self.onFinished();
self.onFinished = nil;
end
end
end
local function ConvertAndCacheMapPositions(positions, onCoordReceivedFunc, onFinishedFunc)
if not Converter then
Converter = CreateFrame("Frame");
end
local MAPTYPE_CONTINENT = Enum.UIMapType.Continent;
if not MAPTYPE_CONTINENT then
print("Plumber WoW API Changed");
return
end
local calls, n, oldWaypoint;
if Converter.t then
--still processing
calls = Converter.calls;
n = #calls;
oldWaypoint = Converter.oldWaypoint;
else
calls = {};
n = 0;
oldWaypoint = C_Map.GetUserWaypoint();
Converter.oldWaypoint = oldWaypoint;
Converter.index = 0;
end
for _, data in ipairs(positions) do
local info = GetMapInfo(data.uiMapID);
if info then
local continentMapID; --uiMapID
while info do
if info.mapType == MAPTYPE_CONTINENT then
continentMapID = info.mapID;
break
elseif info.parentMapID then
info = GetMapInfo(info.parentMapID);
else
info = nil;
end
end
if continentMapID then
local uiMapID = data.uiMapID;
local poiID = data.poiID;
local point = {
uiMapID = uiMapID,
position = CreateVector2D(data.x, data.y);
};
n = n + 1;
local function SetWaypoint()
C_Map.SetUserWaypoint(point);
Converter.t = 0;
end
calls[n] = SetWaypoint;
n = n + 1;
local function ProcessWaypoint()
local posVector = C_Map.GetUserWaypointPositionForMap(continentMapID);
if posVector then
local x, y = posVector:GetXY();
local positionData = {
uiMapID = uiMapID,
continent = continentMapID,
x = x,
y = y,
poiID = poiID,
};
onCoordReceivedFunc(positionData)
C_Map.ClearUserWaypoint();
end
Converter.t = 0.033;
end
calls[n] = ProcessWaypoint;
end
end
end
Converter.onFinished = function()
if Converter.oldWaypoint then
C_Map.SetUserWaypoint(oldWaypoint);
Converter.oldWaypoint = nil;
end
if onFinishedFunc then
onFinishedFunc();
end
end
Converter.calls = calls;
Converter.t = 0;
Converter.delay = -0.1;
Converter:SetScript("OnUpdate", Converter_OnUpdate);
return true
end
API.ConvertAndCacheMapPositions = ConvertAndCacheMapPositions;
--[[
function YeetPos()
local uiMapID = C_Map.GetBestMapForUnit("player");
local x, y = GetPlayerMapCoord(uiMapID);
print(x, y);
local position = C_Map.GetPlayerMapPosition(uiMapID, "player");
local x0, y0 = position:GetXY();
print(x0, y0);
end
--]]
local MARGIN_X = 0.02;
local MARGIN_Y = MARGIN_X * 1.42;
local function AreWaypointsClose(userX, userY, preciseX, preciseY)
--Examine if the left coords (user set) is roughly the same as the precise position
--We don't calculate the exact distance (e.g. in yards)
--We assume the user waypoint falls into a square around around their target, cuz manually placed pin cannot reach that precision
--The margin of Y is larger than that of X, due to map ratio
return (userX > preciseX - MARGIN_X) and (userX < preciseX + MARGIN_X) and (userY > preciseY - MARGIN_Y) and (userY < preciseY + MARGIN_Y)
end
API.AreWaypointsClose = AreWaypointsClose;
local MAP_PIN_HYPERLINK = MAP_PIN_HYPERLINK or "|A:Waypoint-MapPin-ChatIcon:13:13:0:0|a Map Pin Location";
local FORMAT_USER_WAYPOINT = "|cffffff00|Hworldmap:%d:%.0f:%.0f|h["..MAP_PIN_HYPERLINK.."]|h|r"; --Message will be blocked by the server if you changing the map pin's name
local function CreateWaypointHyperlink(uiMapID, normalizedX, normalizedY)
if uiMapID and normalizedX and normalizedY then
return format(FORMAT_USER_WAYPOINT, uiMapID, 10000*normalizedX, 10000*normalizedY);
end
end
API.CreateWaypointHyperlink = CreateWaypointHyperlink;
local function GetZoneName(areaID)
return C_Map.GetAreaInfo(areaID) or ("Area:"..areaID)
end
API.GetZoneName = GetZoneName;
local HasActiveDelve = C_DelvesUI and C_DelvesUI.HasActiveDelve or Nop;
local function IsInDelves()
--See Blizzard InstanceDifficulty.lua
local _, _, _, mapID = UnitPosition("player");
return HasActiveDelve(mapID);
end
API.IsInDelves = IsInDelves;
end
do --Instance --Map
local GetInstanceInfo = GetInstanceInfo;
local function GetMapID()
local instanceID = select(8, GetInstanceInfo());
return instanceID
end
API.GetMapID = GetMapID;
end
do --Pixel
local GetPhysicalScreenSize = GetPhysicalScreenSize;
local function GetPixelForScale(scale, pixelSize)
local SCREEN_WIDTH, SCREEN_HEIGHT = GetPhysicalScreenSize();
if pixelSize then
return pixelSize * (768/SCREEN_HEIGHT)/scale
else
return (768/SCREEN_HEIGHT)/scale
end
end
API.GetPixelForScale = GetPixelForScale;
local function GetPixelForWidget(widget, pixelSize)
local scale = widget:GetEffectiveScale();
return GetPixelForScale(scale, pixelSize);
end
API.GetPixelForWidget = GetPixelForWidget;
end
do --Easing
local EasingFunctions = {};
addon.EasingFunctions = EasingFunctions;
local sin = math.sin;
local cos = math.cos;
local pow = math.pow;
local pi = math.pi;
--t: total time elapsed
--b: beginning position
--e: ending position
--d: animation duration
function EasingFunctions.linear(t, b, e, d)
return (e - b) * t / d + b
end
function EasingFunctions.outSine(t, b, e, d)
return (e - b) * sin(t / d * (pi / 2)) + b
end
function EasingFunctions.inOutSine(t, b, e, d)
return -(e - b) / 2 * (cos(pi * t / d) - 1) + b
end
function EasingFunctions.outQuart(t, b, e, d)
t = t / d - 1;
return (b - e) * (pow(t, 4) - 1) + b
end
function EasingFunctions.outQuint(t, b, e, d)
t = t / d
return (b - e)* (pow(1 - t, 5) - 1) + b
end
function EasingFunctions.inQuad(t, b, e, d)
t = t / d
return (e - b) * pow(t, 2) + b
end
end
do --Currency
local GetCurrencyInfo = C_CurrencyInfo.GetCurrencyInfo;
local CurrencyDataProvider = CreateFrame("Frame");
CurrencyDataProvider.cache = {};
CurrencyDataProvider.icons = {};
local RelevantKeys = {"name", "quantity", "iconFileID", "maxQuantity", "quality"};
CurrencyDataProvider:SetScript("OnEvent", function(self, event, currencyID, quantity, quantityChange)
if currencyID and self.cache[currencyID] then
self.cache[currencyID] = nil;
end
end);
function CurrencyDataProvider:CacheAndGetCurrencyInfo(currencyID)
if not self.cache[currencyID] then
local info = GetCurrencyInfo(currencyID);
if not info then return end;
local vital = {};
end
if not self.registered then
self.registered = true;
self:RegisterEvent("CURRENCY_DISPLAY_UPDATE");
end
return self.cache[currencyID]
end
function CurrencyDataProvider:GetIcon(currencyID)
if not self.icons[currencyID] then
self:CacheAndGetCurrencyInfo(currencyID);
end
end
end
do --Chat Message
--Check if the a rare spawn info has been announced by other players
local function SearchChatHistory(searchFunc)
local pool = ChatFrame1 and ChatFrame1.fontStringPool;
if pool then
local text, uiMapID, x, y;
for fontString in pool:EnumerateActive() do
text = fontString:GetText();
if text ~= nil then
if searchFunc(text) then
return true
end
end
end
end
return false
end
API.SearchChatHistory = SearchChatHistory;
end
do --Cursor Position
local UI_SCALE_RATIO = 1;
local UIParent = UIParent;
local EL = CreateFrame("Frame");
local GetCursorPosition = GetCursorPosition;
EL:RegisterEvent("UI_SCALE_CHANGED");
EL:SetScript("OnEvent", function(self, event)
UI_SCALE_RATIO = 1 / UIParent:GetEffectiveScale();
end);
local function GetScaledCursorPosition()
local x, y = GetCursorPosition();
return x*UI_SCALE_RATIO, y*UI_SCALE_RATIO
end
API.GetScaledCursorPosition = GetScaledCursorPosition;
end
do --TomTom Compatibility
local TomTomUtil = {};
addon.TomTomUtil = TomTomUtil;
TomTomUtil.waypointUIDs = {};
TomTomUtil.pauseCrazyArrowUpdate = false;
local TT;
function TomTomUtil:IsTomTomAvailable()
if self.available == nil then
self.available = (TomTom and TomTom.AddWaypoint and TomTom.RemoveWaypoint and TomTom.SetClosestWaypoint and TomTomCrazyArrow and true) or false
if self.available then
TT = TomTom;
end
end
return self.available
end
function TomTomUtil:AddWaypoint(uiMapID, x, y, desc, plumberTag, plumberArg1, plumberArg2)
--x, y: 0-1
if self:IsTomTomAvailable() then
plumberTag = plumberTag or "plumber";
local opts = {
title = desc or "TomTom Waypoint via Plumber",
from = "Plumber",
persistent = false, --waypoint will not be saved
crazy = true,
cleardistance = 8,
arrivaldistance = 15,
world = false, --don't show on WorldMap
minimap = false,
plumberTag = plumberTag,
plumberArg1 = plumberArg1,
plumberArg2 = plumberArg2,
};
local uid = securecallfunction(TT.AddWaypoint, TT, uiMapID, x, y, opts);
if uid then
if not self.waypointUIDs[uid] then
self.waypointUIDs[uid] = {plumberTag, plumberArg1, plumberArg2};
end
return uid
else
return
end
end
end
function TomTomUtil:SelectClosestWaypoint()
local announceInChat = false;
securecallfunction(TT.SetClosestWaypoint, TT, announceInChat);
end
function TomTomUtil:RemoveWaypoint(uid)
if self:IsTomTomAvailable() then
securecallfunction(TT.RemoveWaypoint, TT, uid);
end
end
function TomTomUtil:RemoveWaypointsByTag(tag)
for uid, data in pairs(self.waypointUIDs) do
if data[1] == tag then
self.waypointUIDs[uid] = nil;
self:RemoveWaypoint(uid);
end
end
end
function TomTomUtil:RemoveWaypointsByRule(rule)
for uid, data in pairs(self.waypointUIDs) do
if rule(unpack(data)) then
self.waypointUIDs[uid] = nil;
self:RemoveWaypoint(uid);
end
end
end
function TomTomUtil:RemoveAllPlumberWaypoints()
for uid, tag in pairs(self.waypointUIDs) do
self:RemoveWaypoint(uid);
end
self.waypointUIDs = {};
end
function TomTomUtil:GetDistanceToWaypoint(uid)
return securecallfunction(TT.GetDistanceToWaypoint, TT, uid)
end
end
do --Game UI
local function IsInEditMode()
return EditModeManagerFrame and EditModeManagerFrame:IsShown();
end
API.IsInEditMode = IsInEditMode;
end
do --Reputation
local GetFriendshipReputation = C_GossipInfo.GetFriendshipReputation;
local GetFriendshipReputationRanks = C_GossipInfo.GetFriendshipReputationRanks;
local GetFactionParagonInfo = C_Reputation.GetFactionParagonInfo;
local function GetFriendshipProgress(factionID)
local repInfo = factionID and GetFriendshipReputation(factionID);
if repInfo and repInfo.friendshipFactionID and repInfo.friendshipFactionID > 0 then
local currentValue, maxValue;
if repInfo.nextThreshold then
currentValue = repInfo.standing - repInfo.reactionThreshold;
maxValue = repInfo.nextThreshold - repInfo.reactionThreshold;
if maxValue == 0 then
currentValue = 1;
maxValue = 1;
end
else
currentValue = 1;
maxValue = 1;
end
local rankInfo = GetFriendshipReputationRanks(repInfo.friendshipFactionID);
local level = rankInfo.currentLevel;
local isFull = level >= rankInfo.maxLevel;
return level, isFull, currentValue, maxValue
end
end
API.GetFriendshipProgress = GetFriendshipProgress;
local function GetParagonValuesAndLevel(factionID)
local totalEarned, threshold = GetFactionParagonInfo(factionID);
if totalEarned and threshold and threshold ~= 0 then
local paragonLevel = floor(totalEarned / threshold); --How many times the player has reached paragon
local currentValue = totalEarned - paragonLevel * threshold;
return currentValue, threshold, paragonLevel
end
return 0, 1, 0
end
API.GetParagonValuesAndLevel = GetParagonValuesAndLevel;
end
do --Spell
if IS_TWW then
local GetSpellInfo_Table = C_Spell.GetSpellInfo;
local SPELL_INFO_KEYS = {"name", "rank", "iconID", "castTime", "minRange", "maxRange", "spellID", "originalIconID"};
local function GetSpellInfo_Flat(spellID)
local info = spellID and GetSpellInfo_Table(spellID);
if info then
local tbl = {};
local n = 0;
for _, key in ipairs(SPELL_INFO_KEYS) do
n = n + 1;
tbl[n] = info[key];
end
return unpack(tbl)
end
end
API.GetSpellInfo = GetSpellInfo_Flat;
else
API.GetSpellInfo = GetSpellInfo;
end
end
do --System
if IS_TWW then
local GetMouseFoci = GetMouseFoci;
local function GetMouseFocus()
local objects = GetMouseFoci();
return objects and objects[1]
end
API.GetMouseFocus = GetMouseFocus;
else
API.GetMouseFocus = GetMouseFocus;
end
end
do --Player
local function GetPlayerMaxLevel()
local serverExpansionLevel = GetServerExpansionLevel();
local maxLevel = GetMaxLevelForExpansionLevel(serverExpansionLevel);
return maxLevel or 80
end
API.GetPlayerMaxLevel = GetPlayerMaxLevel;
local function IsPlayerAtMaxLevel()
local maxLevel = GetPlayerMaxLevel();
local playerLevel = UnitLevel("player");
return playerLevel >= maxLevel
end
API.IsPlayerAtMaxLevel = IsPlayerAtMaxLevel;
end
do --Scenario
--[[
local SCENARIO_DELVES = addon.L["Scenario Delves"] or "Delves";
local GetScenarioInfo = C_ScenarioInfo.GetScenarioInfo;
local function IsInDelves()
local scenarioInfo = GetScenarioInfo();
return scenarioInfo and scenarioInfo.name == SCENARIO_DELVES
end
API.IsInDelves = IsInDelves;
--]]
end
--[[
local DEBUG = CreateFrame("Frame");
DEBUG:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_START", "player");
DEBUG:RegisterUnitEvent("UNIT_SPELLCAST_CHANNEL_STOP", "player");
DEBUG:RegisterUnitEvent("UNIT_SPELLCAST_SUCCEEDED", "player");
DEBUG:SetScript("OnEvent", function(self, event, ...)
print(event);
if event == "UNIT_SPELLCAST_SUCCEEDED" then
local name, text, texture, startTime, endTime, isTradeSkill = UnitChannelInfo("player");
self.endTime = endTime;
elseif event == "UNIT_SPELLCAST_CHANNEL_STOP" then
local t = GetTime();
t = t * 1000;
if self.endTime then
local diff = t - self.endTime;
if diff < 200 and diff > -200 then
print("Natural Complete")
else
print("Interrupted")
end
end
end
end);
--]]