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.

558 lines
19 KiB

--[[
]]
local ADDON_NAME, Internal = ...
local L = Internal.L
local External = _G[ADDON_NAME]
local format = string.format
local wipe = table.wipe
local concat = table.concat
local function StringToTable(text)
local stringType = text:sub(1,1)
if stringType == "{" then
local func, err = loadstring("return " .. text, "Import")
if not func then
return false, err
end
setfenv(func, {});
return pcall(func)
else
return false, L["Invalid string"]
end
end
local function TableToString(tbl)
local str = {}
for k,v in pairs(tbl) do
if type(k) == "number" then
k = "[" .. k .. "]"
elseif type(k) ~= "string" then
error(format(L["Invalid key type %s"], type(k)))
end
if type(v) == "table" then
v = TableToString(v)
elseif type(v) == "string" then
v = format("%q", v)
elseif type(v) ~= "number" and type(v) ~= "boolean" then
error(format(L["Invalid value type %s for key %s"], type(v), tostring(k)))
end
str[#str+1] = k .. "=" .. tostring(v)
end
return "{" .. concat(str, ",") .. "}"
end
local Base64Encode, Base64Decode
do
local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
function Base64Encode(data)
return ((data:gsub('.', function(x)
local r,b='',x:byte()
for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end
return r;
end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x)
if (#x < 6) then return '' end
local c=0
for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end
return b:sub(c+1,c+1)
end)..({ '', '==', '=' })[#data%3+1])
end
function Base64Decode(data)
data = string.gsub(data, '[^'..b..'=]', '')
return (data:gsub('.', function(x)
if (x == '=') then return '' end
local r,f='',(b:find(x)-1)
for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
return r;
end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
if (#x ~= 8) then return '' end
local c=0
for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
return string.char(c)
end))
end
end
local function Encode(content, format)
if #format == 1 then
if format == "N" then
return "N" .. content
elseif format == "B" then
return "B" .. Base64Encode(content)
else
error("Unsupported format")
end
elseif #format > 1 then
local f, ormat = format:match("^([A-Z])([A-Z]*)$")
return Encode(Encode(content, ormat), f)
end
end
local function Decode(content, err)
local format, content = content:match("^([A-Z])(.*)$")
if format == "N" then
return true, content
elseif format == "B" then
return Decode(Base64Decode(content))
else
return false, err or L["Unsupported format"]
end
end
local function VerifySource(source, sourceType, version, name, ...)
version = source.version or version
sourceType = source.type or sourceType
name = source.name or name
if not name then
return false, L["Missing name"]
elseif type(name) ~= "string" then
return false, L["Invalid name"]
end
if sourceType == "loadout" then
if version ~= 2 then -- Only supported version
return false, L["Invalid source version"]
end
local specID = source.specID or ...
if not specID or not GetSpecializationInfoByID(specID) then
return false, L["Invalid specialization"]
end
for _,segment in Internal.EnumerateLoadoutSegments() do
if source[segment.id] then
for _,set in ipairs(source[segment.id]) do
local result, err = VerifySource(set, segment.id, 1, name, specID)
if not result then
return result, err
end
end
end
end
else
local segment = Internal.GetLoadoutSegment(sourceType)
if not segment then
return false, L["Invalid import type"]
end
if not segment.verify then
return false, L["Missing verification function"]
end
return segment.verify(source, ...)
end
-- if sourceType == "loadout" then
-- if version ~= 2 then -- Only supported version
-- return false, L["Invalid source version"]
-- end
-- local specID = source.specID or ...
-- if not source.talents and not source.pvptalents and not source.essences then
-- return false, L["Missing value"]
-- end
-- if not specID or not GetSpecializationInfoByID(specID) then
-- return false, L["Invalid specialization"]
-- end
-- local function VerifyType(source, type, version, name, ...)
-- return VerifySource(source[type], type, version, name, ...)
-- end
-- for _,subtype in ipairs({"talents", "pvptalents", "essences"}) do
-- for _,set in ipairs(source[subtype]) do
-- local result, err = VerifySource(set, subtype, 1, name, specID)
-- if not result then
-- return result, err
-- end
-- end
-- end
-- -- if source.talents then
-- -- for _,set in ipairs(source.talents) do
-- -- end
-- -- local result, err = VerifySource(source.talents, "talents", 1, name, specID)
-- -- if not result then
-- -- return result, err
-- -- end
-- -- end
-- -- if source.pvptalents then
-- -- local result, err = VerifySource(source.pvptalents, "pvptalents", 1, name, specID)
-- -- if not result then
-- -- return result, err
-- -- end
-- -- end
-- -- if source.essences then
-- -- local result, err = VerifySource(source.essences, "essences", 1, name, GetSpecializationRoleByID(specID))
-- -- if not result then
-- -- return result, err
-- -- end
-- -- end
-- elseif sourceType == "talents" then
-- if version ~= 1 then -- Only supported version
-- return false, L["Invalid source version"]
-- end
-- local specID = source.specID or ...
-- if type(source.talents) ~= "table" then
-- return false, L["Missing talents"]
-- end
-- if not specID or not GetSpecializationInfoByID(specID) then
-- return false, L["Invalid specialization"]
-- end
-- elseif sourceType == "pvptalents" then
-- if version ~= 1 then -- Only supported version
-- return false, L["Invalid source version"]
-- end
-- local specID = source.specID or ...
-- if type(source.talents) ~= "table" then
-- return false, L["Missing pvp talents"]
-- end
-- if not specID or not GetSpecializationInfoByID(specID) then
-- return false, L["Invalid specialization"]
-- end
-- elseif sourceType == "essences" then
-- if version ~= 1 then -- Only supported version
-- return false, L["Invalid source version"]
-- end
-- local role = source.role or GetSpecializationRoleByID(...)
-- if type(source.essences) ~= "table" then
-- return false, L["Missing essences"]
-- end
-- if role ~= "DAMAGER" and role ~= "HEALER" and role ~= "TANK" then
-- return false, L["Invalid role"]
-- end
-- else
-- return false, L["Invalid import type"]
-- end
return true
end
local DFTalentImportMixin = {
ImportLoadout = function (self, importText, loadoutName)
local importStream = ExportUtil.MakeImportDataStream(importText);
local headerValid, serializationVersion, specID, treeHash = self:ReadLoadoutHeader(importStream);
if not headerValid then
return false, LOADOUT_ERROR_BAD_STRING
end
if serializationVersion ~= 1 then
return false, LOADOUT_ERROR_SERIALIZATION_VERSION_MISMATCH
end
local classInfo = Internal.GetClassInfoBySpecID(specID);
local treeInfo = Internal.GetTreeInfoBySpecID(specID);
local treeNodes = Internal.GetTreeInfoBySpecID(specID);
if not self:IsHashEmpty(treeHash) then
-- allow third-party sites to generate loadout strings with an empty tree hash, which bypasses hash validation
if not self:HashEquals(treeHash, C_Traits.GetTreeHash(treeInfo.ID)) then
return false, LOADOUT_ERROR_TREE_CHANGED;
end
end
local loadoutContent = self:ReadLoadoutContent(importStream, treeInfo.ID);
local loadoutEntryInfo = self:ConvertToImportLoadoutEntryInfo(treeInfo.ID, loadoutContent);
local _, specName = GetSpecializationInfoByID(specID);
local result = {};
result.version = 1;
result.type = "dftalents";
result.name = format(loadoutName or L["New %s Set"], specName);
result.treeID = treeInfo.ID;
result.specID = specID;
result.classID = classInfo.classID;
result.nodes = loadoutEntryInfo;
return true, result
end,
ConvertToImportLoadoutEntryInfo = function (self, treeID, loadoutContent)
local results = {};
local treeNodes = C_Traits.GetTreeNodes(treeID);
local count = 1;
for i, treeNodeID in ipairs(treeNodes) do
local indexInfo = loadoutContent[i];
if indexInfo.isNodeSelected then
local treeNode = Internal.GetNodeInfo(treeNodeID);
if indexInfo.isChoiceNode then
results[treeNodeID] = indexInfo.choiceNodeSelection;
else
results[treeNodeID] = indexInfo.isPartiallyRanked and indexInfo.partialRanksPurchased or treeNode.maxRanks;
end
end
end
return results;
end,
}
local DFTalentImport
DFTalentImport = {
ImportLoadout = function (...)
LoadAddOn("Blizzard_ClassTalentUI")
Mixin(DFTalentImport, ClassTalentImportExportMixin, DFTalentImportMixin);
return DFTalentImport.ImportLoadout(...)
end
}
-- Import Frame
do
local function GetUniqueName(setType, name)
if not Internal.GetSetByName(setType, name) then
return name
end
local index = 2
while Internal.GetSetByName(setType, format("%s (%d)", name, index)) do
index = index + 1
end
return format("%s (%d)", name, index)
end
local function AddSource(source, sourceType, version, name, ...)
sourceType = source.type or sourceType
version = source.version or version
name = source.name or name
if sourceType == "loadout" then
assert(version == 2, "Unsupported loadout version")
local name = GetUniqueName("profiles", name)
for _,segment in Internal.EnumerateLoadoutSegments() do
if segment.import and source[segment.id] then
for i=#source[segment.id],1,-1 do
if not source[segment.id][i] then
table.remove(source[segment.id], i)
end
end
for index,set in ipairs(source[segment.id]) do
if type(set) == "table" then
set = AddSource(set, segment.id, 1, name)
source[segment.id][index] = set
end
set = segment.get(set)
set.useCount = (set.useCount or 0) + 1
end
end
end
local set = Internal.ImportLoadout(source, version, name)
return set.setID
else
local segment = Internal.GetLoadoutSegment(sourceType)
if not segment then
return false, L["Unknown source type"]
end
local name = GetUniqueName(segment.id, name)
local set = segment.import(source, version, name, ...)
return set.setID
end
end
local currentImport
BtWLoadoutsImportFrameMixin = {};
function BtWLoadoutsImportFrameMixin:OnLoad()
BackdropTemplateMixin.OnBackdropLoaded(self)
if self.TooltipBackdropOnLoad then
self:TooltipBackdropOnLoad()
end
tinsert(UISpecialFrames, self:GetName());
self:RegisterForDrag("LeftButton");
self.checkboxPool = CreateFramePool("CheckButton", self, "BtWLoadoutsImportCheckButtonTemplate")
end
function BtWLoadoutsImportFrameMixin:OnDragStart()
self:StartMoving();
end
function BtWLoadoutsImportFrameMixin:OnDragStop()
self:StopMovingOrSizing();
end
function BtWLoadoutsImportFrameMixin:OnShow()
local source = currentImport
if not source then
self:Hide();
return
end
self.checkboxPool:ReleaseAll()
local previousButton = self.Name
if source.type == "loadout" then
self.Name:SetText(format(L["Importing Loadout \"%s\""], source.name))
for _,segment in Internal.EnumerateLoadoutSegments() do
local sets = source[segment.id]
if sets then
for index,set in ipairs(sets) do
local button = self.checkboxPool:Acquire()
button:SetPoint("TOPLEFT", previousButton, "BOTTOMLEFT", 0, -5)
if type(set) == "number" then
local set = segment.get(set)
button.Text:SetText(format(L["Use existing %s set \"%s\""], segment.name, set.name))
else
button.Text:SetText(format(L["Include %s as \"%s\""], segment.name, set.name))
end
button.index = index
button.type = segment.id
button:SetChecked(true)
button:Show()
previousButton = button
end
end
end
else
local segment = Internal.GetLoadoutSegment(source.type)
if not segment then
return
end
self.Name:SetText(format(L["Importing %s \"%s\""], segment.name, source.name))
end
self:SetHeight(self:GetTop() - previousButton:GetBottom() + 45)
end
function BtWLoadoutsImportFrameMixin:Accept()
local source = currentImport
-- Remove parts that the user disabled
if source.type == "loadout" then
for button in self.checkboxPool:EnumerateActive() do
if not button:GetChecked() then
source[button.type][button.index] = false
end
end
end
local setID = AddSource(source)
BtWLoadoutsFrame:SetSetByID(source.type, setID)
currentImport = nil
BtWLoadoutsImportFrame:Hide()
end
function Internal.UpdateImportFrame(source)
BtWLoadoutsImportFrame:Hide()
currentImport = source
BtWLoadoutsImportFrame:Show()
end
end
function External.Import(source)
local success, err
if type(source) == "string" then
success, err = DFTalentImport:ImportLoadout(source)
if success then -- Official DF Talents
source = err;
else
success, source = Decode(source, err)
if not success then
return false, source
end
success, source = StringToTable(source)
if not success then
return false, source
end
end
end
if type(source) ~= "table" then
return false, L["Invalid source type"]
end
success, err = VerifySource(source)
if not success then
return false, err
end
if source.type == "loadout" then
-- Find existing sets with the same content
for _,segment in Internal.EnumerateLoadoutSegments() do
if source[segment.id] then
for index,set in ipairs(source[segment.id]) do
local target = segment.getByValue(set)
if target then
source[segment.id][index] = target.setID
end
end
end
end
end
Internal.UpdateImportFrame(source)
return true
end
function Internal.Export(segmentType, id, format)
if segmentType == "loadout" then
local loadout = Internal.GetProfile(id)
local result = {}
assert(loadout.version == 2, "Unable to export loadouts that are not version 2")
result.version = 2
result.type = "loadout"
result.name = loadout.name
result.specID = loadout.specID
for _,segment in Internal.EnumerateLoadoutSegments() do
if segment.export and #loadout[segment.id] > 0 then
local sets = {}
for _,setID in ipairs(loadout[segment.id]) do
sets[#sets+1] = segment.export(segment.get(setID))
end
result[segment.id] = sets
end
end
return Encode(TableToString(result), format or "BN")
else
local segment = Internal.GetLoadoutSegment(segmentType)
assert(segment ~= nil)
local set = segment.get(id)
local result = segment.export(set)
result.type = segment.id
return Encode(TableToString(result), format or "BN")
end
end
function External.Export(id, format)
return Internal.Export("loadout", id, format)
end
-- /run BtWLoadoutsFrame:SetExport(BtWLoadouts.Export(1))
BtWLoadoutsImportMixin = {}
function BtWLoadoutsImportMixin:Update()
self:GetParent():SetTitle(L["Import"]);
self:GetParent().Sidebar:Hide()
BtWLoadoutsFrame:SetNPEShown(false);
self.Scroll.EditBox.text = ""
self.Scroll.EditBox:SetText("")
self:GetParent().RefreshButton:Hide();
self:GetParent().ActivateButton:Hide();
self:GetParent().ExportButton:Hide();
self:GetParent().DeleteButton:Hide();
self:GetParent().AddButton:Hide();
end
function BtWLoadoutsImportMixin:OnShow()
self.Scroll.EditBox:SetFocus()
end