348 lines
9.3 KiB
Lua
348 lines
9.3 KiB
Lua
local AddonName = ...
|
|
local Private = select(2, ...)
|
|
|
|
Private.Features:Register({
|
|
id = "undo",
|
|
autoEnable = {"dev", "pr", "alpha"},
|
|
enabled = true,
|
|
persist = true,
|
|
})
|
|
|
|
local TimeMachine = {
|
|
next = {
|
|
forward = {},
|
|
backward = {}
|
|
},
|
|
transaction = false,
|
|
changes = {},
|
|
actions = {},
|
|
effects = {},
|
|
index = 0,
|
|
sub = Private.CreateSubscribableObject(),
|
|
}
|
|
|
|
Private.TimeMachine = TimeMachine
|
|
|
|
local function resolveKey(data, path)
|
|
if type(path) ~= 'table' then
|
|
return data, path
|
|
end
|
|
local tbl = data
|
|
local i = 1
|
|
while i < #path do
|
|
if tbl[path[i]] == nil then
|
|
tbl[path[i]] = {}
|
|
elseif type(tbl[path[i]]) ~= 'table' then
|
|
error("Path is not valid: " .. table.concat(path, '.') .. " at " .. path[i])
|
|
end
|
|
tbl = tbl[path[i]]
|
|
i = i + 1
|
|
end
|
|
return tbl, path[#path]
|
|
end
|
|
|
|
local function copy(tbl, key)
|
|
if type(tbl[key]) == "table" then
|
|
return CopyTable(tbl[key])
|
|
else
|
|
return tbl[key]
|
|
end
|
|
end
|
|
|
|
function TimeMachine:RegisterEffect(tag, func, idempotent)
|
|
if self.effects[tag] then
|
|
error("Effect already registered: " .. tag)
|
|
end
|
|
self.effects[tag] = {
|
|
idempotent = idempotent,
|
|
func = func
|
|
}
|
|
end
|
|
|
|
TimeMachine:RegisterEffect("add", function(uid, data)
|
|
Private.Add(data)
|
|
end, true)
|
|
|
|
TimeMachine:RegisterEffect("options_cu", function(uid, data)
|
|
if WeakAuras.IsOptionsOpen() then
|
|
WeakAuras.ClearAndUpdateOptions(data.id, true)
|
|
end
|
|
end, true)
|
|
|
|
function TimeMachine:RegisterAction(actionType, actor, inverter, autoEffects)
|
|
if self.actions[actionType] then
|
|
error("Action already registered: " .. actionType)
|
|
end
|
|
self.actions[actionType] = {
|
|
actor = actor,
|
|
inverter = inverter,
|
|
autoEffects = autoEffects,
|
|
}
|
|
end
|
|
TimeMachine:RegisterAction("none",
|
|
function(_data, _path)
|
|
end,
|
|
function(_data, path)
|
|
return 'none', path, nil
|
|
end
|
|
)
|
|
|
|
TimeMachine:RegisterAction("set",
|
|
function(data, path, value)
|
|
local tbl, key = resolveKey(data, path)
|
|
tbl[key] = value
|
|
end,
|
|
function(data, path)
|
|
local tbl, key = resolveKey(data, path)
|
|
return 'set', path, copy(tbl, key)
|
|
end,
|
|
{"add", "options_cu"}
|
|
)
|
|
|
|
TimeMachine:RegisterAction("setmany",
|
|
function(data, path, values)
|
|
local tbl, key = resolveKey(data, path)
|
|
for k, v in pairs(values) do
|
|
tbl[key][k] = v
|
|
end
|
|
end,
|
|
function(data, path, values)
|
|
local tbl, key = resolveKey(data, path)
|
|
local inverse = {}
|
|
for k, v in pairs(values) do
|
|
inverse[k] = copy(tbl[key], k)
|
|
end
|
|
return 'setmany', path, inverse
|
|
end,
|
|
{"add", "options_cu"}
|
|
)
|
|
|
|
TimeMachine:RegisterAction("insert",
|
|
function(data, path, payload)
|
|
local tbl, key = resolveKey(data, path)
|
|
if payload.index == nil then
|
|
table.insert(tbl[key], payload.value)
|
|
else
|
|
table.insert(tbl[key], payload.index, payload.value)
|
|
end
|
|
end,
|
|
function(data, path, payload)
|
|
return 'remove', path, payload.index
|
|
end,
|
|
{"add", "options_cu"}
|
|
)
|
|
|
|
TimeMachine:RegisterAction("remove",
|
|
function(data, path, payload)
|
|
local tbl, key = resolveKey(data, path)
|
|
if payload == nil then
|
|
table.remove(tbl[key])
|
|
else
|
|
table.remove(tbl[key], payload)
|
|
end
|
|
end,
|
|
function(data, path, payload)
|
|
local tbl, key = resolveKey(data, path)
|
|
return 'insert', path, {index = payload, value = copy(tbl[key], payload or #tbl[key])}
|
|
end,
|
|
{"add", "options_cu"}
|
|
)
|
|
|
|
TimeMachine:RegisterAction("swap",
|
|
function(data, path, payload)
|
|
local tbl, key = resolveKey(data, path)
|
|
tbl[key][payload[1]], tbl[key][payload[2]] = tbl[key][payload[2]], tbl[key][payload[1]]
|
|
end,
|
|
function(data, path, payload)
|
|
return 'swap', path, {payload[2], payload[1]}
|
|
end,
|
|
{"add", "options_cu"}
|
|
)
|
|
|
|
TimeMachine:RegisterAction("move",
|
|
function(data, path, payload)
|
|
local tbl, key = resolveKey(data, path)
|
|
local value = table.remove(tbl, payload[1])
|
|
table.insert(tbl[key], payload[2], value)
|
|
end,
|
|
function(data, path, payload)
|
|
return 'move', path, {payload[2], payload[1]}
|
|
end,
|
|
{"add", "options_cu"}
|
|
)
|
|
|
|
local function keyPathToString(path)
|
|
if type(path) == 'table' then
|
|
return table.concat(path, '.')
|
|
else
|
|
return path
|
|
end
|
|
end
|
|
|
|
local function invertEffects(effects)
|
|
local inverted = {}
|
|
for i = #effects, 1, -1 do
|
|
table.insert(inverted, effects[i])
|
|
end
|
|
return inverted
|
|
end
|
|
|
|
function TimeMachine:StartTransaction()
|
|
if self.transaction then
|
|
WeakAuras.prettyPrint("If you're reading this, a time machine transaction was started, but there was already one in progress. That's not supposed to happen. Please report this to the WeakAuras developers, thanks!")
|
|
self:Reject()
|
|
end
|
|
self.transaction = true
|
|
end
|
|
|
|
function TimeMachine:Append(record)
|
|
local action = self.actions[record.actionType]
|
|
Private.DebugPrint("Forward action", record.actionType, "for", record.uid, "at", keyPathToString(record.path), "with", record.payload)
|
|
if not action then
|
|
error("No action for actionType: " .. record.actionType)
|
|
end
|
|
local inverter = action.inverter
|
|
if not inverter then
|
|
error("No inverter for action: " .. record.actionType)
|
|
end
|
|
local actionType, path, payload = inverter(Private.GetDataByUID(record.uid), record.path, record.payload)
|
|
local inverseRecord = {
|
|
uid = record.uid,
|
|
actionType = actionType,
|
|
path = path,
|
|
payload = payload,
|
|
suppressAutoEffects = record.suppressAutoEffects and CopyTable(record.suppressAutoEffects) or nil,
|
|
effects = record.effects and invertEffects(record.effects) or nil,
|
|
}
|
|
Private.DebugPrint("Backward action", actionType, "for", record.uid, "at", keyPathToString(path), "with", payload)
|
|
table.insert(self.next.forward, record)
|
|
table.insert(self.next.backward, 1, inverseRecord)
|
|
if not self.transaction then
|
|
self:Commit(true)
|
|
end
|
|
end
|
|
|
|
function TimeMachine:AppendMany(records)
|
|
local commit = false
|
|
if not self.transaction then
|
|
self:StartTransaction()
|
|
commit = true
|
|
end
|
|
for _, record in ipairs(records) do
|
|
self:Append(record)
|
|
end
|
|
if commit then
|
|
self:Commit()
|
|
end
|
|
end
|
|
|
|
function TimeMachine:Reject()
|
|
self.next = {
|
|
forward = {},
|
|
backward = {}
|
|
}
|
|
self.transaction = false
|
|
end
|
|
|
|
function TimeMachine:Commit(instant)
|
|
if not self.transaction and not instant then
|
|
WeakAuras.prettyPrint("If you're reading this, a time machine transaction was committed, but there was no transaction in progress. That's not supposed to happen. Please report this to the WeakAuras developers, thanks!")
|
|
return
|
|
end
|
|
while self.index < #self.changes do
|
|
table.remove(self.changes)
|
|
end
|
|
table.insert(self.changes, self.next)
|
|
self.next = {
|
|
forward = {},
|
|
backward = {}
|
|
}
|
|
self.transaction = false
|
|
return self:StepForward()
|
|
end
|
|
|
|
function TimeMachine:Apply(records, delayedEffects)
|
|
for _, record in ipairs(records) do
|
|
local action = self.actions[record.actionType]
|
|
if not action then
|
|
error("No action for actionType: " .. record.actionType)
|
|
end
|
|
local data = Private.GetDataByUID(record.uid)
|
|
action.actor(data, record.path, record.payload)
|
|
if action.autoEffects or record.effects then
|
|
local effects = {}
|
|
if action.autoEffects then
|
|
for _, effect in ipairs(action.autoEffects) do
|
|
if not record.suppressAutoEffects or not record.suppressAutoEffects[effect] then
|
|
table.insert(effects, effect)
|
|
end
|
|
end
|
|
end
|
|
if record.effects then
|
|
for _, effect in ipairs(record.effects) do
|
|
table.insert(effects, effect)
|
|
end
|
|
end
|
|
for _, effectType in ipairs(effects) do
|
|
local effect = self.effects[effectType]
|
|
if not effect then
|
|
error("No effect for effectType: " .. effect)
|
|
end
|
|
if not delayedEffects or not effect.idempotent then
|
|
if not record.effects or record.suppressAutoEffects then
|
|
effect.func(record.uid, data)
|
|
end
|
|
else
|
|
delayedEffects[record.uid] = delayedEffects[record.uid] or {}
|
|
delayedEffects[record.uid][effectType] = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return delayedEffects
|
|
end
|
|
|
|
function TimeMachine:StepForward()
|
|
if self.index < #self.changes then
|
|
self.index = self.index + 1
|
|
self:Apply(self.changes[self.index].forward)
|
|
if self.sub:HasSubscribers("Step") then
|
|
self.sub:Notify("Step", self.index)
|
|
end
|
|
end
|
|
end
|
|
|
|
function TimeMachine:StepBackward()
|
|
if self.index > 0 then
|
|
self:Apply(self.changes[self.index].backward)
|
|
self.index = self.index - 1
|
|
if self.sub:HasSubscribers("Step") then
|
|
self.sub:Notify("Step", self.index)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- much safer than the name suggests!
|
|
function TimeMachine:DestroyTheUniverse(id)
|
|
if self.transaction then
|
|
WeakAuras.prettyPrint("If you're reading this, a time machine transaction was destroyed, but there was one in progress. That's not supposed to happen. Please report this to the WeakAuras developers, thanks!")
|
|
self:Reject()
|
|
end
|
|
if #self.changes > 0 then
|
|
Private.DebugPrint(string.format("Destroying the universe where %i change(s) happpened, because an unexpected change happened to %q.", #self.changes, id))
|
|
end
|
|
self.changes = {}
|
|
self.index = 0
|
|
if self.sub:HasSubscribers("Step") then
|
|
self.sub:Notify("Step", self.index)
|
|
end
|
|
end
|
|
|
|
function TimeMachine:DescribeNext()
|
|
return self.changes[self.index + 1] and self.changes[self.index + 1].forward
|
|
end
|
|
|
|
function TimeMachine:DescribePrevious()
|
|
return self.changes[self.index] and self.changes[self.index].backward
|
|
end
|