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.

1778 lines
61 KiB

5 years ago
--[[ Transmisson.lua
This file contains all transmission related functionality, e.g. import/export and chat links.
For that it hooks into the chat frame and addon message channels.
This file adds the following API to the WeakAuras object:
DisplayToString(id, forChat)
Converts the display id to a plain text string
DataToString(id)
Converts the display id to a formatted table
SerializeTable(data)
Converts the table data to a formatted table
ShowDisplayTooltip(data, children, icon, icons, import, compressed)
Shows a tooltip frame for an aura, which allows for importing if import is true
Import(str, [target])
Imports an aura from a table, which may or may not be encoded as a B64 string.
If target is installed data, or is a uid which points to installed data, then the import will be an update to that aura
]]--
if not WeakAuras.IsCorrectVersion() then return end
local AddonName, Private = ...
-- Lua APIs
local tinsert = table.insert
local tostring, string_char, strsplit = tostring, string.char, strsplit
local pairs, type, unpack = pairs, type, unpack
local error = error
local bit_band, bit_lshift, bit_rshift = bit.band, bit.lshift, bit.rshift
local coroutine = coroutine
local WeakAuras = WeakAuras;
local L = WeakAuras.L;
local versionString = WeakAuras.versionString;
local regionOptions = WeakAuras.regionOptions;
local regionTypes = WeakAuras.regionTypes;
-- Local functions
local decodeB64, GenerateUniqueID
local CompressDisplay, ShowTooltip, TableToString, StringToTable
local RequestDisplay, TransmitError, TransmitDisplay
local bytetoB64 = {
[0]="a","b","c","d","e","f","g","h",
"i","j","k","l","m","n","o","p",
"q","r","s","t","u","v","w","x",
"y","z","A","B","C","D","E","F",
"G","H","I","J","K","L","M","N",
"O","P","Q","R","S","T","U","V",
"W","X","Y","Z","0","1","2","3",
"4","5","6","7","8","9","(",")"
}
local B64tobyte = {
a = 0, b = 1, c = 2, d = 3, e = 4, f = 5, g = 6, h = 7,
i = 8, j = 9, k = 10, l = 11, m = 12, n = 13, o = 14, p = 15,
q = 16, r = 17, s = 18, t = 19, u = 20, v = 21, w = 22, x = 23,
y = 24, z = 25, A = 26, B = 27, C = 28, D = 29, E = 30, F = 31,
G = 32, H = 33, I = 34, J = 35, K = 36, L = 37, M = 38, N = 39,
O = 40, P = 41, Q = 42, R = 43, S = 44, T = 45, U = 46, V = 47,
W = 48, X = 49, Y = 50, Z = 51,["0"]=52,["1"]=53,["2"]=54,["3"]=55,
["4"]=56,["5"]=57,["6"]=58,["7"]=59,["8"]=60,["9"]=61,["("]=62,[")"]=63
}
-- This code is based on the Encode7Bit algorithm from LibCompress
-- Credit goes to Galmok (galmok@gmail.com)
local decodeB64Table = {}
function decodeB64(str)
local bit8 = decodeB64Table;
local decoded_size = 0;
local ch;
local i = 1;
local bitfield_len = 0;
local bitfield = 0;
local l = #str;
while true do
if bitfield_len >= 8 then
decoded_size = decoded_size + 1;
bit8[decoded_size] = string_char(bit_band(bitfield, 255));
bitfield = bit_rshift(bitfield, 8);
bitfield_len = bitfield_len - 8;
end
ch = B64tobyte[str:sub(i, i)];
bitfield = bitfield + bit_lshift(ch or 0, bitfield_len);
bitfield_len = bitfield_len + 6;
if i > l then
break;
end
i = i + 1;
end
return table.concat(bit8, "", 1, decoded_size)
end
function GenerateUniqueID()
-- generates a unique random 11 digit number in base64
local s = {}
for i=1,11 do
tinsert(s, bytetoB64[math.random(0, 63)])
end
return table.concat(s)
end
WeakAuras.GenerateUniqueID = GenerateUniqueID
local function stripNonTransmissableFields(datum, fieldMap)
for k, v in pairs(fieldMap) do
if type(v) == "table" and type(datum[k]) == "table" then
stripNonTransmissableFields(datum[k], v)
elseif v == true then
datum[k] = nil
end
end
end
function CompressDisplay(data)
-- Clean up custom trigger fields that are unused
-- Those can contain lots of unnecessary data.
-- Also we warn about any custom code, so removing unnecessary
-- custom code prevents unnecessary warnings
for triggernum, triggerData in ipairs(data.triggers) do
local trigger, untrigger = triggerData.trigger, triggerData.untrigger
if (trigger and trigger.type ~= "custom") then
trigger.custom = nil;
trigger.customDuration = nil;
trigger.customName = nil;
trigger.customIcon = nil;
trigger.customTexture = nil;
trigger.customStacks = nil;
if (untrigger) then
untrigger.custom = nil;
end
end
end
local copiedData = CopyTable(data)
stripNonTransmissableFields(copiedData, Private.non_transmissable_fields)
copiedData.tocversion = WeakAuras.BuildInfo
return copiedData;
end
local function filterFunc(_, event, msg, player, l, cs, t, flag, channelId, ...)
if flag == "GM" or flag == "DEV" or (event == "CHAT_MSG_CHANNEL" and type(channelId) == "number" and channelId > 0) then
return
end
local newMsg = "";
local remaining = msg;
local done;
repeat
local start, finish, characterName, displayName = remaining:find("%[WeakAuras: ([^%s]+) %- ([^%]]+)%]");
if(characterName and displayName) then
characterName = characterName:gsub("|c[Ff][Ff]......", ""):gsub("|r", "");
displayName = displayName:gsub("|c[Ff][Ff]......", ""):gsub("|r", "");
newMsg = newMsg..remaining:sub(1, start-1);
newMsg = newMsg.."|Hgarrmission:weakauras|h|cFF8800FF["..characterName.." |r|cFF8800FF- "..displayName.."]|h|r";
remaining = remaining:sub(finish + 1);
else
done = true;
end
until(done)
if newMsg ~= "" then
local trimmedPlayer = Ambiguate(player, "none")
if event == "CHAT_MSG_WHISPER" and not UnitInRaid(trimmedPlayer) and not UnitInParty(trimmedPlayer) then -- XXX: Need a guild check
local _, num = BNGetNumFriends()
for i=1, num do
if C_BattleNet then -- introduced in 8.2.5 PTR
local toon = C_BattleNet.GetFriendNumGameAccounts(i)
for j=1, toon do
local gameAccountInfo = C_BattleNet.GetFriendGameAccountInfo(i, j);
if gameAccountInfo.characterName == trimmedPlayer and gameAccountInfo.clientProgram == "WoW" then
return false, newMsg, player, l, cs, t, flag, channelId, ...; -- Player is a real id friend, allow it
end
end
else -- keep old method for 8.2 and Classic
local toon = BNGetNumFriendGameAccounts(i)
for j=1, toon do
local _, rName, rGame = BNGetFriendGameAccountInfo(i, j)
if rName == trimmedPlayer and rGame == "WoW" then
return false, newMsg, player, l, cs, t, flag, channelId, ...; -- Player is a real id friend, allow it
end
end
end
end
return true -- Filter strangers
else
return false, newMsg, player, l, cs, t, flag, channelId, ...;
end
end
end
ChatFrame_AddMessageEventFilter("CHAT_MSG_CHANNEL", filterFunc)
ChatFrame_AddMessageEventFilter("CHAT_MSG_YELL", filterFunc)
ChatFrame_AddMessageEventFilter("CHAT_MSG_GUILD", filterFunc)
ChatFrame_AddMessageEventFilter("CHAT_MSG_OFFICER", filterFunc)
ChatFrame_AddMessageEventFilter("CHAT_MSG_PARTY", filterFunc)
ChatFrame_AddMessageEventFilter("CHAT_MSG_PARTY_LEADER", filterFunc)
ChatFrame_AddMessageEventFilter("CHAT_MSG_RAID", filterFunc)
ChatFrame_AddMessageEventFilter("CHAT_MSG_RAID_LEADER", filterFunc)
ChatFrame_AddMessageEventFilter("CHAT_MSG_SAY", filterFunc)
ChatFrame_AddMessageEventFilter("CHAT_MSG_WHISPER", filterFunc)
ChatFrame_AddMessageEventFilter("CHAT_MSG_WHISPER_INFORM", filterFunc)
ChatFrame_AddMessageEventFilter("CHAT_MSG_BN_WHISPER", filterFunc)
ChatFrame_AddMessageEventFilter("CHAT_MSG_BN_WHISPER_INFORM", filterFunc)
ChatFrame_AddMessageEventFilter("CHAT_MSG_INSTANCE_CHAT", filterFunc)
ChatFrame_AddMessageEventFilter("CHAT_MSG_INSTANCE_CHAT_LEADER", filterFunc)
local buttonAnchor = CreateFrame("FRAME", "WeakAurasTooltipAnchor", ItemRefTooltip)
buttonAnchor:SetAllPoints()
buttonAnchor:Hide()
buttonAnchor:RegisterEvent("PLAYER_REGEN_ENABLED")
buttonAnchor:RegisterEvent("PLAYER_REGEN_DISABLED")
local thumbnailAnchor = CreateFrame("FRAME", "WeakAurasTooltipThumbnailFrame", buttonAnchor)
thumbnailAnchor:SetSize(40,40)
thumbnailAnchor:SetPoint("TOPRIGHT", -27, -8)
local importButton = CreateFrame("BUTTON", "WeakAurasTooltipImportButton", buttonAnchor, "UIPanelButtonTemplate")
importButton:SetPoint("BOTTOMRIGHT", -8, 8)
local showCodeButton = CreateFrame("BUTTON", "WeakAurasTooltipImportButton", buttonAnchor, "UIPanelButtonTemplate")
showCodeButton:SetPoint("BOTTOMLEFT", 8, 8)
showCodeButton:SetText(L["Show Code"])
showCodeButton:SetWidth(90)
local urlEditBox = CreateFrame("EDITBOX", "WeakAurasTooltipUrlEditBox", buttonAnchor)
urlEditBox:SetWidth(250)
urlEditBox:SetHeight(34)
urlEditBox:SetFont(STANDARD_TEXT_FONT, 12)
urlEditBox:SetPoint("TOPLEFT", 8, -57)
urlEditBox:SetScript("OnMouseUp", function() urlEditBox:HighlightText() end)
urlEditBox:SetScript("OnChar", function() urlEditBox:SetText(urlEditBox.text) urlEditBox:HighlightText() end)
urlEditBox:SetAutoFocus(false)
local checkButtons, radioButtons, keyToButton, pendingData = {}, {}, {}, {}
for _, key in pairs(Private.internal_fields) do
keyToButton[key] = false
end
for index,category in pairs(Private.update_categories) do
local button = CreateFrame("checkButton", "WeakAurasTooltipCheckButton"..index, buttonAnchor, "ChatConfigCheckButtonTemplate")
for k, v in pairs(category) do
button[k] = v
end
button.text = _G[button:GetName().."Text"]
local point, relativeFrame, relativePoint, xOff, yOff = button.text:GetPoint(1)
xOff = xOff + 4
yOff = yOff - 2
button.text:SetPoint(point, relativeFrame, relativePoint, xOff, yOff)
button.text:SetText(button.label)
button.text:SetTextColor(1, 0.82, 0)
button.func = function()
if button:GetChecked() then
pendingData.buttonsChecked = min(pendingData.buttonsChecked + 1, #checkButtons)
else
pendingData.buttonsChecked = max(pendingData.buttonsChecked - 1, 0)
end
radioButtons[#radioButtons]:Click()
end
checkButtons[index] = button
checkButtons[button.name] = button
for _, key in pairs(category.fields) do
keyToButton[key] = button
end
end
radioButtons = {
CreateFrame("CHECKBUTTON", "WeakAurasTooltipRadioButtonCopy", buttonAnchor, "UIRadioButtonTemplate"),
-- CreateFrame("CHECKBUTTON", "WeakAurasTooltipRadioButtonReplace", buttonAnchor, "UIRadioButtonTemplate"),
CreateFrame("CHECKBUTTON", "WeakAurasTooltipRadioButtonUpdate", buttonAnchor, "UIRadioButtonTemplate"),
}
local radioButtonLabels = {
L["Create a Copy"],
L["Update Auras"]
}
for index, button in ipairs(radioButtons) do
button.text = _G[button:GetName().."Text"]
button.text:SetFontObject("GameFontNormal")
button.text:SetText(radioButtonLabels[index])
button:SetScript("OnClick", function(self)
for _, button in ipairs(radioButtons) do
button:SetChecked(button == self)
end
pendingData.mode = index
Private.RefreshTooltipButtons()
end)
end
-- Ideally, we would not add an __index method to this
-- But that would greatly bloat the update_category for display
-- A better solution would be to refactor the data
-- Such that all of the display tab settings are in their own subtable
setmetatable(keyToButton,{__index = function(_, key) return key and checkButtons.display end,})
local deleted = {} -- magic value
local function recurseUpdate(data, chunk)
for k,v in pairs(chunk) do
if v == deleted then
data[k] = nil
elseif type(v) == 'table' and type(data[k]) == 'table' then
recurseUpdate(data[k], v)
else
data[k] = v
end
end
end
local function Update(data, diff)
-- modifies the installed data such that it matches the pending import, sans user-specified options
-- diff is expected to be output generated by diff
if not diff then
WeakAuras.Add(data)
return
end
if not data then
return
end
recurseUpdate(data, diff)
WeakAuras.Add(data)
return data
end
local function install(data, oldData, patch, mode, isParent)
-- munch the provided data and add, update, or delete as appropriate
-- return the data which the SV knows about afterwards (if there is any)
local installedUID, imported
if mode == 1 then
-- always import as a new thing, set preferToUpdate = false
if data then
imported = CopyTable(data)
data.id = WeakAuras.FindUnusedId(data.id)
WeakAuras.Add(data)
installedUID = data.uid
data.preferToUpdate = false
if oldData then
oldData.preferToUpdate = false
end
else
-- nothing to add
return
end
elseif not oldData then
-- this is a new thing
if not checkButtons.newchildren:GetChecked() then
-- user has chosen to not add new children, so do nothing
return
end
imported = CopyTable(data)
data.id = WeakAuras.FindUnusedId(data.id)
data.preferToUpdate = true
WeakAuras.Add(data)
installedUID = data.uid
elseif not data then
-- this is an old thing
if checkButtons.oldchildren:GetChecked() then
WeakAuras.Delete(oldData)
return
else
-- user has chosen to not delete obsolete auras, so do nothing
return Private.GetDataByUID(oldData.uid)
end
else
-- something to update
if patch then -- if there's no result from Diff, then there's nothing to do
for key in pairs(patch) do
local checkButton = keyToButton[key]
if not isParent and checkButton == checkButtons.anchor then
checkButton = checkButtons.arrangement
end
if checkButton and not checkButton:GetChecked() then
patch[key] = nil
end
end
if patch.id then
patch.id = WeakAuras.FindUnusedId(patch.id)
end
end
WeakAuras.Delete(oldData)
if data.uid and data.uid ~= oldData.uid then
oldData.uid = data.uid
end
Update(oldData, patch)
oldData.preferToUpdate = true
installedUID = oldData.uid
imported = data
end
-- if at this point, then some change has been made in the db. Update History to reflect the change
Private.SetHistory(installedUID, imported, "import")
return Private.GetDataByUID(installedUID)
end
local function importPendingData()
-- get necessary info
local imports, old, diffs = pendingData.newData, pendingData.oldData or {}, pendingData.diffs or {}
local addedChildren, deletedChildren = pendingData.added or 0, pendingData.deleted or 0
local mode = pendingData.mode or 1
local indexMap = pendingData.indexMap
-- cleanup the mess
ItemRefTooltip:Hide()-- this also wipes pendingData via the hook on L521
buttonAnchor:Hide()
if thumbnailAnchor.currentThumbnail then
regionOptions[thumbnailAnchor.currentThumbnailType].releaseThumbnail(thumbnailAnchor.currentThumbnail)
thumbnailAnchor.currentThumbnail = nil
end
if imports and Private.LoadOptions() then
if not WeakAuras.IsOptionsOpen() then
WeakAuras.OpenOptions()
end
else
return
end
WeakAuras.CloseImportExport()
WeakAuras.SetImporting(true)
-- import parent/single aura
local data, oldData, patch = imports[0], old[0], diffs[0]
data.parent = nil
-- handle sortHybridTable
local hybridTables
if (data and data.sortHybridTable) or (mode ~= 1 and oldData and oldData.sortHybridTable) then
hybridTables = {
new = data and data.sortHybridTable or {},
old = mode ~= 1 and oldData and oldData.sortHybridTable or {},
merged = {},
}
end
if data then
data.authorMode = nil
end
if oldData then
oldData.authorMode = nil
end
local installedData = {[0] = install(data, oldData, patch, mode, true)}
WeakAuras.NewDisplayButton(installedData[0])
coroutine.yield()
-- handle children, if there are any
if #imports > 0 then
local parentData = installedData[0]
local preserveOldArrangement = mode ~= 1 and not checkButtons.arrangement:GetChecked()
local map
if not indexMap then
map = {}
else
map = preserveOldArrangement and indexMap.oldToNew or indexMap.newToOld
end
for i = 1, preserveOldArrangement and #old or #imports do
local j = map[i]
if preserveOldArrangement then
oldData = old[i]
if j then
data, patch = imports[j], diffs[j]
else
data, patch = nil, nil
end
else
data, patch = imports[i], diffs[i]
if j then
oldData = old[j]
else
oldData = nil
end
end
local oldID, newID = oldData and oldData.id, data and data.id
if oldData and mode ~= 1 then
oldData.parent = parentData.id
end
if data then
data.parent = parentData.id
data.authorMode = nil
end
if oldData then
oldData.authorMode = nil
end
local childData = install(data, oldData, patch, mode)
if childData then
tinsert(installedData, childData)
if hybridTables then
if preserveOldArrangement then
hybridTables.merged[childData.id] = oldID and hybridTables.old[oldID]
else
hybridTables.merged[childData.id] = newID and hybridTables.new[newID]
end
end
end
coroutine.yield()
end
-- now, the other side of the data may have some children which need to be handled
if mode ~= 1 then
if preserveOldArrangement then
if checkButtons.newchildren:GetChecked() and addedChildren > 0 then
-- we have children which need to be added
local nextInsert, highestInsert, toInsert, newToOld = 1, 0, {}, indexMap.newToOld
for newIndex, data in ipairs(imports) do
local oldIndex = newToOld[newIndex]
if oldIndex then
nextInsert = oldIndex + 1
else
if nextInsert > highestInsert then
highestInsert = nextInsert
end
toInsert[nextInsert] = toInsert[nextInsert] or {}
tinsert(toInsert[nextInsert], data)
end
coroutine.yield()
end
for index = highestInsert, 1, -1 do -- go backwards so that tinsert doesn't screw things up
if toInsert[index] then
for i = #toInsert[index], 1, -1 do
local data = toInsert[index][i]
local id = data.id
data.id = WeakAuras.FindUnusedId(id)
data.parent = parentData.id;
WeakAuras.Add(data)
tinsert(installedData, index, data)
if hybridTables and hybridTables.new[id] then
hybridTables.merged[data.id] = true
end
coroutine.yield()
end
end
end
end
elseif deletedChildren > 0 then
if not checkButtons.oldchildren:GetChecked() then
-- we have existing children which need migrating
-- same algorithm as before, but mirror image
local nextInsert, highestInsert, toInsert, oldToNew = 1, 0, {}, indexMap.oldToNew
for oldIndex, data in ipairs(old) do
local newIndex = oldToNew[oldIndex]
if newIndex then
nextInsert = newIndex + 1
else
if nextInsert > highestInsert then
highestInsert = nextInsert
end
toInsert[nextInsert] = toInsert[nextInsert] or {}
tinsert(toInsert[nextInsert], data)
end
end
coroutine.yield()
for index = highestInsert, 1, -1 do -- go backwards so that tinsert doesn't screw things up
if toInsert[index] then
for i = #toInsert[index], 1, -1 do
local data = toInsert[index][i]
if hybridTables and hybridTables.old[data.id] then
hybridTables.merged[data.id] = true
end
tinsert(installedData, index, data)
end
end
end
coroutine.yield()
else-- we have children which need deleting
local oldToNew = indexMap.oldToNew
for oldIndex, oldData in ipairs(old) do
if not oldToNew[oldIndex] and WeakAuras.GetData(oldData.id) then
WeakAuras.Delete(oldData)
coroutine.yield()
end
end
end
end
end
parentData.controlledChildren = {}
parentData.sortHybridTable = hybridTables and hybridTables.merged
for index, installedChild in ipairs(installedData) do
installedChild.parent = parentData.id
tinsert(parentData.controlledChildren, installedChild.id)
WeakAuras.NewDisplayButton(installedChild)
local childButton = WeakAuras.GetDisplayButton(installedChild.id)
childButton:SetGroup(parentData.id, parentData.regionType == "dynamicgroup")
childButton:SetGroupOrder(index, #parentData.controlledChildren)
coroutine.yield()
end
WeakAuras.Add(parentData);
local button = WeakAuras.GetDisplayButton(parentData.id)
button.callbacks.UpdateExpandButton()
WeakAuras.UpdateGroupOrders(parentData)
WeakAuras.UpdateDisplayButton(parentData)
WeakAuras.ClearAndUpdateOptions(parentData.id)
Private.callbacks:Fire("Import")
end
WeakAuras.SetImporting(false)
return WeakAuras.PickDisplay(installedData[0].id)
end
ItemRefTooltip:HookScript("OnHide", function(self)
buttonAnchor:Hide()
wipe(pendingData)
if (ItemRefTooltip.WeakAuras_Desc_Box) then
ItemRefTooltip.WeakAuras_Desc_Box:Hide();
end
end)
importButton:SetScript("OnClick", function()
Private.dynFrame:AddAction("import", coroutine.create(importPendingData))
end)
showCodeButton:SetScript("OnClick", function()
WeakAuras.OpenOptions()
WeakAuras.OpenCodeReview(pendingData.codes)
end)
local Compresser = LibStub:GetLibrary("LibCompress")
local LibDeflate = LibStub:GetLibrary("LibDeflate")
local Serializer = LibStub:GetLibrary("AceSerializer-3.0")
local LibSerialize = LibStub("LibSerialize")
local Comm = LibStub:GetLibrary("AceComm-3.0")
local configForDeflate = {level = 9} -- the biggest bottleneck by far is in transmission and printing; so use maximal compression
local configForLS = {
errorOnUnserializableType = false
}
local tooltipLoading;
local receivedData;
hooksecurefunc("SetItemRef", function(link, text)
buttonAnchor:Hide()
if(link == "garrmission:weakauras") then
local _, _, characterName, displayName = text:find("|Hgarrmission:weakauras|h|cFF8800FF%[([^%s]+) |r|cFF8800FF%- ([^%]]+)%]|h");
if(characterName and displayName) then
characterName = characterName:gsub("|c[Ff][Ff]......", ""):gsub("|r", "");
displayName = displayName:gsub("|c[Ff][Ff]......", ""):gsub("|r", "");
if(IsShiftKeyDown()) then
local editbox = GetCurrentKeyBoardFocus();
if(editbox) then
editbox:Insert("[WeakAuras: "..characterName.." - "..displayName.."]");
end
else
characterName = characterName:gsub("%.", "")
ShowTooltip({
{2, "WeakAuras", displayName, 0.5, 0, 1, 1, 1, 1},
{1, L["Requesting display information from %s ..."]:format(characterName), 1, 0.82, 0},
{1, L["Note, that cross realm transmission is possible if you are on the same group"], 1, 0.82, 0}
});
tooltipLoading = true;
receivedData = false;
RequestDisplay(characterName, displayName);
WeakAuras.timer:ScheduleTimer(function()
if (tooltipLoading and not receivedData and ItemRefTooltip:IsVisible()) then
ShowTooltip({
{2, "WeakAuras", displayName, 0.5, 0, 1, 1, 1, 1},
{1, L["Error not receiving display information from %s"]:format(characterName), 1, 0, 0},
{1, L["Note, that cross realm transmission is possible if you are on the same group"], 1, 0.82, 0}
})
end
end, 5);
end
else
ShowTooltip({
{1, "WeakAuras", 0.5, 0, 1},
{1, L["Malformed WeakAuras link"], 1, 0, 0}
});
end
end
end);
local compressedTablesCache = {}
function TableToString(inTable, forChat)
local serialized = LibSerialize:SerializeEx(configForLS, inTable)
local compressed
-- get from / add to cache
if compressedTablesCache[serialized] then
compressed = compressedTablesCache[serialized].compressed
compressedTablesCache[serialized].lastAccess = time()
else
compressed = LibDeflate:CompressDeflate(serialized, configForDeflate)
compressedTablesCache[serialized] = {
compressed = compressed,
lastAccess = time(),
}
end
-- remove cache items after 5 minutes
for k, v in pairs(compressedTablesCache) do
if v.lastAccess < (time() - 300) then
compressedTablesCache[k] = nil
end
end
local encoded = "!WA:2!"
if(forChat) then
encoded = encoded .. LibDeflate:EncodeForPrint(compressed)
else
encoded = encoded .. LibDeflate:EncodeForWoWAddonChannel(compressed)
end
return encoded
end
function StringToTable(inString, fromChat)
-- encoding format:
-- version 0: simple b64 string, compressed with LC and serialized with AS
-- version 1: b64 string prepended with "!", compressed with LD and serialized with AS
-- version 2+: b64 string prepended with !WA:N! (where N is encode version)
-- compressed with LD and serialized with LS
local _, _, encodeVersion, encoded = inString:find("^(!WA:%d+!)(.+)$")
if encodeVersion then
encodeVersion = tonumber(encodeVersion:match("%d+"))
else
encoded, encodeVersion = inString:gsub("^%!", "")
end
local decoded
if(fromChat) then
if encodeVersion > 0 then
decoded = LibDeflate:DecodeForPrint(encoded)
else
decoded = decodeB64(encoded)
end
else
decoded = LibDeflate:DecodeForWoWAddonChannel(encoded)
end
if not decoded then
return "Error decoding."
end
local decompressed, errorMsg = nil, "unknown compression method"
if encodeVersion > 0 then
decompressed = LibDeflate:DecompressDeflate(decoded)
else
decompressed, errorMsg = Compresser:Decompress(decoded)
end
if not(decompressed) then
return "Error decompressing: " .. errorMsg
end
local success, deserialized
if encodeVersion < 2 then
success, deserialized = Serializer:Deserialize(decompressed)
else
success, deserialized = LibSerialize:Deserialize(decompressed)
end
if not(success) then
return "Error deserializing "..deserialized
end
return deserialized
end
Private.StringToTable = StringToTable
function Private.DisplayToString(id, forChat)
local data = WeakAuras.GetData(id);
if(data) then
data.uid = data.uid or GenerateUniqueID()
local transmitData = CompressDisplay(data);
local children = data.controlledChildren;
local transmit = {
m = "d",
d = transmitData,
v = 1421, -- Version of Transmisson, won't change anymore.
s = versionString
};
local firstTrigger = data.triggers[1].trigger
if(firstTrigger.type == "aura" and WeakAurasOptionsSaved and WeakAurasOptionsSaved.spellCache) then
transmit.a = {};
for i,v in pairs(firstTrigger.names) do
transmit.a[v] = WeakAuras.spellCache.GetIcon(v);
end
end
if(children) then
transmit.c = {};
local uids = {}
for index,childID in pairs(children) do
local childData = WeakAuras.GetData(childID);
if(childData) then
if childData.uid then
if uids[childData.uid] then
childData.uid = GenerateUniqueID()
else
uids[childData.uid] = true
end
else
childData.uid = GenerateUniqueID()
end
transmit.c[index] = CompressDisplay(childData);
end
end
end
return TableToString(transmit, forChat);
else
return "";
end
end
local function recurseStringify(data, level, lines)
for k, v in pairs(data) do
local lineFormat = strrep(" ", level) .. "[%s] = %s"
local form1, form2, value
local kType, vType = type(k), type(v)
if kType == "string" then
form1 = "%q"
elseif kType == "number" then
form1 = "%d"
else
form1 = "%s"
end
if vType == "string" then
form2 = "%q"
v = v:gsub("\\", "\\\\"):gsub("\n", "\\n"):gsub("\"", "\\\"")
elseif vType == "boolean" then
v = tostring(v)
form2 = "%s"
else
form2 = "%s"
end
lineFormat = lineFormat:format(form1, form2)
if vType == "table" then
tinsert(lines, lineFormat:format(k, "{"))
recurseStringify(v, level + 1, lines)
tinsert(lines, strrep(" ", level) .. "},")
else
tinsert(lines, lineFormat:format(k, v) .. ",")
end
end
end
function Private.DataToString(id)
local data = WeakAuras.GetData(id)
if data then
return Private.SerializeTable(data):gsub("|", "||")
end
end
function Private.SerializeTable(data)
local lines = {"{"}
recurseStringify(data, 1, lines)
tinsert(lines, "}")
return table.concat(lines, "\n")
end
function Private.RefreshTooltipButtons()
importButton:Disable()
importButton.tooltipText = nil
showCodeButton:Enable()
if not pendingData.codes or #pendingData.codes == 0 then
showCodeButton:Hide()
else
showCodeButton:Show()
end
urlEditBox:Enable()
if pendingData.url then
urlEditBox:Show()
urlEditBox.text = pendingData.url
urlEditBox:SetText(pendingData.url)
urlEditBox:HighlightText(0, 0)
else
urlEditBox:Hide()
end
if InCombatLockdown() then
importButton:SetText(L["In Combat"])
importButton.tooltipText = L["Importing is disabled while in combat"]
showCodeButton:Disable()
elseif WeakAuras.IsImporting() then
importButton:SetText(L["Import in progress"])
elseif pendingData.newData then
if pendingData.activeCategories then
if pendingData.mode == 1 then
for button in pairs(pendingData.activeCategories) do
button.text:SetTextColor(.5, .5, .5)
end
else
for button in pairs(pendingData.activeCategories) do
button.text:SetTextColor(1, 0.82, 0)
end
end
end
if pendingData.mode == #radioButtons and pendingData.buttonsChecked == 0 then
importButton:SetText(L["Choose a category"])
else
importButton:Enable()
if pendingData.diffs then
if pendingData.mode ~= 1 then
importButton:SetText(L["Import as Update"])
else
importButton:SetText(L["Import as Copy"])
end
else
if pendingData.mode == 1 then
importButton:SetText(L["Import as Copy"])
elseif #pendingData.newData > 0 then
importButton:SetText(L["Import Group"])
else
importButton:SetText(L["Import"])
end
end
end
end
local importWidth = importButton.Text:GetStringWidth()
importButton:SetWidth(importWidth + 30)
end
buttonAnchor:SetScript("OnEvent", Private.RefreshTooltipButtons)
local function SetCheckButtonStates(radioButtonAnchor, activeCategories)
if activeCategories then
for i, button in ipairs(radioButtons) do
button:Show()
button:Enable()
button:SetChecked(i == pendingData.mode)
button:SetPoint("TOPLEFT", radioButtonAnchor, "TOPLEFT", 0, -20 * (i - .7))
end
local checkButtonAnchor = radioButtons[#radioButtons]
local buttonsChecked, buttonsShown = 0, 0
for i, button in ipairs(checkButtons) do
if activeCategories[button] then
button:Show()
button:Enable()
button:SetPoint("TOPLEFT", checkButtonAnchor, "TOPLEFT", 17, -27*(0.6 + buttonsShown))
button:SetChecked(button.default)
buttonsShown = buttonsShown + 1
buttonsChecked = buttonsChecked + (button:GetChecked() and 1 or 0)
else
button:Hide()
button:Disable()
end
end
pendingData.buttonsChecked = buttonsChecked
else
for _, button in ipairs(radioButtons) do
button:Hide()
button:Disable()
end
for _, button in ipairs(checkButtons) do
button:Disable()
button:Hide()
end
end
end
function ShowTooltip(lines, linesFromTop, activeCategories)
ItemRefTooltip:Show();
if not ItemRefTooltip:IsVisible() then
ItemRefTooltip:SetOwner(UIParent, "ANCHOR_PRESERVE");
end
ItemRefTooltip:ClearLines();
for i, line in ipairs(lines) do
local sides, a1, a2, a3, a4, a5, a6, a7, a8 = unpack(line);
if(sides == 1) then
ItemRefTooltip:AddLine(a1, a2, a3, a4, a5);
elseif(sides == 2) then
ItemRefTooltip:AddDoubleLine(a1, a2, a3, a4, a5, a6, a7, a8);
end
end
ItemRefTooltip:Show()
if pendingData.newData then
local checkButtonAnchor = _G["ItemRefTooltipTextLeft" .. (linesFromTop or 1)]
SetCheckButtonStates(checkButtonAnchor, activeCategories)
Private.RefreshTooltipButtons()
buttonAnchor:Show()
else
buttonAnchor:Hide()
end
end
local function notEmptyString(str)
return str and str ~= "" and string.find(str, "%S")
end
local function addCode(codes, text, code, ...)
-- The 4th paramter is a "check" if the code is active
-- The following line let's distinguish between addCode(a, b, c, nil) and addCode(a, b, c)
if (select("#", ...) > 0) then
if not select(1, ...) then
return
end
end
if code and notEmptyString(code) then
local t = {};
t.text = text;
t.value = text
t.code = code
tinsert(codes, t);
end
end
-- TODO: Should savedvariables data ever be refactored, then shunting the custom scripts
-- into their own special subtable will allow us to simplify the scam check significantly.
local function checkTrigger(codes, id, trigger, untrigger)
if not trigger or trigger.type ~= "custom" then return end;
addCode(codes, L["%s Trigger Function"]:format(id), trigger.custom)
if trigger.custom_type == "stateupdate" then
addCode(codes, L["%s Custom Variables"]:format(id), trigger.customVariables, trigger.custom_type == "stateupdate")
else
addCode(codes, L["%s Untrigger Function"]:format(id), untrigger and untrigger.custom)
addCode(codes, L["%s Duration Function"]:format(id), trigger.customDuration)
addCode(codes, L["%s Name Function"]:format(id), trigger.customName)
addCode(codes, L["%s Icon Function"]:format(id), trigger.customIcon)
addCode(codes, L["%s Texture Function"]:format(id),trigger.customTexture)
addCode(codes, L["%s Stacks Function"]:format(id), trigger.customStacks)
for i = 1, 7 do
local property = "customOverlay" .. i;
addCode(codes, L["%s Overlay Function"]:format(id), trigger[property])
end
end
end
local function checkAnimation(codes, id, a)
if not a or a.type ~= "custom" then return end
addCode(codes, L["%s - Alpha Animation"]:format(id), a.alphaFunc, a.alphaType == "custom" and a.use_alpha)
addCode(codes, L["%s - Translate Animation"]:format(id), a.translateFunc, a.translateType == "custom" and a.use_translate)
addCode(codes, L["%s - Scale Animation"]:format(id), a.scaleFunc, a.scaleType == "custom" and a.use_scale)
addCode(codes, L["%s - Rotate Animation"]:format(id), a.rotateFunc, a.rotateType == "custom" and a.use_rotate)
addCode(codes, L["%s - Color Animation"]:format(id), a.colorFunc, a.colorType == "custom" and a.use_color)
end
local function scamCheck(codes, data)
for i, v in ipairs(data.triggers) do
checkTrigger(codes, L["%s - %i. Trigger"]:format(data.id, i), v.trigger, v.untrigger);
end
addCode(codes, L["%s - Trigger Logic"]:format(data.id), data.triggers.customTriggerLogic, data.triggers.disjunctive == "custom");
addCode(codes, L["%s - Custom Text"]:format(data.id), data.customText)
addCode(codes, L["%s - Custom Anchor"]:format(data.id), data.customAnchor, data.anchorFrameType == "CUSTOM")
if (data.actions) then
if data.actions.init then
addCode(codes, L["%s - Init Action"]:format(data.id), data.actions.init.custom, data.actions.init.do_custom)
end
if data.actions.start then
addCode(codes, L["%s - Start Action"]:format(data.id), data.actions.start.custom, data.actions.start.do_custom)
addCode(codes, L["%s - Start Custom Text"]:format(data.id), data.actions.start.message_custom, data.actions.start.do_message)
end
if data.actions.finish then
addCode(codes, L["%s - Finish Action"]:format(data.id), data.actions.finish.custom, data.actions.finish.do_custom)
addCode(codes, L["%s - Finish Custom Text"]:format(data.id), data.actions.finish.message_custom, data.actions.finish.do_message)
end
end
if (data.animation) then
checkAnimation(codes, L["%s - Start"]:format(data.id), data.animation.start);
checkAnimation(codes, L["%s - Main"]:format(data.id), data.animation.main);
checkAnimation(codes, L["%s - Finish"]:format(data.id), data.animation.finish);
end
addCode(codes, L["%s - Custom Grow"]:format(data.id), data.customGrow, data.regionType == "dynamicgroup" and data.grow == "CUSTOM")
addCode(codes, L["%s - Custom Sort"]:format(data.id), data.customSort, data.regionType == "dynamicgroup" and data.sort == "custom")
addCode(codes, L["%s - Custom Anchor"]:format(data.id), data.customAnchorPerUnit,
data.regionType == "dynamicgroup" and data.grow ~= "CUSTOM" and data.useAnchorPerUnit and data.anchorPerUnit == "CUSTOM")
if (data.conditions) then
for _, condition in ipairs(data.conditions) do
if (condition and condition.changes) then
for _, property in ipairs(condition.changes) do
if ((property.property == "chat" or property.property == "customcode") and type(property.value) == "table" and property.value.custom) then
addCode(codes, L["%s - Condition Custom Chat"]:format(data.id), property.value.custom);
end
end
end
end
end
end
local ignoredForDiffChecking = CreateFromMixins(Private.internal_fields, Private.non_transmissable_fields)
local function recurseDiff(ours, theirs)
local diff, seen, same = {}, {}, true
for key, ourVal in pairs(ours) do
if not ignoredForDiffChecking[key] then
seen[key] = true
local theirVal = theirs[key]
if type(ourVal) == "table" and type(theirVal) == "table" then
local diffVal = recurseDiff(ourVal, theirVal)
if diffVal then
diff[key] = diffVal
same = false
end
elseif ourVal ~= theirVal and -- of course, floating points can be nonequal by less than we could possibly care
not(type(ourVal) == "number" and type(theirVal) == "number" and math.abs(ourVal - theirVal) < 1e-6) then
if (theirVal == nil) then
diff[key] = deleted
else
diff[key] = theirVal;
end
same = false
end
end
end
for key, theirVal in pairs(theirs) do
if not seen[key] and not ignoredForDiffChecking[key] then
diff[key] = theirVal
same = false
end
end
if not same then return diff end
end
-- for debug purposes
local function recurseSerial(lines, depth, chunk)
for k, v in pairs(chunk) do
if v == deleted then
tinsert(lines, string.rep(" ", depth) .. "|cFFFF0000" .. k .. " -> deleted|r")
elseif type(v) == "table" then
tinsert(lines, string.rep(" ", depth) .. k .. " -> {")
recurseSerial(lines, depth + 1, v)
tinsert(lines, string.rep(" ", depth) .. "}")
else
tinsert(lines, string.rep(" ", depth) .. k .. " -> " .. tostring(v))
end
end
end
local function diff(ours, theirs)
-- generates a diff which WeakAuras.Update can use
local debug = false
if not ours or not theirs then return end
local diff = recurseDiff(ours, theirs)
if diff then
if debug then
local lines = {
"==========================",
"Diff detected: ",
"ours: " .. (ours.id or "unknown") .. "," .. tostring(ours.uid),
"theirs: " .. (theirs.id or "unknown") .. "," .. tostring(theirs.uid),
"{",
}
recurseSerial(lines, 1, diff)
tinsert(lines, "}")
tinsert(lines, "==========================")
print(table.concat(lines, "\n"))
end
return diff
end
end
local function findMatch(data, children)
local function isParentMatch(old, new)
if old.parent then return end
if old.uid and new.uid then
if old.uid ~= new.uid then
return
else
if (children == nil) ~= (old.controlledChildren == nil) then
return
end
return true
end
else
if children then
if not old.controlledChildren then return end
return old.id == new.id
else
if old.controlledChildren then return end
return old.id == new.id
end
end
end
local oldParent = WeakAuras.GetData(data.id)
if oldParent and not isParentMatch(oldParent, data) then
oldParent = nil
end
if not oldParent or not isParentMatch(oldParent, data) then
oldParent = nil
if data.uid then
for id, existingData in pairs(WeakAurasSaved.displays) do
if isParentMatch(existingData, data) then
oldParent = existingData
break
end
end
else
return nil
end
end
return oldParent
end
local function MatchInfo(data, children, oldParent)
if oldParent then
-- Either both have children, or both don't have
if type(children) ~= type(oldParent.controlledChildren) then
return nil
end
else
-- match the parent/single aura (if no children)
oldParent = findMatch(data, children)
end
if not oldParent then
return nil
end
-- setup
local info = {
mode = 1,
unmodified = 0,
modified = 0,
added = 0,
deleted = 0,
oldData = {},
newData = {},
indexMap = {
oldToNew = {},
newToOld = {},
},
diffs = {},
activeCategories = {},
}
info.oldData[0] = oldParent
if info.oldData[0].preferToUpdate then
info.mode = 2
end
info.newData[0] = data
info.diffs[0] = diff(oldParent, data)
if info.diffs[0] then
info.modified = info.modified + 1
end
local hybridTables
if oldParent.sortHybridTable or data.sortHybridTable then
hybridTables = {
old = {},
new = {},
}
if oldParent.sortHybridTable then
for id, isHybrid in pairs(oldParent.sortHybridTable) do
hybridTables.old[id] = isHybrid
end
end
if data.sortHybridTable then
for id, isHybrid in pairs(data.sortHybridTable) do
hybridTables.new[id] = isHybrid
end
end
end
if children then
-- setup
info.deleted = #oldParent.controlledChildren
info.added = #children
local UIDtoID, IDtoIndex = {}, {}
for index, oldChildID in ipairs(oldParent.controlledChildren) do
local oldChild = WeakAuras.GetData(oldChildID)
info.oldData[index] = oldChild
IDtoIndex[oldChildID] = index
if oldChild.uid and not UIDtoID[oldChild.uid] then
UIDtoID[oldChild.uid] = oldChildID
end
end
-- confirm uid matches, find tentative matches
local tentativeMatches = {}
for newIndex, newChild in ipairs(children) do
info.newData[newIndex] = newChild
local oldIndex = IDtoIndex[newChild.id]
local matchedID = newChild.uid and UIDtoID[newChild.uid]
if matchedID then
oldIndex = IDtoIndex[matchedID]
info.indexMap.oldToNew[oldIndex] = newIndex
info.indexMap.newToOld[newIndex] = oldIndex
info.deleted = info.deleted - 1
info.added = info.added - 1
if hybridTables then
if (hybridTables.new[newChild.id] and not hybridTables.old[matchedID])
or (not hybridTables.new[newChild.id] and hybridTables.old[matchedID]) then
info.activeCategories[checkButtons.arrangement] = true
hybridTables = nil
else
hybridTables.new[newChild.id] = nil
hybridTables.old[matchedID] = nil
end
end
UIDtoID[newChild.uid] = nil
tentativeMatches[oldIndex] = nil
elseif oldIndex then
tentativeMatches[oldIndex] = newIndex
end
end
-- confirm remaining tentative matches
for oldIndex, newIndex in pairs(tentativeMatches) do
local newChild, oldChild = info.newData[newIndex], info.oldData[oldIndex]
if not newChild.uid or not oldChild.uid or newChild.uid == oldChild.uid then
if hybridTables then
if (hybridTables.new[newChild.id] and not hybridTables.old[oldChild.id])
or (not hybridTables.new[newChild.id] and hybridTables.old[oldChild.id]) then
info.activeCategories[checkButtons.arrangement] = true
hybridTables = nil
else
hybridTables.new[newChild.id] = nil
hybridTables.old[oldChild.id] = nil
end
end
info.indexMap.oldToNew[oldIndex] = newIndex
info.indexMap.newToOld[newIndex] = oldIndex
info.deleted = info.deleted - 1
info.added = info.added - 1
end
end
if hybridTables then
if next(hybridTables.new) ~= nil or next(hybridTables.old) ~= nil then
info.activeCategories[checkButtons.arrangement] = true
end
end
-- detect order mismatch and find diffs
local lastMatch = 0
for newIndex, newChild in ipairs(info.newData) do
local oldIndex = info.indexMap.newToOld[newIndex]
if oldIndex then
local oldChild = info.oldData[oldIndex]
if lastMatch and lastMatch > oldIndex then
lastMatch = nil
info.activeCategories[checkButtons.arrangement] = true
else
lastMatch = oldIndex
end
local childDiff = diff(oldChild, newChild, true)
if childDiff then
info.modified = info.modified + 1
info.diffs[newIndex] = childDiff
end
end
end
end
if info.added ~= 0 then
info.activeCategories[checkButtons.newchildren] = true
end
if info.deleted ~= 0 then
info.activeCategories[checkButtons.oldchildren] = true
end
if info.modified ~= 0 then
for i, diff in pairs(info.diffs) do
for key in pairs(diff) do
local button = keyToButton[key]
if button then
if i > 0 and button == checkButtons.anchor then
button = checkButtons.arrangement
end
info.activeCategories[button] = true
end
end
end
end
local modified = next(info.activeCategories) ~= nil
return modified and info or false
end
local function ShowDisplayTooltip(data, children, matchInfo, icon, icons, import, compressed)
-- since we have new data, wipe the old pending data
wipe(pendingData)
local regionType = data.regionType;
local regionData = regionOptions[regionType or ""]
local displayName = regionData and regionData.displayName or regionType or "";
local tooltip = {
{2, data.id, " ", 0.5333, 0, 1},
{2, displayName, " ", 1, 0.82, 0},
{1, " ", 1, 1, 1}
};
pendingData.codes = {};
local highestVersion = data.internalVersion or 1;
local hasDescription = data.desc and data.desc ~= "";
local hasUrl = data.url and data.url ~= "";
pendingData.url = hasUrl and data.url
local hasVersion = (data.semver and data.semver ~= "") or (data.version and data.version ~= "");
local tocversion = data.tocversion;
if hasDescription or hasUrl or hasVersion then
tinsert(tooltip, {1, " "});
end
if hasUrl then
tinsert(tooltip, {1, " "});
end
if hasDescription then
tinsert(tooltip, {1, "\""..data.desc.."\"", 1, 0.82, 0, 1});
end
if hasVersion then
tinsert(tooltip, {1, L["Version: "] .. (data.semver or data.version), 1, 0.82, 0, 1});
end
-- WeakAuras.GetData needs to be replaced temporarily so that when the subsequent code constructs the thumbnail for
-- the tooltip, it will look to the incoming transmission data for child data. This allows thumbnails of incoming
-- groups to be constructed properly.
local RegularGetData = WeakAuras.GetData
if children then
WeakAuras.GetData = function(id)
if(children) then
for index, childData in pairs(children) do
if(childData.id == id) then
return childData;
end
end
end
end
end
local linesFromTop
if import then
-- import description
pendingData.newData = {[0] = data}
if children then
for index,child in ipairs(children) do
pendingData.newData[index] = child
end
end
--TODO: only scamCheck codes that have actually changed
for i = 0, #pendingData.newData do
scamCheck(pendingData.codes, pendingData.newData[i])
end
if matchInfo ~= nil then
-- we know of at least one match
tinsert(tooltip, {1, " "})
if type(matchInfo) ~= "table" then
-- there is no difference whatsoever
tinsert(tooltip, {1, L["You already have this group/aura. Importing will create a duplicate."], 1, 0.82, 0,})
else
-- load pendingData
for k,v in pairs(matchInfo) do
pendingData[k] = v
end
-- tally up changes
if children and #children > 0 then
tinsert(tooltip, {1, L["This is a modified version of your group, |cff9900FF%s.|r"]:format(pendingData.oldData[0].id), 1, 0.82, 0,})
tinsert(tooltip, {1, " "})
if matchInfo.added ~= 0 then
tinsert(tooltip, {1, L[" • %d auras added"]:format(matchInfo.added), 1, 0.82, 0})
end
if matchInfo.modified ~= 0 then
tinsert(tooltip, {1, L[" • %d auras modified"]:format(matchInfo.modified), 1, 0.82, 0})
end
if matchInfo.deleted ~= 0 then
tinsert(tooltip, {1, L[" • %d auras deleted"]:format(matchInfo.deleted), 1, 0.82, 0})
end
tinsert(tooltip, {1, " "})
tinsert(tooltip, {1, L["What do you want to do?"], 1, 0.82, 0})
tinsert(tooltip, {1, " "})
else
local oldID = type(matchInfo.oldData[0]) == "table" and matchInfo.oldData[0].id or "unknown"
tinsert(tooltip, {1, L["This is a modified version of your aura, |cff9900FF%s.|r"]:format(pendingData.oldData[0].id), 1, 0.82, 0})
tinsert(tooltip, {1, L["What do you want to do?"], 1, 0.82, 0})
tinsert(tooltip, {1, " "})
end
linesFromTop = #tooltip
for _ in ipairs(radioButtons) do
tinsert(tooltip, {1, " "})
end
for _, button in ipairs(checkButtons) do
if pendingData.activeCategories[button] then
tinsert(tooltip, {1, " "})
tinsert(tooltip, {1, " "})
end
end
end
else
if not children and (not data.controlledChildren or #data.controlledChildren == 0) then
tinsert(tooltip, {1, L["No Children"], 1, 1, 1})
for triggernum = 1, min(#data.triggers, 10) do
local trigger = data.triggers[triggernum].trigger
if(trigger) then
if(trigger.type == "aura") then
for index, name in pairs(trigger.names) do
local left = " ";
if(index == 1) then
if(#trigger.names > 0) then
if(#trigger.names > 1) then
left = L["Auras:"];
else
left = L["Aura:"];
end
end
end
if (icons) then
tinsert(tooltip, {2, left, name..(icons[name] and (" |T"..icons[name]..":12:12:0:0:64:64:4:60:4:60|t") or ""), 1, 1, 1, 1, 1, 1});
else
local icon = WeakAuras.spellCache and WeakAuras.spellCache.GetIcon(name) or "Interface\\Icons\\INV_Misc_QuestionMark";
tinsert(tooltip, {2, left, name.." |T"..icon..":12:12:0:0:64:64:4:60:4:60|t", 1, 1, 1, 1, 1, 1});
end
end
elseif(trigger.type == "aura2") then
tinsert(tooltip, {2, L["Trigger:"], L["Aura"], 1, 1, 1, 1, 1, 1});
elseif(Private.category_event_prototype[trigger.type]) then
tinsert(tooltip, {2, L["Trigger:"], (Private.event_prototypes[trigger.event].name or L["Undefined"]), 1, 1, 1, 1, 1, 1});
if(trigger.event == "Combat Log" and trigger.subeventPrefix and trigger.subeventSuffix) then
tinsert(tooltip, {2, L["Message type:"], (Private.subevent_prefix_types[trigger.subeventPrefix] or L["Undefined"]).." "..(Private.subevent_suffix_types[trigger.subeventSuffix] or L["Undefined"]), 1, 1, 1, 1, 1, 1});
end
else
tinsert(tooltip, {2, L["Trigger:"], L["Custom"], 1, 1, 1, 1, 1, 1});
end
end
end
else
tinsert(tooltip, {1, L["Children:"], 1, 1, 1})
local excessChildren = -30
for _, child in pairs(children or data.controlledChildren) do
excessChildren = excessChildren + 1
if children then
highestVersion = max(highestVersion, child.internalVersion or 1)
end
if excessChildren <= 0 then
tinsert(tooltip, {2, " ", child.id, 1, 1, 1, 1, 1, 1})
end
tocversion = tocversion or child.tocversion
end
if excessChildren > 0 then
tinsert(tooltip, {2, " ", "[...]", 1, 1, 1, 1, 1, 1})
tinsert(tooltip, {1, L["%s total auras"]:format(excessChildren + 30), "", 1, 1, 1, 1})
end
end
end
tinsert(tooltip, {1, " "})
if(type(import) == "string" and import ~= "unknown") then
tinsert(tooltip, {2, L["From"]..": "..import, " ", 0, 1, 0})
end
if #pendingData.codes > 0 then
tinsert(tooltip, {1, L["This aura contains custom Lua code."], 1, 0, 0})
tinsert(tooltip, {1, L["Make sure you can trust the person who sent it!"], 1, 0, 0})
end
if (highestVersion > WeakAuras.InternalVersion()) then
tinsert(tooltip, {1, L["This aura was created with a newer version of WeakAuras."], 1, 0, 0})
tinsert(tooltip, {1, L["It might not work correctly with your version!"], 1, 0, 0})
end
if WeakAuras.IsClassic() and (not tocversion or tocversion > 20000) then
tinsert(tooltip, {1, L["This aura was created with the retail version of World of Warcraft."], 1, 0, 0})
tinsert(tooltip, {1, L["It might not work correctly on Classic!"], 1, 0, 0})
elseif tocversion and not WeakAuras.IsClassic() and tocversion < 20000 then
tinsert(tooltip, {1, L["This aura was created with the Classic version of World of Warcraft."], 1, 0, 0})
tinsert(tooltip, {1, L["It might not work correctly on Retail!"], 1, 0, 0})
end
tinsert(tooltip, {2, " ", " ", 0, 1, 0})
tinsert(tooltip, {2, " ", " ", 0, 1, 0})
end
if not IsAddOnLoaded('WeakAurasOptions') then
LoadAddOn('WeakAurasOptions')
end
if thumbnailAnchor.currentThumbnail then
regionOptions[thumbnailAnchor.currentThumbnailType].releaseThumbnail(thumbnailAnchor.currentThumbnail)
thumbnailAnchor.currentThumbnail = nil
end
if regionOptions[regionType] then
local ok, thumbnail = pcall(regionOptions[regionType].acquireThumbnail, thumbnailAnchor, data);
if not ok then
error(string.format("Error creating thumbnail for %s %s", regionType, thumbnail), 2)
end
thumbnailAnchor.currentThumbnail = thumbnail
thumbnailAnchor.currentThumbnailType = regionType
thumbnail:SetAllPoints(thumbnailAnchor);
thumbnail:SetParent(thumbnailAnchor)
if (thumbnail.SetIcon) then
local i;
if(icon) then
i = icon;
end
if (i) then
thumbnail:SetIcon(i);
else
thumbnail:SetIcon();
end
end
end
WeakAuras.GetData = RegularGetData or WeakAuras.GetData
ShowTooltip(tooltip, linesFromTop, matchInfo and matchInfo.activeCategories)
end
function WeakAuras.Import(inData, target)
local data, children, icon, icons
if type(inData) == 'string' then
-- encoded data
local received = StringToTable(inData, true)
if type(received) == 'string' then
-- this is probably an error message from LibDeflate. Display it.
ShowTooltip{
{1, "WeakAuras", 0.5333, 0, 1},
{1, received, 1, 0, 0, 1}
}
return nil, received
elseif received.m == "d" then
data = received.d
children = received.c
icon = received.i
icons = received.a
end
elseif type(inData.d) == 'table' then
data = inData.d
children = inData.c
icon = inData.i
icons = inData.a
end
if type(data) ~= "table" then
return nil, "Invalid import data."
end
local status, msg = true, ""
if type(target) ~= 'nil' then
local targetData
local uid = type(target) == 'table' and target.uid or target
if type(uid) == 'string' then
for _, installedData in pairs(WeakAurasSaved.displays) do
if installedData.uid == uid then
targetData = installedData
break
end
end
end
if not targetData then
status = false
msg = "Invalid update target. Importing as an unknown payload."
target = nil
else
target = targetData
end
end
WeakAuras.PreAdd(data)
if children then
for _, child in ipairs(children) do
WeakAuras.PreAdd(child)
end
end
tooltipLoading = nil;
local matchInfo = MatchInfo(data, children, target)
if matchInfo == nil and target then
return false, "Import data did not match to target"
end
ShowDisplayTooltip(data, children, matchInfo, icon, icons, "unknown")
return status, msg
end
-- backwards compatibility
WeakAuras.ImportString = WeakAuras.Import
local function crossRealmSendCommMessage(prefix, text, target, queueName, callbackFn, callbackArg)
local chattype = "WHISPER"
if target and not UnitIsSameServer(target) then
if UnitInRaid(target) then
chattype = "RAID"
text = ("§§%s:%s"):format(target, text)
elseif UnitInParty(target) then
chattype = "PARTY"
text = ("§§%s:%s"):format(target, text)
end
end
Comm:SendCommMessage(prefix, text, chattype, target, queueName, callbackFn, callbackArg)
end
local safeSenders = {}
function RequestDisplay(characterName, displayName)
safeSenders[characterName] = true
safeSenders[Ambiguate(characterName, "none")] = true
local transmit = {
m = "dR",
d = displayName
};
local transmitString = TableToString(transmit);
crossRealmSendCommMessage("WeakAuras", transmitString, characterName);
end
function TransmitError(errorMsg, characterName)
local transmit = {
m = "dE",
eM = errorMsg
};
crossRealmSendCommMessage("WeakAuras", TableToString(transmit), characterName);
end
function TransmitDisplay(id, characterName)
local encoded = Private.DisplayToString(id);
if(encoded ~= "") then
crossRealmSendCommMessage("WeakAuras", encoded, characterName, "BULK", function(displayName, done, total)
crossRealmSendCommMessage("WeakAurasProg", done.." "..total.." "..displayName, characterName, "ALERT");
end, id);
else
TransmitError("dne", characterName);
end
end
Comm:RegisterComm("WeakAurasProg", function(prefix, message, distribution, sender)
if distribution == "PARTY" or distribution == "RAID" then
local dest, msg = string.match(message, "^§§(.+):(.+)$")
if dest then
local dName, dServer = string.match(dest, "^(.*)-(.*)$")
local myName, myServer = UnitFullName("player")
if myName == dName and myServer == dServer then
message = msg
else
return
end
end
end
if tooltipLoading and ItemRefTooltip:IsVisible() and safeSenders[sender] then
receivedData = true;
local done, total, displayName = strsplit(" ", message, 3)
done = tonumber(done)
total = tonumber(total)
if(done and total and total >= done) then
local red = min(255, (1 - done / total) * 511)
local green = min(255, (done / total) * 511)
ShowTooltip({
{2, "WeakAuras", displayName, 0.5, 0, 1, 1, 1, 1},
{1, L["Receiving display information"]:format(sender), 1, 0.82, 0},
{2, " ", ("|cFF%2x%2x00"):format(red, green)..done.."|cFF00FF00/"..total}
})
end
end
end)
Comm:RegisterComm("WeakAuras", function(prefix, message, distribution, sender)
if distribution == "PARTY" or distribution == "RAID" then
local dest, msg = string.match(message, "^§§([^:]+):(.+)$")
if dest then
local dName, dServer = string.match(dest, "^(.*)-(.*)$")
local myName, myServer = UnitFullName("player")
if myName == dName and myServer == dServer then
message = msg
else
return
end
end
end
local received = StringToTable(message);
if(received and type(received) == "table" and received.m) then
if(received.m == "d") and safeSenders[sender] then
tooltipLoading = nil;
local data, children, icon, icons = received.d, received.c, received.i, received.a
WeakAuras.PreAdd(data)
if children then
for _, child in ipairs(children) do
WeakAuras.PreAdd(child)
end
end
local matchInfo = MatchInfo(data, children)
ShowDisplayTooltip(data, children, matchInfo, icon, icons, sender, true)
elseif(received.m == "dR") then
if(Private.linked and Private.linked[received.d]) then
TransmitDisplay(received.d, sender);
end
elseif(received.m == "dE") then
tooltipLoading = nil;
if(received.eM == "dne") then
ShowTooltip({
{1, "WeakAuras", 0.5333, 0, 1},
{1, L["Requested display does not exist"], 1, 0, 0}
});
elseif(received.eM == "na") then
ShowTooltip({
{1, "WeakAuras", 0.5333, 0, 1},
{1, L["Requested display not authorized"], 1, 0, 0}
});
end
end
elseif(ItemRefTooltip.WeakAuras_Tooltip_Thumbnail and ItemRefTooltip.WeakAuras_Tooltip_Thumbnail:IsVisible()) then
ShowTooltip({
{1, "WeakAuras", 0.5333, 0, 1},
{1, L["Transmission error"], 1, 0, 0}
});
end
end);