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.
734 lines
26 KiB
734 lines
26 KiB
local RingKeeper, _, T = {}, ...
|
|
local RK_RingDesc, RK_CollectionIDs, RK_Version, RK_Rev, EV, PC, SV = {}, {}, 2, 50, T.Evie, T.OPieCore
|
|
local unlocked, queue, RK_DeletedRings, RK_FlagStore, sharedCollection = false, {}, {}, {}, {}
|
|
local MODERN = select(4,GetBuildInfo()) >= 8e4
|
|
|
|
local function assert(condition, text, level, ...)
|
|
return (not condition) and error(tostring(text):format(...), 1 + (level or 1)) or condition
|
|
end
|
|
|
|
local AB = assert(T.ActionBook:compatible(2,19), "A compatible version of ActionBook is required")
|
|
local RW = assert(T.ActionBook:compatible("Rewire", 1,10), "A compatible version of Rewire is required")
|
|
local ORI = OPie.UI
|
|
local CLASS, FULLNAME, FACTION
|
|
|
|
local RK_ParseMacro, RK_QuantizeMacro do -- +RingKeeper:SetMountPreference(groundSpellID, airSpellID)
|
|
local castAlias = {["#show"]=0, ["#showtooltip"]=0} do
|
|
for n,v in ("CAST:1 USE:1 CASTSEQUENCE:2 CASTRANDOM:3 USERANDOM:3"):gmatch("(%a+):(%d+)") do
|
|
local v, idx, s = v+0, 1
|
|
repeat
|
|
if s then
|
|
castAlias[s] = v
|
|
end
|
|
s, idx = _G["SLASH_" .. n .. idx], idx+1
|
|
until not s
|
|
end
|
|
end
|
|
local function replaceSpellID(ctype, sidlist, prefix, tk)
|
|
local sr, ar
|
|
for id, sn in sidlist:gmatch("%d+") do
|
|
id = id + 0
|
|
sn, sr = GetSpellInfo(id), GetSpellSubtext(id)
|
|
ar = GetSpellSubtext(sn)
|
|
local isCastable, castFlag = RW:IsSpellCastable(id)
|
|
if not MODERN and not isCastable and tk ~= "spellr" then
|
|
local id2 = select(7,GetSpellInfo(sn))
|
|
if id2 then
|
|
id, isCastable, castFlag = id2, RW:IsSpellCastable(id2)
|
|
end
|
|
end
|
|
if isCastable then
|
|
if castFlag == "forced-id-cast" and (ctype == 1 or ctype == 3) then
|
|
sn = "spell:" .. id
|
|
elseif ctype == 3 and sn and sn:match(",") then
|
|
sn = "spell:" .. id
|
|
elseif sr and sr ~= "" and (MODERN or tk == "spellr") then
|
|
sn = sn .. "(" .. sr .. ")"
|
|
elseif tk == "spell" and not MODERN and ar ~= sr and ar then
|
|
sn = sn .. "(" .. ar .. ")"
|
|
end
|
|
return prefix .. sn
|
|
end
|
|
end
|
|
end
|
|
local replaceMountTag do
|
|
local skip, gmSid, gmPref, fmSid, fmPref = {[44153]=1, [44151]=1, [61451]=1, [75596]=1, [61309]=1, [169952]=1, [171844]=1, [213339]=1,}
|
|
local function IsKnownSpell(sid)
|
|
local sn, sr = GetSpellInfo(sid or 0), GetSpellSubtext(sid or 0)
|
|
return GetSpellInfo(sn, sr) ~= nil and sid or (RW:GetCastEscapeAction(sn) and sid)
|
|
end
|
|
local function findMount(prefSID, mtype)
|
|
local myFactionId, nc, cs = UnitFactionGroup("player") == "Horde" and 0 or 1, 0
|
|
local idm = C_MountJournal.GetMountIDs()
|
|
local gmi, gmiex = C_MountJournal.GetMountInfoByID, C_MountJournal.GetMountInfoExtraByID
|
|
for i=1, #idm do
|
|
i = idm[i]
|
|
local _1, sid, _3, _4, _5, _6, _7, factionLocked, factionId, hide, have = gmi(i)
|
|
if have and not hide
|
|
and (not factionLocked or factionId == myFactionId)
|
|
and RW:IsSpellCastable(sid)
|
|
then
|
|
local _, _, _, _, t = gmiex(i)
|
|
if sid == prefSID then
|
|
return sid
|
|
elseif t == mtype and not skip[sid] then
|
|
nc = nc + 1
|
|
if math.random(1,nc) == 1 then
|
|
cs = sid
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return cs
|
|
end
|
|
function replaceMountTag(ctype, tag, prefix)
|
|
if not MODERN then
|
|
elseif tag == "ground" then
|
|
gmSid = gmSid and IsKnownSpell(gmSid) or findMount(gmPref or gmSid, 230)
|
|
return replaceSpellID(ctype, tostring(gmSid), prefix)
|
|
elseif tag == "air" then
|
|
fmSid = fmSid and IsKnownSpell(fmSid) or findMount(fmPref or fmSid, 248)
|
|
return replaceSpellID(ctype, tostring(fmSid), prefix)
|
|
end
|
|
return nil
|
|
end
|
|
function RingKeeper:SetMountPreference(groundSpellID, airSpellID)
|
|
if type(groundSpellID) == "number" then
|
|
gmPref = groundSpellID
|
|
end
|
|
if type(airSpellID) == "number" then
|
|
fmPref = airSpellID
|
|
end
|
|
end
|
|
end
|
|
local function replaceAlternatives(ctype, replaceFunc, args)
|
|
local ret, alt2, rfCtx
|
|
for alt, cpos in (args .. ","):gmatch("(.-),()") do
|
|
alt2, rfCtx = replaceFunc(ctype, alt, rfCtx, args, cpos)
|
|
if alt == alt2 or (alt2 and alt2:match("%S")) then
|
|
ret = (ret and (ret .. ", ") or "") .. alt2:match("^%s*(.-)%s*$")
|
|
end
|
|
end
|
|
return ret
|
|
end
|
|
local function genLineParser(replaceFunc)
|
|
return function(commandPrefix, command, args)
|
|
local ctype = castAlias[command:lower()]
|
|
if not ctype then return end
|
|
local pos, len, ret = 1, #args
|
|
repeat
|
|
local cstart, cend, vend = pos
|
|
repeat
|
|
local ce, cs = args:match("();", pos) or (len+1), args:match("()%[", pos)
|
|
if cs and cs < ce then
|
|
pos = args:match("%]()", cs)
|
|
else
|
|
cend, vend, pos = pos, ce-1, ce + 1
|
|
end
|
|
until cend or not pos
|
|
if not pos then return end
|
|
local cval = args:sub(cend, vend)
|
|
if ctype < 2 then
|
|
cval = replaceFunc(ctype, args:sub(cend, vend))
|
|
else
|
|
local val, reset = args:sub(cend, vend)
|
|
if ctype == 2 then reset, val = val:match("^(%s*reset=%S+%s*)"), val:gsub("^%s*reset=%S+%s*", "") end
|
|
val = replaceAlternatives(ctype, replaceFunc, val)
|
|
cval = val and ((reset or "") .. val) or nil
|
|
end
|
|
if cval or ctype == 0 then
|
|
local clause = (cstart < cend and (args:sub(cstart, cend-1):match("^%s*(.-)%s*$") .. " ") or "") .. (cval and cval:match("^%s*(.-)%s*$") or "")
|
|
ret = (ret and (ret .. "; ") or commandPrefix) .. clause
|
|
end
|
|
until not pos or pos > #args
|
|
return ret or ""
|
|
end
|
|
end
|
|
local parseLine, quantizeLine, prepareQuantizer do
|
|
local tip = CreateFrame("GameTooltip")
|
|
tip:AddFontStrings(tip:CreateFontString(), tip:CreateFontString())
|
|
tip:SetOwner(UIParent, "ANCHOR_NONE")
|
|
parseLine = genLineParser(function(ctype, value)
|
|
local prefix, tkey, tval = value:match("^%s*(!?)%s*{{(%a+):([%a%d/]+)}}%s*$")
|
|
if tkey == "spell" or tkey == "spellr" then
|
|
return replaceSpellID(ctype, tval, prefix, tkey)
|
|
elseif tkey == "mount" then
|
|
return replaceMountTag(ctype, tval, prefix)
|
|
end
|
|
return value
|
|
end)
|
|
local spells, OTHER_SPELL_IDS = {}, {150544, 243819}
|
|
quantizeLine = genLineParser(function(ctype, value, ctx, args, cpos)
|
|
if type(ctx) == "number" and ctx > 0 then
|
|
return nil, ctx-1
|
|
end
|
|
local cc, mark, name = 0, value:match("^%s*(!?)(.-)%s*$")
|
|
repeat
|
|
local sid, peek, cnpos = spells[name:lower()]
|
|
if sid then
|
|
if not MODERN then
|
|
local rname = name:gsub("%s*%([^)]+%)$", "")
|
|
local sid2 = rname ~= name and spells[rname:lower()]
|
|
if sid2 then
|
|
return (mark .. "{{spellr:" .. sid .. "}}"), cc
|
|
end
|
|
end
|
|
return (mark .. "{{spell:" .. sid .. "}}"), cc
|
|
end
|
|
if ctype >= 2 and args then
|
|
peek, cnpos = args:match("^([^,]+),?()", cpos)
|
|
if peek then
|
|
cc, name, cpos = cc + 1, name .. ", " .. peek:match("^%s*(.-)%s*$"), cnpos
|
|
end
|
|
end
|
|
until not peek or cc > 5
|
|
return value
|
|
end)
|
|
local function addModernSpells()
|
|
local gmi, idm = C_MountJournal.GetMountInfoByID, C_MountJournal.GetMountIDs()
|
|
for i=1, #idm do
|
|
local _, sid = gmi(idm[i])
|
|
local sname = GetSpellInfo(sid)
|
|
if sname then
|
|
spells[sname:lower()] = sid
|
|
end
|
|
end
|
|
for spec=1,GetNumSpecializations() do
|
|
for tier=1, MAX_TALENT_TIERS do
|
|
for column=1, 3 do
|
|
tip:SetTalent(GetTalentInfoBySpecialization(spec, tier, column))
|
|
local name, id = tip:GetSpell()
|
|
if id and type(name) == "string" then
|
|
spells[name:lower()] = id
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
local function addSpell(n, id, allowGenericOverwrite)
|
|
local nl, sr, k = n:lower(), GetSpellSubtext(id)
|
|
spells[nl] = allowGenericOverwrite and id or spells[nl] or id
|
|
if sr and sr ~= "" then
|
|
k = nl .. "(" .. sr:lower() .. ")"; spells[k] = spells[k] or id
|
|
k = nl .. " (" .. sr:lower() .. ")"; spells[k] = spells[k] or id
|
|
end
|
|
end
|
|
local function addSpellBookTab(ofs, c, allowGenericOverwrite)
|
|
for j=ofs+1,ofs+c do
|
|
local n, st, id = GetSpellBookItemName(j, "spell"), GetSpellBookItemInfo(j, "spell")
|
|
if type(n) ~= "string" or not id then
|
|
elseif st == "SPELL" or st == "FUTURESPELL" then
|
|
addSpell(n, id, allowGenericOverwrite and st == "SPELL")
|
|
elseif st == "FLYOUT" then
|
|
for j=1,select(3,GetFlyoutInfo(id)) do
|
|
local sid, _, _, sname = GetFlyoutSlotInfo(id, j)
|
|
if sid and type(sname) == "string" then
|
|
addSpell(sname, sid)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
function prepareQuantizer(reuse)
|
|
if reuse and next(spells) then return end
|
|
wipe(spells)
|
|
for i=1,#OTHER_SPELL_IDS do
|
|
local sn = GetSpellInfo(OTHER_SPELL_IDS[i])
|
|
if sn then
|
|
spells[sn:lower()] = OTHER_SPELL_IDS[i]
|
|
end
|
|
end
|
|
if MODERN then
|
|
addModernSpells()
|
|
end
|
|
for curSpec=0,1 do
|
|
for i=GetNumSpellTabs()+12,1,-1 do
|
|
local _, _, ofs, c, _, sid = GetSpellTabInfo(i)
|
|
if ((curSpec == 0) == (sid == 0)) then
|
|
addSpellBookTab(ofs, c, not MODERN)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
function RK_ParseMacro(macro)
|
|
if type(macro) == "string" and (macro:match("{{spellr?:[%d/]+}}") or macro:match("{{mount:%a+}}") ) then
|
|
macro = ("\n" .. macro):gsub("(\n([#/]%S+) ?)([^\n]*)", parseLine)
|
|
end
|
|
return macro
|
|
end
|
|
function RK_QuantizeMacro(macro, useCache)
|
|
return type(macro) == "string" and (prepareQuantizer(useCache) or true) and ("\n" .. macro):gsub("(\n([#/]%S+) ?)([^\n]*)", quantizeLine):sub(2) or macro
|
|
end
|
|
end
|
|
local function RK_IsRelevantRingDescription(desc)
|
|
if desc then
|
|
local limit = desc.limit
|
|
return limit == nil or limit == FULLNAME or limit == CLASS or limit == FACTION
|
|
end
|
|
end
|
|
local serialize, unserialize do
|
|
local sigT, sigN = {}
|
|
for i, c in ("01234qwertyuiopasdfghjklzxcvbnm5678QWERTYUIOPASDFGHJKLZXCVBNM9"):gmatch("()(.)") do sigT[i-1], sigT[c], sigN = c, i-1, i end
|
|
local function checksum(s)
|
|
local h = (134217689 * #s) % 17592186044399
|
|
for i=1,#s,4 do
|
|
local a, b, c, d = s:match("(.?)(.?)(.?)(.?)", i)
|
|
a, b, c, d = sigT[a], (sigT[b] or 0) * sigN, (sigT[c] or 0) * sigN^2, (sigT[d] or 0) * sigN^3
|
|
h = (h * 211 + a + b + c + d) % 17592186044399
|
|
end
|
|
return h % 3298534883309
|
|
end
|
|
local function nenc(v, b, rest)
|
|
if b == 0 then return v == 0 and rest or error("numeric overflow") end
|
|
local v1 = v % sigN
|
|
local v2 = (v - v1) / sigN
|
|
return nenc(v2, b - 1, sigT[v1] .. (rest or ""))
|
|
end
|
|
local function cenc(c)
|
|
local b, m = c:byte(), sigN-1
|
|
return sigT[(b - b % m) / m] .. sigT[b % m]
|
|
end
|
|
local function venc(v, t, reg)
|
|
if reg[v] then
|
|
table.insert(t, sigT[1] .. sigT[reg[v]])
|
|
elseif type(v) == "table" then
|
|
local n = math.min(sigN-1, #v)
|
|
for i=n,1,-1 do venc(v[i], t, reg) end
|
|
table.insert(t, sigT[3] .. sigT[n])
|
|
for k,v2 in pairs(v) do
|
|
if not (type(k) == "number" and k >= 1 and k <= n and k % 1 == 0) then
|
|
venc(v2, t, reg)
|
|
venc(k, t, reg)
|
|
table.insert(t, sigT[4])
|
|
end
|
|
end
|
|
elseif type(v) == "number" then
|
|
if v % 1 ~= 0 then error("non-integer value") end
|
|
if v < -1000000 then error("integer underflow") end
|
|
table.insert(t, sigT[5] .. nenc(v + 1000000, 4))
|
|
elseif type(v) == "string" then
|
|
table.insert(t, sigT[6] .. v:gsub("[^a-zA-Z5-8]", cenc) .. "9")
|
|
else
|
|
table.insert(t, sigT[1] .. ((v == true and sigT[1]) or (v == nil and sigT[0]) or sigT[2]))
|
|
end
|
|
return t
|
|
end
|
|
local function tenc(t, rt)
|
|
local u, ua = {}, {}
|
|
for i=1,#t do
|
|
local k = t[i]
|
|
if #k < 4 then
|
|
elseif u[k] then
|
|
u[k] = u[k] + 1
|
|
else
|
|
ua[#ua+1], u[k] = k, 1
|
|
end
|
|
end
|
|
local freeSlot = 5
|
|
while rt[freeSlot] ~= nil do
|
|
freeSlot = freeSlot + 1
|
|
end
|
|
table.sort(ua, function(a, b)
|
|
return (#a-2)*(u[a]-1) > (#b-2)*(u[b]-1)
|
|
end)
|
|
for i=math.max(1, sigN-freeSlot+1), #ua do
|
|
u[ua[i]], ua[i] = nil
|
|
end
|
|
for i=1,#t do
|
|
local uk = u[t[i]]
|
|
if uk == nil then
|
|
elseif type(uk) == "string" then
|
|
t[i] = uk
|
|
elseif freeSlot < sigN and uk > 1 then
|
|
u[t[i]], t[i] = sigT[1] .. sigT[freeSlot], t[i] .. sigT[2] .. sigT[freeSlot]
|
|
freeSlot = freeSlot + 1
|
|
end
|
|
end
|
|
return t
|
|
end
|
|
|
|
local ops = {"local ops, sigT, sigN, s, r, pri = {}, ...\nlocal cdec, ndec = function(c, l) return string.char(sigT[c]*(sigN-1) + sigT[l]) end, function(s) local r = 0 for i=1,#s do r = r * sigN + sigT[s:sub(i,i)] end return r end",
|
|
"s[d+1], d, pos = r[sigT[pri:sub(pos,pos)]], d + 1, pos + 1",
|
|
"r[sigT[pri:sub(pos,pos)]], pos = s[d], pos + 1",
|
|
"local t, n = {}, sigT[pri:sub(pos,pos)]\nfor i=1,n do t[i] = s[d-i+1] end\ns[d - n + 1], d, pos = t, d - n + 1, pos + 1",
|
|
"s[d-2][s[d]], d = s[d-1], d - 2",
|
|
"s[d+1], d, pos = ndec(pri:sub(pos, pos + 3)) - 1000000, d + 1, pos + 4",
|
|
"d, s[d+1], pos = d + 1, pri:match('^(.-)9()', pos)\ns[d] = s[d]:gsub('([0-4])(.)', cdec)",
|
|
"s[d-1], d = s[d-1]+s[d], d - 1",
|
|
"s[d-1], d = s[d-1]*s[d], d - 1",
|
|
"s[d-1], d = s[d-1]/s[d], d - 1",
|
|
"function ops.bind(...) s, r, pri = ... end\nreturn ops"
|
|
}
|
|
for i=2,#ops-1 do
|
|
ops[i] = ("ops[%q] = function(d, pos)\n %s\n return d, pos\nend"):format(sigT[i-1], ops[i])
|
|
end
|
|
ops = loadstring(table.concat(ops, "\n"))(sigT, sigN)
|
|
|
|
function serialize(t, sign, regGhost)
|
|
local rt = setmetatable({}, regGhost)
|
|
local payload = table.concat(tenc(venc(t, {}, rt), rt), "")
|
|
return ((sign .. nenc(checksum(sign .. payload), 7) .. payload):gsub("(.......)", "%1 "):gsub(" ?$", ".", 1))
|
|
end
|
|
function unserialize(s, sign, regGhost)
|
|
local h, pri = s:gsub("[^a-zA-Z0-9.]", ""):match("^" .. sign .. "(.......)([^.]+)")
|
|
if nenc(checksum(sign .. pri), 7) ~= h then return end
|
|
|
|
local stack, depth, pos, len = {}, 0, 1, #pri
|
|
ops.bind(stack, setmetatable({true, false}, regGhost), pri)
|
|
while pos <= len do
|
|
depth, pos = ops[pri:sub(pos, pos)](depth, pos + 1)
|
|
end
|
|
return depth == 1 and stack[1]
|
|
end
|
|
end
|
|
local encodeMacro, decodeMacro do
|
|
local function slash_i18n(command, lead)
|
|
if lead == "!" then return "\n!" .. command end
|
|
local key = command:upper()
|
|
if type(hash_ChatTypeInfoList[key]) == "string" and not hash_ChatTypeInfoList[key]:match("!") then
|
|
return "\n!" .. hash_ChatTypeInfoList[key] .. "!" .. command
|
|
elseif type(hash_EmoteTokenList[key]) == "string" and not hash_EmoteTokenList[key]:match("!") then
|
|
return "\n!" .. hash_EmoteTokenList[key] .. "!" .. command
|
|
end
|
|
end
|
|
local function slash_l10n(key, command)
|
|
if key == "" then return "\n!" .. command end
|
|
local k2 = command:upper()
|
|
if hash_ChatTypeInfoList[k2] == key or hash_EmoteTokenList[k2] == key then
|
|
elseif _G["SLASH_" .. key .. 1] then
|
|
return "\n" .. _G["SLASH_" .. key .. 1]
|
|
else
|
|
local i, v = 2, EMOTE1_TOKEN
|
|
while v do
|
|
if v == key then
|
|
return "\n" .. _G["EMOTE" .. (i-1) .. "_CMD1"]
|
|
end
|
|
i, v = i + 1, _G["EMOTE" .. i .. "_TOKEN"]
|
|
end
|
|
end
|
|
return "\n" .. command
|
|
end
|
|
function encodeMacro(m)
|
|
ChatFrame_ImportAllListsToHash()
|
|
return ("\n" .. m):gsub("\n(([/!])%S*)", slash_i18n):sub(2)
|
|
end
|
|
function decodeMacro(m)
|
|
ChatFrame_ImportAllListsToHash()
|
|
return ("\n" .. m):gsub("\n!(.-)!(%S*)", slash_l10n):sub(2)
|
|
end
|
|
end
|
|
local copy do
|
|
local copies = {}
|
|
function copy(t, isInnerCall)
|
|
if not isInnerCall then wipe(copies) end
|
|
local into = {} copies[t] = into
|
|
for k,v in pairs(t) do
|
|
into[type(k) == "table" and (copies[k] or copy(k, true)) or k] = type(v) == "table" and (copies[v] or copy(v, true)) or v
|
|
end
|
|
if not isInnerCall then wipe(copies) end
|
|
return into
|
|
end
|
|
end
|
|
|
|
local sReg, sRegRev, sSign = {__index={nil, nil, "name", "hotkey", "offset", "noOpportunisticCA", "noPersistentCA", "internal", "limit", "id", "skipSpecs", "caption", "icon", "show"}}, {__index={}}, string.char(111,101,116,111,104,72,55)
|
|
for k,v in pairs(sReg.__index) do sRegRev.__index[v] = k end
|
|
|
|
local function pullOptions(e, a, ...)
|
|
if a then return e[a], pullOptions(e, ...) end
|
|
end
|
|
local function unpackABAction(e, s)
|
|
if e[s] then return e[s], unpackABAction(e, s+1) end
|
|
return pullOptions(e, AB:GetActionOptions(e[1]))
|
|
end
|
|
local function RK_SyncRing(name, force, tok)
|
|
local desc, changed, cid = RK_RingDesc[name], (force == true), RK_CollectionIDs[name]
|
|
if not RK_IsRelevantRingDescription(desc) then return end
|
|
tok = tok or AB:GetLastObserverUpdateToken("*")
|
|
if not force and tok == desc._lastUpdateToken then return end
|
|
desc._lastUpdateToken = tok
|
|
|
|
local limit = desc.limit
|
|
desc.sortScope = limit == FULLNAME and 30 or limit == CLASS and 20 or 10
|
|
if not cid then
|
|
wipe(sharedCollection)
|
|
changed, cid = true, AB:CreateActionSlot(nil, nil, "collection", sharedCollection)
|
|
RK_CollectionIDs[name], RK_CollectionIDs[cid] = cid, name
|
|
OneRingLib:SetRing(name, cid, desc)
|
|
end
|
|
|
|
local onOpenSlice, onOpenAction = desc.onOpen
|
|
for i=1, #desc do
|
|
local e = desc[i]
|
|
local ident, action = e[1]
|
|
if ident == "macrotext" then
|
|
local m = RK_ParseMacro(e[2])
|
|
if m:match("%S") then action = AB:GetActionSlot("macrotext", m) end
|
|
elseif type(ident) == "string" then
|
|
action = AB:GetActionSlot(unpackABAction(e, 1))
|
|
end
|
|
changed = changed or (action ~= e._action) or (e.fastClick ~= e._fastClick) or (e.rotationMode ~= e._rotationMode) or (action and (e.show ~= e._show) or (e.embed ~= e._embed))
|
|
e._action, e._fastClick, e._rotationMode = action, e.fastClick, e.rotationMode
|
|
if i == onOpenSlice then
|
|
onOpenAction = e._action
|
|
end
|
|
end
|
|
changed = changed or (desc._embed ~= desc.embed) or (desc._onOpen ~= onOpenAction)
|
|
|
|
if not changed and not force then return end
|
|
local collection, cn = sharedCollection, 1
|
|
wipe(collection)
|
|
for i=1, #desc do
|
|
local e = desc[i]
|
|
if e._action and i ~= onOpenSlice then
|
|
collection[e.sliceToken], collection[cn], cn = e._action, e.sliceToken, cn + 1
|
|
collection['__visibility-' .. e.sliceToken], e._show = e.show or nil, e.show
|
|
collection['__embed-' .. e.sliceToken], e._embed = e.embed, e.embed
|
|
ORI:SetDisplayOptions(e.sliceToken, e.icon, nil, e._r, e._g, e._b)
|
|
end
|
|
end
|
|
collection['__embed'], desc._embed = desc.embed, desc.embed
|
|
collection['__openAction'], desc._onOpen = onOpenAction, onOpenAction
|
|
AB:UpdateActionSlot(cid, collection)
|
|
OneRingLib:SetRing(name, cid, desc)
|
|
end
|
|
local function dropUnderscoreKeys(t)
|
|
for k in pairs(t) do
|
|
if type(k) == "string" and k:sub(1,1) == "_" then
|
|
t[k] = nil
|
|
end
|
|
end
|
|
end
|
|
local function RK_SanitizeDescription(props)
|
|
local uprefix = type(props._u) == "string" and props._u
|
|
for i=#props,1,-1 do
|
|
local v = props[i]
|
|
if type(v.c) == "string" then
|
|
local r,g,b = v.c:match("^(%x%x)(%x%x)(%x%x)$")
|
|
if r then
|
|
v._r, v._g, v._b = tonumber(r, 16)/255, tonumber(g, 16)/255, tonumber(b, 16)/255
|
|
end
|
|
end
|
|
local rt, id = v.rtype, v.id
|
|
if rt and id then
|
|
v[1], v[2], v.rtype, v.id = rt, id
|
|
elseif type(id) == "number" then
|
|
v[1], v[2], v.rtype, v.id = "spell", id
|
|
elseif type(id) == "string" then
|
|
v[1], v[2], v.rtype, v.id = "macrotext", id
|
|
elseif v[1] == nil then
|
|
table.remove(props, i)
|
|
end
|
|
if v.lockRotation ~= nil and v.rotationMode == nil then
|
|
-- DEPRECATED [1902/3.96/W1]: lockRotation->rotationMode transition.
|
|
v.rotationMode = v.lockRotation and "reset" or nil
|
|
v.lockRotation = nil
|
|
end
|
|
v.show = v.show ~= "" and v.show or nil
|
|
v.sliceToken = v.sliceToken or (uprefix and type(v._u) == "string" and (uprefix .. v._u)) or AB:CreateToken()
|
|
v._action, v._embed = nil
|
|
end
|
|
props._embed = nil
|
|
return props
|
|
end
|
|
local function RK_SerializeDescription(props)
|
|
for _, slice in ipairs(props) do
|
|
if slice[1] == "spell" or slice[1] == "macrotext" then
|
|
slice.id, slice[1], slice[2] = slice[2]
|
|
end
|
|
dropUnderscoreKeys(slice)
|
|
end
|
|
dropUnderscoreKeys(props)
|
|
props.sortScope = nil
|
|
return props
|
|
end
|
|
function EV.PLAYER_REGEN_DISABLED()
|
|
for k in pairs(RK_RingDesc) do
|
|
securecall(RK_SyncRing, k)
|
|
end
|
|
end
|
|
local function abPreOpen(_, _, id)
|
|
local k = RK_CollectionIDs[id]
|
|
if k then
|
|
RK_SyncRing(k)
|
|
end
|
|
end
|
|
local function svInitializer(event, _name, sv)
|
|
if event == "LOGOUT" and unlocked then
|
|
for k in pairs(sv) do sv[k] = nil end
|
|
for k, v in pairs(RK_RingDesc) do
|
|
if type(v) == "table" and not RK_DeletedRings[k] and v.save then
|
|
sv[k] = RK_SerializeDescription(v)
|
|
end
|
|
end
|
|
sv.OPieDeletedRings, sv.OPieFlagStore = next(RK_DeletedRings) and RK_DeletedRings, next(RK_FlagStore) and RK_FlagStore
|
|
|
|
elseif event == "LOGIN" then
|
|
local name, realm, _ = UnitFullName("player")
|
|
FULLNAME, FACTION, _, CLASS = name .. '-' .. realm, UnitFactionGroup("player"), UnitClass("player")
|
|
|
|
unlocked = true
|
|
local deleted, flags, mousemap = SV.OPieDeletedRings or RK_DeletedRings, SV.OPieFlagStore or RK_FlagStore
|
|
mousemap, SV.OPieDeletedRings, SV.OPieFlagStore = {PRIMARY=OneRingLib:GetOption("PrimaryButton"), SECONDARY=OneRingLib:GetOption("SecondaryButton")}
|
|
for k,v in pairs(flags) do RK_FlagStore[k] = v end
|
|
|
|
local storageVersion = flags.StoreVersion or (flags.FlushedDefaultColors and 1) or 0
|
|
storageVersion = type(storageVersion) == "number" and storageVersion or 0
|
|
local colorFlush, onOpenFlush = storageVersion < 1, storageVersion < 2
|
|
RK_FlagStore.StoreVersion, RK_FlagStore.FlushedDefaultColors = 2, nil
|
|
|
|
for k, v in pairs(queue) do
|
|
if v.hotkey then v.hotkey = v.hotkey:gsub("[^-; ]+", mousemap) end
|
|
if deleted[k] == nil and SV[k] == nil then
|
|
securecall(RingKeeper.SetRing, RingKeeper, k, v)
|
|
SV[k] = nil
|
|
elseif deleted[k] then
|
|
RK_DeletedRings[k] = true
|
|
end
|
|
end
|
|
for k, v in pairs(SV) do
|
|
if type(v) == "table" then
|
|
if colorFlush then
|
|
for _,v in pairs(v) do
|
|
if type(v) == "table" and v.c == "e5ff00" then
|
|
v.c = nil
|
|
end
|
|
end
|
|
end
|
|
if onOpenFlush and v.onOpen ~= nil then
|
|
v.quarantineOnOpen, v.onOpen = v.onOpen, nil
|
|
end
|
|
end
|
|
securecall(RingKeeper.SetRing, RingKeeper, k, v)
|
|
end
|
|
|
|
collectgarbage("collect")
|
|
end
|
|
end
|
|
local function ringIterator(isDeleted, k)
|
|
local nk, v = next(isDeleted and RK_DeletedRings or RK_RingDesc, k)
|
|
if nk and isDeleted then
|
|
return RK_IsRelevantRingDescription(queue[nk]) and nk or ringIterator(isDeleted, nk)
|
|
elseif nk then
|
|
return nk, v.name or nk, RK_CollectionIDs[nk] ~= nil, #v, v.internal, v.limit
|
|
end
|
|
end
|
|
|
|
-- Public API
|
|
function RingKeeper:GetVersion()
|
|
return RK_Version, RK_Rev
|
|
end
|
|
function RingKeeper:SetRing(name, desc)
|
|
assert(type(name) == "string" and (type(desc) == "table" or desc == false), "Syntax: RingKeeper:SetRing(name, descTable or false)", 2)
|
|
if not unlocked then
|
|
queue[name] = desc
|
|
elseif desc == false then
|
|
if RK_RingDesc[name] then
|
|
OneRingLib:SetRing(name, nil)
|
|
if RK_CollectionIDs[name] then RK_CollectionIDs[RK_CollectionIDs[name]] = nil end
|
|
RK_DeletedRings[name], RK_RingDesc[name], RK_CollectionIDs[name], SV[name] = queue[name] and true or nil
|
|
end
|
|
else
|
|
RK_RingDesc[name], RK_DeletedRings[name] = RK_SanitizeDescription(copy(desc)), nil
|
|
RK_SyncRing(name, true)
|
|
end
|
|
end
|
|
function RingKeeper:GetManagedRings()
|
|
return ringIterator, false, nil
|
|
end
|
|
function RingKeeper:GetDeletedRings()
|
|
return ringIterator, true, nil
|
|
end
|
|
function RingKeeper:GetRingDescription(name, serialize)
|
|
assert(type(name) == "string", 'Syntax: desc = RingKeeper:GetRingDescription("name"[, serialize])', 2)
|
|
local ring = RK_RingDesc[name] and copy(RK_RingDesc[name]) or false
|
|
return serialize and ring and RK_SerializeDescription(ring) or ring
|
|
end
|
|
function RingKeeper:GetRingInfo(name)
|
|
assert(type(name) == "string", 'Syntax: title, numSlices, isDefault, isOverriden = RingKeeper:GetRingInfo("name")', 2)
|
|
local ring = RK_RingDesc[name]
|
|
return ring and (ring.name or name), ring and #ring, not not queue[name], ring and ring.save
|
|
end
|
|
function RingKeeper:RestoreDefaults(name)
|
|
if name == nil then
|
|
for k, v in pairs(queue) do
|
|
if RK_IsRelevantRingDescription(v) then
|
|
self:SetRing(k, queue[k])
|
|
end
|
|
end
|
|
elseif queue[name] then
|
|
self:SetRing(name, queue[name])
|
|
end
|
|
end
|
|
function RingKeeper:GetDefaultDescription(name)
|
|
assert(type(name) == "string", 'Syntax: desc = RingKeeper:GetDefaultDescription("name")', 2)
|
|
return queue[name] and copy(queue[name]) or false
|
|
end
|
|
function RingKeeper:GenFreeRingName(base, t)
|
|
assert(type(base) == "string" and (t == nil or type(t) == "table"), 'Syntax: name = RingKeeper:GenFreeRingName("base"[, reservedNamesTable])', 2)
|
|
base = base:gsub("[^%a%d]", ""):sub(-10)
|
|
if base:match("^OPie") or not base:match("^%a") then base = "x" .. base end
|
|
local suffix, c = "", 1
|
|
while RK_RingDesc[base .. suffix] or SV[base .. suffix] or (t and t[base .. suffix] ~= nil) or OneRingLib:IsKnownRingName(base .. suffix) do
|
|
suffix, c = math.random(2^c), c < 30 and (c + 1) or c
|
|
end
|
|
return base .. suffix
|
|
end
|
|
function RingKeeper:UnpackABAction(slice)
|
|
if type(slice) == "table" and slice[1] == "macrotext" and type(slice[2]) == "string" then
|
|
local pmt = RK_ParseMacro(slice[2])
|
|
return "macrotext", pmt == "" and slice[2] ~= "" and "#empty" or pmt, unpackABAction(slice, 3)
|
|
else
|
|
return unpackABAction(slice, 1)
|
|
end
|
|
end
|
|
function RingKeeper:IsRingSliceActive(ring, slice)
|
|
return RK_RingDesc[ring] and RK_RingDesc[ring][slice] and RK_RingDesc[ring][slice]._action and true or false
|
|
end
|
|
function RingKeeper:SoftSync(name)
|
|
assert(type(name) == "string", 'Syntax: RingKeeper:SoftSync("name")', 2)
|
|
securecall(RK_SyncRing, name)
|
|
end
|
|
function RingKeeper:GetRingSnapshot(name)
|
|
assert(type(name) == "string", 'Syntax: snapshot = RingKeeper:GetRingSnapshot("name")', 2)
|
|
local ring, first = RK_RingDesc[name] and RK_SerializeDescription(copy(RK_RingDesc[name])) or false, true
|
|
if ring then
|
|
ring.limit, ring.save = type(ring.limit) == "string" and ring.limit:match("[^A-Z]") and "PLAYER" or ring.limit
|
|
for i=1,#ring do
|
|
local v = ring[i]
|
|
if v[1] == nil and type(v.id) == "string" then
|
|
v.id, first = encodeMacro(RK_QuantizeMacro(v.id, not first)), false
|
|
end
|
|
v.caption = nil -- DEPRECATED [2101/3.105/X4]
|
|
v.sliceToken = nil
|
|
end
|
|
end
|
|
return ring and serialize(ring, sSign, sRegRev)
|
|
end
|
|
function RingKeeper:GetSnapshotRing(snap)
|
|
assert(type(snap) == "string", 'Syntax: desc = RingKeeper:GetSnapshotRing("snapshot")', 2)
|
|
local ok, ret = pcall(unserialize, snap, sSign, sReg)
|
|
if ok and type(ret) == "table" and type(ret.name) == "string" and #ret > 0 then
|
|
for i=1,#ret do
|
|
local v = ret[i]
|
|
if not v then
|
|
return
|
|
else
|
|
v.caption = nil -- DEPRECATED [2101/3.105/X4]
|
|
if v[1] == nil and type(v.id) == "string" then
|
|
v.id = decodeMacro(v.id)
|
|
end
|
|
end
|
|
end
|
|
ret.name = ret.name:gsub("|?|", "||")
|
|
ret.quarantineBind, ret.hotkey = type(ret.hotkey) == "string" and ret.hotkey or nil
|
|
ret.quarantineOnOpen, ret.onOpen = ret.onOpen, nil
|
|
return ret
|
|
end
|
|
end
|
|
function RingKeeper:QuantizeMacro(macrotext)
|
|
return RK_QuantizeMacro(macrotext)
|
|
end
|
|
|
|
SV, OneRingLib.ext.RingKeeper = PC:RegisterPVar("RingKeeper", SV, svInitializer), RingKeeper
|
|
AB:AddObserver("internal.collection.preopen", abPreOpen)
|