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