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.

566 lines
22 KiB

5 years ago
local apiV, AB, MAJ, REV, ext, T = {}, {}, 2, 26, ...
if T.ActionBook then return end
apiV[MAJ], ext, T.Kindred, T.Rewire = AB, {Kindred=T.Kindred, Rewire=T.Rewire, ActionBook={}}
local function assert(condition, err, ...)
return (not condition) and error(tostring(err):format(...), 3) or condition
end
local safequote do
local r = {u="\\117", ["{"]="\\123", ["}"]="\\125"}
function safequote(s)
return (("%q"):format(s):gsub("[{}u]", r))
end
end
local listToString do
local function walk(p, n, a, ...)
if n > 0 then
local at, c = type(a), (p == "" and "" or ", ")
if at == "string" then
p = p .. c .. safequote(a)
elseif at == "number" then
p = p .. c .. ("%g"):format(a)
else
-- RE unpack does not accept start/finish indices, so replace nil/invalid values by a special table.
p = p .. c .. (at == "boolean" and (a and "true" or "false") or "_NIL")
end
return walk(p, n-1, ...)
end
return p
end
function listToString(...)
return walk('', select("#", ...), ...)
end
end
local istore do
local function write(t, n, i, a, b, ...) -- overwrites by up to 1 element
if n > 0 then
t[i], t[i+1] = a, b
return write(t, n-2, i+2, ...)
end
return i+n-1
end
local base, api, inner = newproxy(true), {}, setmetatable({}, {__mode="k"}), {}
function istore()
local r, d = newproxy(base), {[0]=0}
inner[r] = d
return r
end
function api:reset()
local h = inner[self]
h[0] = 0, wipe(h)
end
function api:insert(...)
local h, n = inner[self], select("#", ...)
if n > 0 then
local c = h[0]
h[0], h[-c-1] = c+1, write(h, n, h[-c]+1, ...)
end
end
function api:filter(func, farg)
local h, c, ni, first = inner[self], 0, 0, 0
for i=1, h[0] do
local last = h[-i]
if securecall(func, farg, unpack(h, first+1, last)) then
c, h[-c-1] = c + 1, ni + last-first
for j=1,last == h[-c] and 0 or (last-first) do
h[ni+j] = h[first+j]
end
ni = h[-c]
end
first = last
end
h[0] = c
end
function api:size()
return inner[self][0]
end
function api:get(i)
local h = inner[self]
if i > 0 and h[-i] then
return unpack(h, (i == 1 and 0 or h[-i+1]) + 1, h[-i])
end
end
local meta = getmetatable(base)
meta.__index, meta.__len, meta.__call = api, api.size, api.get
end
local LW, L do
local LT, LR = {}
local function lookup(_, k)
return LT[k] or k
end
LR, LW = newproxy(true), newproxy(true)
local LM, LWM = getmetatable(LR), getmetatable(LW)
LM.__index, LM.__call = lookup, lookup
function LWM:__newindex(k, v)
if type(k) ~= "string" or (v ~= nil and type(v) ~= "string") then
error("Localization phrase invalid", 2)
end
LT[k] = v
end
ext.ActionBook.L = LR
end
local actionCallbacks, core, coreEnv = {}, CreateFrame("Frame", nil, nil, "SecureHandlerBaseTemplate") do
core:SetFrameRef("KR", ext.Kindred:compatible(1,0):seclib())
for s in ("0123456789QWERTYUIOP"):gmatch(".") do
local bni, bn = 1 repeat
bn, bni = "AB!" .. bni .. s, bni + 1
until GetClickFrame(bn) == nil
core:WrapScript(CreateFrame("Button", bn, core, "SecureActionButtonTemplate"), "OnClick",
[[-- AB:OnClick_Pre
local t = actInfo[tonumber(button)]
busy[self], idle[self] = t
for i=3, 2+t[2], 2 do
self:SetAttribute(t[i], t[i+1])
end
return nil, 1
]], [[-- AB:OnClick_Post
local t = busy[self]
idle[self], busy[self] = self:GetName()
for i=3, 2+t[2], 2 do
self:SetAttribute(t[i], nil)
end
]])
end
core:Execute([=[-- AB_Init
collections, tokens, metadata, actConditionals, tokConditionals = newtable(), newtable(), newtable(), newtable(), newtable()
actInfo, busy, idle, _NIL, sidCastID = newtable(), newtable(), newtable(), newtable(), 30 + math.random(9)
actInfo[sidCastID] = newtable("attribute", 6, "spell",nil, "unit",nil, "type","spell")
for _, c in pairs(self:GetChildList(newtable())) do idle[c] = c:GetName() end
KR, colStack, idxStack, ecStack, onStack, outCount = self:GetFrameRef("KR"), newtable(), newtable(), newtable(), newtable(), newtable()
]=])
coreEnv = GetManagedEnvironment(core)
end
core:SetAttribute("GetCollectionContent", [[-- AB:GetCollectionContent(slot)
local i, ret, root, col, idx, aid, ecol = 1, "", tonumber((...)) or 0
wipe(outCount) wipe(onStack)
colStack[i], idxStack[i], ecStack[i] = root, 1, nil
repeat
col, idx, ecol = colStack[i], idxStack[i], ecStack[i]
if idx == 1 and not outCount[col] then
if outCount[col] == nil then
self:CallMethod("notifyCollectionOpen", col)
end
if not ecol then
onStack[col], outCount[col] = true, 0
end
end
aid, idxStack[i] = collections[col][idx], idx + 1
if aid then
local tok = tokens[col][idx]
local check1, check2 = actConditionals[aid], tokConditionals[tok]
if (check1 == nil or (KR:RunAttribute("EvaluateCmdOptions", check1) or "hide") ~= "hide") and
(check2 == nil or (KR:RunAttribute("EvaluateCmdOptions", check2) or "hide") ~= "hide") then
local isCollection = collections[aid]
local tem = isCollection and metadata["tokEmbed-" .. tok]
local isEmbed = isCollection and (tem == nil and metadata["embed-" .. aid] or tem)
if isEmbed then
local canEmbed = true
for j=1, i-1 do
if colStack[j] == aid then
canEmbed = false
break
end
end
if canEmbed then
i, colStack[i+1], idxStack[i+1], ecStack[i+1] = i + 1, aid, 1, ecol or col, ecol or col
end
elseif isCollection and not outCount[aid] then
i, idxStack[i], colStack[i+1], idxStack[i+1], ecStack[i+1] = i + 1, idx, aid, 1, false
elseif (outCount[aid] or 1) > 0 or onStack[aid] then
local col = ecol or col
local nid = outCount[col] + 1
ret = ret .. "\n" .. col .. " " .. nid .. " " .. aid .. " " .. tok
outCount[col] = nid
end
end
else
if not ecol then
onStack[col] = nil
local openAction = (i == 1 or (outCount[col] or 0) > 0) and metadata["openAction-" .. col]
if openAction then
ret = ret .. "\n" .. col .. " 0 " .. openAction .. " AOOA::" .. col
end
end
i = i - 1
end
until i == 0
return ret, metadata["openAction-" .. root] -- 2nd return is deprecated; use idx 0 actions in ret
]])
core:SetAttribute("UseAction", [[-- AB:UseAction(slot[, ...])
local at = actInfo[...]
if at == "icall" then
return self:CallMethod("icall", ...) or ""
elseif type(at) ~= "table" then
elseif at[1] == "attribute" then
local _, name = next(idle)
return ("/click %s %d"):format(name, ...)
elseif at[1] == "recall" then
return at[2]:RunAttribute(select(3, unpack(at))) or ""
end
return ""
]])
core:SetAttribute("CastSpellByID", [[-- AB:CastSpellByID(sid[, "target"])
local at = actInfo[sidCastID]
at[4], at[6] = ...
return self:RunAttribute("UseAction", sidCastID)
]])
function core:notifyCollectionOpen(id)
AB:NotifyObservers("internal.collection.preopen", id)
end
function core:icall(id)
local t = actionCallbacks[id]
local ttype = type(t)
if ttype == "table" then
t[1](unpack(t,3,t[2]))
elseif ttype == "function" then
t()
end
end
local DeferExecute do
local q = {}
function DeferExecute(block)
if InCombatLockdown() then
q[#q+1] = block
elseif type(block) == "function" then
securecall(block)
else
core:Execute(block)
end
end
core:SetScript("OnEvent", function()
for i=1,#q do
q[i] = DeferExecute(q[i])
end
end)
core:RegisterEvent("PLAYER_REGEN_ENABLED")
end
local actionCreators, actionDescribers, optData, optStart, optEnd, categories = {}, {}, {}, {}, {}, {}
local nextActionId, allocatedActions, allocatedActionType, allocatedActionArg = 42, {}, {}, {}
local createHandlers, updateHandlers = {}, {}
function createHandlers.attribute(id, cnt, ...)
DeferExecute(("actInfo[%d] = newtable(%s)"):format(id, listToString("attribute", cnt, ...)))
return true
end
function createHandlers.func(id, cnt, func, ...)
if type(func) ~= "function" then
return false, "Callback expected, got %s", type(func)
end
DeferExecute(("actInfo[%d] = 'icall'"):format(id))
actionCallbacks[id] = cnt > 1 and {func, cnt+1, ...} or func
return true
end
function createHandlers.recall(id, _count, handle, attr, ...)
if type(handle) ~= "table" or type(attr) ~= "string" then
return false, "Frame handle, attribute method expected; got %s/%s", type(handle), type(attr)
elseif type(handle.IsProtected) ~= "function" or type(handle.GetAttribute) ~= "function" then
return false, "Frame handle expected; got %s", type(handle)
elseif not select(2, handle:IsProtected()) then
return false, "Callback frame must be explicitly protected"
elseif type(handle:GetAttribute(attr)) ~= "string" then
return false, "Callback snippet expected in attribute %q, got %s", attr, type(handle:GetAttribute(attr))
end
local s = ("local t = newtable(%s)\n t[2] = self:GetFrameRef('recall-ref')\nactInfo[%d] = t"):format(listToString("recall", nil, attr, ...), id)
DeferExecute(function()
core:SetFrameRef('recall-ref', handle)
core:Execute(s)
end)
return true
end
function updateHandlers.collection(id, _count, idList)
if not (type(idList) == "table") then
return false, "Expected table specifying collection actions, got %q", type(idList)
elseif idList.__openAction and not allocatedActions[idList.__openAction] then
return false, "Collection __openAction key does not specify a valid action"
end
local spec, tokens, visibility = "", "", ""
for i=1,#idList do
local tok = idList[i]
local aid, vis, emb = idList[tok], idList['__visibility-' .. tok], idList['__embed-' .. tok]
if type(tok) ~= "string" then
return false, "Collection entry #%d: unsupported token type (%s)", i, type(tok)
elseif not tok:match("^[A-Za-z][A-Za-z0-9_=/]*$") then
return false, "Collection entry #%d: invalid token format (%s)", i, tok
elseif allocatedActions[aid] == nil then
return false, "Collection entry #%d: unallocated action id", i, tostring(idList[i])
elseif vis ~= nil and type(vis) ~= "string" then
return false, "Collection entry #%d: unsupported visibility conditional type (%s)", i, type(vis)
elseif emb ~= nil and type(emb) ~= "boolean" then
return false, "Collection entry #%d: unsupported embed flag type (%s)", i, type(emb)
end
vis = vis and safequote(vis) or "nil"
emb = emb == nil and "nil" or tostring(emb)
spec, tokens, visibility = spec .. idList[tok] .. ", ", tokens .. safequote(tok) .. ", ", ('%s\ntk = %s; tokConditionals[tk], metadata["tokEmbed-" .. tk] = %s, %s'):format(visibility, safequote(tok), vis, emb)
end
local openAction = type(idList.__openAction) == "number" and idList.__openAction or "nil"
local embed = type(idList.__embed) == "boolean" and tostring(idList.__embed) or "nil"
DeferExecute(("local id, tk = %d; collections[id], tokens[id], metadata['openAction-' .. id], metadata['embed-' .. id] = newtable(%s nil), newtable(%s nil), %s, %s; %s")
:format(id, spec, tokens, openAction, embed, visibility))
return true
end
function createHandlers.clone(id, _count, id2)
DeferExecute(("local d,s = %d, %d; actInfo[d], actConditionals[d] = actInfo[s], actConditionals[s]"):format(id, id2))
return true
end
local function nullInfoFunc() return false end
local getActionIdent, getActionArgs do
function getActionIdent(identTable)
local itt = type(identTable)
if itt == "string" then
return identTable, nil
elseif itt == "table" and type(identTable[1]) == "string" then
return identTable[1], identTable
end
end
local function index(t, a, ...)
if a then
return t[a], index(t, ...)
end
end
local function unfold(t, i)
if t[i] then
return t[i], unfold(t, i+1)
elseif optStart[t[1]] and optEnd[t[1]] then
return index(t, unpack(optData, optStart[t[1]], optEnd[t[1]]))
end
end
function getActionArgs(it, ...)
if it then
return unfold(it, 1)
end
return ...
end
end
local categoryAliases = {}
local function getCategoryName(id)
local LM = id == (#categories+1) and L"Miscellaneous"
return categories[id] or (LM and categories[LM] and LM) or nil
end
local function getCategoryTable(name)
name = categoryAliases[name] or name
local r = categories[name]
if not r then
r = {}
categories[name] = r
if name ~= L"Miscellaneous" then
categories[#categories + 1] = name
end
end
return r
end
local editorPanels = {}
do -- AB:CreateToken()
local seq, dict, dictLength, prefix = 262143, "qwer1tyui2opas3dfgh4jklz5xcvb6nmQWE7RTYU8IOPA9SDFG0HJKL=ZXCV/BNM", 64
local function encode(n)
local ret, d = ""
repeat
d = n % dictLength
ret, n = dict:sub(d+1, d+1) .. ret, (n - d) / dictLength
until n == 0
return ret
end
function AB:CreateToken()
if seq > 262142 then
prefix, seq = "ABu" .. encode(time()*100+(math.floor(GetTime()%1*100)%100)), 0
end
seq = seq + 1
return prefix .. encode(seq)
end
end
function AB:GetActionSlot(actionType, ...)
local ident, at = getActionIdent(actionType)
local id = actionCreators[ident] and actionCreators[ident](getActionArgs(at, ...))
if allocatedActions[id] then
return id
end
assert(ident, 'Syntax: actionId = ActionBook:GetActionSlot(actionTable or "actionType", ...)')
end
function AB:GetActionDescription(actionType, ...)
local ident, at = getActionIdent(actionType)
if actionDescribers[ident] then
return actionDescribers[ident](getActionArgs(at, ...))
end
assert(ident, 'Syntax: typeName, actionName, icon, ext, tipFunc, tipArg, actionType = ActionBook:GetActionDescription(actionTable or "actionType", ...)')
end
function AB:GetActionOptions(actionType)
-- DEPRECATED: Named action options will be going away; use the array portion + :RegisterEditorPanel
assert(type(actionType) == "string", 'Syntax: ... = ActionBook:GetActionOptions("actionType")')
return unpack(optData, optStart[actionType] or 0, optEnd[actionType] or -1)
end
function AB:GetSlotInfo(id, modLock)
assert(type(id) == "number" and (modLock == nil or type(modLock) == "string"), 'Syntax: usable, state, icon, caption, count, cdLeft, cdLength, tipFunc, tipArg, ext = ActionBook:GetSlotInfo(slot[, "modLockState"])')
if allocatedActions[id] then
return allocatedActions[id](allocatedActionArg[id], modLock)
end
end
function AB:GetSlotImplementation(id)
assert(type(id) == "number", "Syntax: actionType, colEntryCount, colEmbedDefault = ActionBook:GetSlotImplementation(slot)")
local aType = allocatedActions[id] and allocatedActionType[id]
local colData = coreEnv.collections[id]
return aType, colData and #colData or nil, colData and coreEnv.metadata['embed-' .. id]
end
function AB:RegisterActionType(actionType, create, describe, opt)
assert(type(actionType) == "string" and type(create) == "function" and type(describe) == "function" and (opt == nil or type(opt) == "table"), 'Syntax: ActionBook:RegisterActionType("actionType", createFunc, describeFunc[, {options}])')
assert(not actionCreators[actionType], "Identifier %q is already registered", actionType)
actionCreators[actionType], actionDescribers[actionType] = create, describe
if opt and #opt > 0 then
local ok
for i=0,#optData-1 do ok = i for j=1,#opt do if opt[j] ~= optData[i+j] then ok = nil break end end if ok then break end end
if not ok then ok = #optData for i=1,#opt do optData[ok+i] = opt[i] end end
optStart[actionType], optEnd[actionType] = ok+1, ok + #opt
end
end
function AB:CreateActionSlot(infoFunc, infoArg, implType, ...)
local as, an = 1, select("#", ...)
local cnd if implType == "conditional" then
as, an, cnd, implType = as + 2, an - 2, select(as, ...)
assert(type(cnd) == "string" and type(implType) == "string", "Conditional option/implementation type expected, got %s/%s", type(cnd), type(implType))
end
assert(type(implType) == "string" and (infoFunc == nil or type(infoFunc) == "function"), 'Syntax: slot = ActionBook:CreateAction(infoFunc, infoArg, "implType", ...)')
local cf, id = assert(createHandlers[implType] or updateHandlers[implType], "Implementation type %q is not creatable", implType), nextActionId
nextActionId, allocatedActionType[id], allocatedActionArg[id] = id + 1, implType, infoArg
assert(cf(id, an, select(as, ...)))
allocatedActions[id] = infoFunc or nullInfoFunc
if cnd then DeferExecute(("actConditionals[%d] = %s"):format(id, safequote(cnd))) end
return id
end
function AB:UpdateActionSlot(id, ...)
assert(type(id) == "number", "Syntax: ActionBook:UpdateActionSlot(actionId, ...)")
assert(allocatedActions[id] and allocatedActionType[id], "Action %d does not exist", id)
assert(updateHandlers[allocatedActionType[id]], "Action type %q is not updatable", allocatedActionType[id])
assert(updateHandlers[allocatedActionType[id]](id, select("#", ...), ...))
end
function AB:AddCategoryAlias(name, aliasOf)
assert(type(name) == "string" and type(aliasOf) == "string", 'Syntax: ActionBook:AddCategoryAlias("name", "aliasOf")')
assert(name == aliasOf or categories[name] == nil, "Category %q already exists.", name)
categoryAliases[name] = categoryAliases[name] or aliasOf
end
function AB:AugmentCategory(name, augFunc)
assert(type(name) == "string" and type(augFunc) == "function" and name ~= "*", 'Syntax: ActionBook:AugmentCategory("name", augFunc)')
table.insert(getCategoryTable(name), augFunc)
end
function AB:AddActionToCategory(name, actionType, ...)
assert(type(name) == "string" and type(actionType) == "string" and name ~= "*", 'Syntax: ActionBook:AddActionToCategory("name", "actionType"[, ...])')
local ct = getCategoryTable(name)
ct.extra = ct.extra or istore()
ct.extra:insert(actionType, ...)
end
function AB:GetNumCategories()
return #categories + (categories[L"Miscellaneous"] and 1 or 0)
end
function AB:GetCategoryInfo(id)
assert(type(id) == "number", 'Syntax: name = ActionBook:GetCategoryInfo(index)')
return getCategoryName(id)
end
function AB:GetCategoryContents(id, into)
assert(type(id) == "number", 'Syntax: contents = ActionBook:GetCategoryContents(index[, into])')
local cname, ret = assert(getCategoryName(id), 'Invalid category index'), into or istore()
local co = categories[cname]
local ex, addToRet = co.extra, #co > 0 and function(...) return ret:insert(...) end
for i=1,#co do
securecall(co[i], cname, addToRet)
end
for i=1,ex and #ex or 0 do
ret:insert(ex(i))
end
return ret
end
local notifyCount, observers = 1, {["*"]={}, ["internal.collection.preopen"] = {}}
function AB:NotifyObservers(ident, data)
assert(type(ident) == "string", 'Syntax: ActionBook:NotifyObservers("identifier"[, data])')
assert(actionCreators[ident] or observers[ident] ~= nil, "Identifier %q is not registered", ident)
notifyCount = (notifyCount + 1) % 4503599627370495
for i=ident == "*" or not observers[ident] and 1 or 2, 1, -1 do
for k,v in pairs(observers[i == 1 and "*" or ident]) do
securecall(k, v, ident, data)
end
end
end
function AB:AddObserver(ident, callback, selfarg)
assert(type(ident) == "string" and type(callback) == "function", 'Syntax: ActionBook:AddObserver("identifier", callbackFunc, callbackSelfArg)')
assert(ident == "*" or actionCreators[ident] or observers[ident], "Identifier %q is not registered", ident)
if observers[ident] == nil then observers[ident] = {} end
observers[ident][callback] = selfarg == nil and true or selfarg
end
function AB:GetLastObserverUpdateToken(ident)
assert(type(ident) == "string", 'Syntax: token = ActionBook:GetLastObserverUpdateToken("identifier")')
assert(ident == "*" or actionCreators[ident], "Identifier %q is not registered", ident)
return notifyCount
end
function AB:RegisterEditorPanel(actionType, editorPanel)
assert(type(actionType) == "string" and type(editorPanel) == "table", 'Syntax: ActionBook:RegisterEditorPanel("actionType", editorPanel)')
assert(actionCreators[actionType] ~= nil, "actionType %q is not registered", actionType)
assert(editorPanels[actionType] == nil, "An editor for %q is already registered", actionType)
assert(
type(editorPanel.SetAction) == "function" and
type(editorPanel.GetAction) == "function" and
type(editorPanel.IsOwned) == "function" and
type(editorPanel.Release) == "function",
"Required editor panel API methods not implemented")
editorPanels[actionType] = editorPanel
end
function AB:GetEditorPanel(actionType)
return editorPanels[actionType]
end
function AB:seclib()
return core
end
function AB:compatible(module, maj, rev, ...)
if ext[module] then
return ext[module]:compatible(maj, rev, ...)
elseif type(module) == "number" and type(maj) == "number" and rev == nil then
maj, rev = module, maj -- Oh hey, it's us!
if maj == 1 then securecall(error, "ActionBook v1 API is deprecated.", 3) end
return rev <= REV and apiV[maj], MAJ, REV
end
end
function AB:locale(getWritableHandle)
if getWritableHandle then
local r = LW
LW = nil
return assert(r, "A writable locale handle has already been returned")
end
return L
end
apiV[1] = {uniq=AB.CreateToken, get=AB.GetActionSlot, describe=AB.GetActionDescription, info=AB.GetSlotInfo, options=AB.GetActionOptions, actionType=AB.GetSlotImplementation,
register=AB.RegisterActionType, update=AB.UpdateActionSlot, notify=AB.NotifyObservers, observe=AB.AddObserver, lastupdate=AB.GetLastObserverUpdateToken, compatible=AB.compatible} do
apiV[1].miscaction = function(_self, ...) return AB:AddActionToCategory(L"Miscellaneous", ...) end
apiV[1].category = function(_self, name, numFunc, getFunc)
assert(type(name) == "string" and type(numFunc) == "function" and type(getFunc) == "function", 'Syntax: ActionBook:category("name", countFunc, entryFunc)')
local count = numFunc()
assert(type(count) == "number" and count >= 0, "countFunc() must return a non-negative integer")
return AB:AugmentCategory(name, function(_, add)
for i=1, numFunc() do
add(getFunc(i))
end
end)
end
local proxy = CreateFrame("Frame", nil, nil, "SecureHandlerBaseTemplate")
proxy:SetFrameRef("core", core)
proxy:Execute("core = self:GetFrameRef('core'), self:SetAttribute('frameref-core', nil)")
proxy:SetAttribute("collection", "return core:RunAttribute('GetCollectionContent', ...)")
proxy:SetAttribute("triggerMacro", "return core:RunAttribute('UseAction', ...)")
apiV[1].seclib = function() return proxy end
apiV[1].create = function(_self, atype, hint, ...)
assert(type(atype) == "string" and (hint == nil or type(hint) == "function"), 'Syntax: id = ActionBook:create("actionType", hintFunc, ...)')
return AB:CreateActionSlot(hint, nextActionId, atype, ...)
end
end
T.ActionBook, ext.ActionBook.compatible = ext.ActionBook, AB.compatible
L = T.ActionBook.L -- TODO: Remove this when write tracing works