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)