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.
589 lines
22 KiB
589 lines
22 KiB
--[[
|
|
Archivist - Data management service for WoW Addons
|
|
Written in 2019 by Allen Faure (emptyrivers) afaure6@gmail.com
|
|
|
|
To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide.
|
|
This software is distributed without any warranty.
|
|
You should have received a copy of the CC0 Public Domain Dedication along with this software.
|
|
If not, see http://creativecommons.org/publicdomain/zero/1.0/.
|
|
]]
|
|
|
|
local embedder, namespace = ...
|
|
local addonName, Archivist = "Archivist", {}
|
|
-- Our only library!
|
|
local LibDeflate = LibStub("LibDeflate")
|
|
|
|
do -- boilerplate & static values
|
|
Archivist.buildDate = "@build-time@"
|
|
Archivist.version = "v1.0.8"
|
|
--[==[@debug@
|
|
Archivist.debug = true
|
|
--@end-debug@]==]
|
|
|
|
Archivist.prototypes = {}
|
|
Archivist.storeMap = {}
|
|
Archivist.activeStores = {}
|
|
namespace.Archivist = Archivist
|
|
local unloader = CreateFrame("FRAME")
|
|
unloader:RegisterEvent("PLAYER_LOGOUT")
|
|
unloader:SetScript("OnEvent", function()
|
|
Archivist:DeInitialize()
|
|
end)
|
|
if embedder == "Archivist" then
|
|
-- Archivist is installed as a standalone addon.
|
|
-- The Archive is in the default location, ACHV_DB
|
|
_G.Archivist = Archivist
|
|
local loader = CreateFrame("frame")
|
|
loader:RegisterEvent("ADDON_LOADED")
|
|
loader:SetScript("OnEvent", function(self, _, addon)
|
|
if addon == addonName then
|
|
if type(ACHV_DB) ~= "table" then
|
|
ACHV_DB = {}
|
|
end
|
|
Archivist:Initialize(ACHV_DB)
|
|
self:UnregisterEvent("ADDON_LOADED")
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
function Archivist:Assert(valid, pattern, ...)
|
|
if not valid then
|
|
if pattern then
|
|
error(pattern:format(...), 2)
|
|
else
|
|
error("Archivist encountered an unknown error.", 2)
|
|
end
|
|
end
|
|
end
|
|
|
|
function Archivist:Warn(valid, pattern, ...) -- Like assert, but doesn't interrupt execution
|
|
if not valid and self.debug then
|
|
if pattern then
|
|
print(pattern:format(...), 2)
|
|
else
|
|
print("Archivist encountered an unknown warning.")
|
|
end
|
|
return true
|
|
end
|
|
end
|
|
|
|
function Archivist:IsInitialized()
|
|
return self.initialized
|
|
end
|
|
|
|
-- Give Archivist its archive to play with. Called automatically, unless Archivist has been embedded.
|
|
function Archivist:Initialize(sv)
|
|
do -- arg validation
|
|
self:Assert(not self:IsInitialized(), "Archivist has already been initialized.")
|
|
self:Assert(type(sv) == "table", "Attempt to initialize Archivist SavedVariables with a %q instead of a table.", type(sv))
|
|
end
|
|
|
|
self.sv = sv
|
|
self.initialized = true
|
|
for id, prototype in pairs(self.prototypes) do
|
|
self.sv[id] = self.sv[id] or {}
|
|
if prototype.Init then
|
|
prototype:Init()
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Shut Archivist down
|
|
function Archivist:DeInitialize()
|
|
if self:IsInitialized() then
|
|
self.initialized = false
|
|
self:CloseAllStores()
|
|
self.sv = nil
|
|
end
|
|
end
|
|
|
|
-- register a store type with Archivist
|
|
-- prototype fields:
|
|
-- id - unique identifier. Preferably also a descriptive name, like "simple" or "snapshot".
|
|
-- version - positive integer. Used for version control, in case any data migrations are needed. Registration will fail if the prototype is outdated.
|
|
-- Init - function (optional). If provided, executes exactly once per session, before any other methods are called.
|
|
-- Create - function (required). Create a brand new active store object.
|
|
-- Update - function (optional). Massage archived data into a format that Open can accept. Useful for data migrations.
|
|
-- Open - function (requried). Create from the provided data an active store object. Prototype may assume ownership of the provided data however it wishes.
|
|
-- Commit - function (required). Return an image of the data that should be archived.
|
|
-- Close - function (required). Release ownership of active store object. Optionally, return image of data to write into archive.
|
|
-- Delete - function (optional). If provided, called when a store is deleted. Useful for cleaning up sub stores.
|
|
-- Please note that Create, Open, Update (if provided), Commit, and Close may be called at any time if Archivist deems it necessary.
|
|
-- Thus, these methods should ideally be as close to purely functional as is practical, to minimize friction.
|
|
function Archivist:RegisterStoreType(prototype)
|
|
do -- prototype validation
|
|
self:Assert(type(prototype) == "table", "Invalid argument #1 to RegisterStoreType: Expected table, got %q instead.", type(prototype))
|
|
-- prototype is now guaranteed to be indexable
|
|
self:Assert(type(prototype.id) == "string", "Invalid prototype field 'id': Expected string, got %q instead.", type(prototype.id))
|
|
self:Assert(type(prototype.version) == "number", "Invalid prototype field 'version': Expected number, got %q instead.", type(prototype.version))
|
|
if self:Warn(prototype.version > 0 and prototype.version == math.floor(prototype.version),
|
|
"Prototype %q version expected to be a positive integer, but got %d instead.", prototype.id, prototype.version) then
|
|
return
|
|
end
|
|
local oldPrototype = self.prototypes[prototype.id]
|
|
self:Assert(not oldPrototype or prototype.version >= oldPrototype.version, "Store type %q already exists with a higher version", oldPrototype and oldPrototype.version)
|
|
-- prototype is now guaranteed to be either new or an Update to existing prototype
|
|
self:Assert(prototype.Init == nil or type(prototype.Init) == "function", "Invalid prototype field 'Init': Expected function, got %q instead.", type(prototype.Init))
|
|
self:Assert(type(prototype.Create) == "function", "Invalid prototype field 'Create': Expected function, got %q instead.", type(prototype.Create))
|
|
self:Assert(type(prototype.Open) == "function", "Invalid prototype field 'Open': Expected function, got %q instead.", type(prototype.Open))
|
|
self:Assert(prototype.Update == nil or type(prototype.Update) == "function", "Invalid prototype field 'Update': Expected function, got %q instead.", type(prototype.Update))
|
|
self:Assert(type(prototype.Commit) == "function", "Invalid prototype field 'Commit': Expected function, got %q instead.", type(prototype.Commit))
|
|
self:Assert(type(prototype.Close) == "function", "Invalid prototype field 'Close': Expected function, got %q instead.", type(prototype.Close))
|
|
self:Assert(prototype.Delete == nil or type(prototype.Delete) == "function", "Invalid prototype field 'Delete': Expected function, got %q instead.", type(prototype.Delete))
|
|
-- prototype is now guaranteed to have Init, Create, Open, Update functions, and is thus well-formed.
|
|
end
|
|
|
|
local oldPrototype = self.prototypes[prototype.id] -- need in case of closing active stores
|
|
self.prototypes[prototype.id] = {
|
|
id = prototype.id,
|
|
version = prototype.version,
|
|
Init = prototype.Init,
|
|
Create = prototype.Create,
|
|
Update = prototype.Update,
|
|
Open = prototype.Open,
|
|
Commit = prototype.Commit,
|
|
Close = prototype.Close,
|
|
Delete = prototype.Delete
|
|
}
|
|
self.activeStores[prototype.id] = self.activeStores[prototype.id] or {}
|
|
if self:IsInitialized() then
|
|
self.sv[prototype.id] = self.sv[prototype.id] or {}
|
|
if prototype.Init then
|
|
prototype:Init()
|
|
end
|
|
-- if prototype was previously registered, and Archivist is initialized, then there may be open stores of the old prototype.
|
|
-- Close them, Update if necessary, then re-Open them with the new prototype.
|
|
if oldPrototype then
|
|
for storeID, store in pairs(self.activeStores[prototype.id]) do
|
|
local image = oldPrototype:Close(store)
|
|
local saved = self.sv[prototype.id][storeID]
|
|
local shouldReArchive = image ~= nil
|
|
if image == nil then
|
|
image = saved.data
|
|
end
|
|
if prototype.Update then
|
|
local newImage = prototype:Update(image, saved.version)
|
|
if newImage ~= nil then
|
|
image = newImage
|
|
shouldReArchive = true
|
|
end
|
|
saved.version = prototype.version
|
|
end
|
|
self.activeStores[prototype.id][storeID] = prototype:Open(image)
|
|
if shouldReArchive then
|
|
-- a meaningful change to saved data has occurred.
|
|
saved.data = self:Archive(image)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
do -- function Archive:GenerateID()
|
|
-- adapted from https://gist.github.com/jrus/3197011
|
|
local function randomHex()
|
|
return ('%x'):format(math.random(0, 0xf))
|
|
end
|
|
|
|
function Archivist:GenerateID()
|
|
local template ='xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
|
|
return (template:gsub('x', randomHex))
|
|
end
|
|
end
|
|
|
|
-- creates and opens a new store of the given store type and with the given id (if given)
|
|
-- store objects are lightly managed by Archivist. On PLAYER_LOGOUT, all open stores are Closed,
|
|
-- and the resultant data is compressed into the archive.
|
|
function Archivist:Create(storeType, id, ...)
|
|
do -- arg validation
|
|
self:Assert(type(storeType) == "string" and self.prototypes[storeType], "Store type must be registered before loading data.")
|
|
self:Assert(id == nil or type(id) == "string" and not self.sv[storeType][id], "A store already exists with that id. Did you mean to call Archivist:Open?")
|
|
end
|
|
|
|
local store, image = self.prototypes[storeType]:Create(...)
|
|
do -- ensure that store exists and is unique
|
|
self:Assert(store ~= nil, "Failed to create a new store of type %q.", storeType)
|
|
self:Assert(self.storeMap[store] == nil, "Store Type %q produced an store object already registered with Archivist instead of creating a new one.", storeType)
|
|
end
|
|
|
|
if id == nil then
|
|
id = self:GenerateID()
|
|
end
|
|
|
|
self.activeStores[storeType][id] = store
|
|
self.storeMap[store] = {
|
|
id = id,
|
|
prototype = self.prototypes[storeType],
|
|
type = storeType
|
|
}
|
|
|
|
if image == nil then
|
|
-- save initial image via Commit
|
|
image = self.prototypes[storeType]:Commit(store)
|
|
end
|
|
self:Assert(image ~= nil, "Create Verb failed to generate initial image for archive.")
|
|
self.sv[storeType][id] = {
|
|
timestamp = time(),
|
|
version = self.prototypes[storeType].version,
|
|
data = self:Archive(image)
|
|
}
|
|
|
|
return store, id
|
|
end
|
|
|
|
-- clones archived data and/or active store object to newId
|
|
-- also provides an active store object of the cloned data if openStore is set
|
|
function Archivist:Clone(storeType, id, newId, openStore)
|
|
do -- arg validation
|
|
self:Assert(type(storeType) == "string" and self.prototypes[storeType], "Store type must be registered to clone a store.")
|
|
self:Assert(type(id) == "string" and (self.sv[storeType][id] or self.activeStores[storeType][id]), "Unable to clone store: store not found.")
|
|
end
|
|
|
|
if type(newId) ~= "string" then
|
|
newId = self:GenerateID()
|
|
end
|
|
|
|
self:Assert(not self.sv[storeType][newId], "Store with ID %q already exists. Choose a different ID.")
|
|
if self.activeStores[storeType][id] then
|
|
-- go ahead and commit active store
|
|
self:Commit(storeType, id)
|
|
end
|
|
|
|
-- thankfully, strings are easy to copy
|
|
self.sv[storeType][newId] = {
|
|
version = self.prototypes[storeType].version,
|
|
timestamp = time(),
|
|
data = self.sv[storeType][id].data
|
|
}
|
|
if openStore then
|
|
return self:Open(storeType, newId), newId
|
|
else
|
|
return nil, newId
|
|
end
|
|
end
|
|
|
|
function Archivist:CloneStore(store, newId, openStore)
|
|
self:Assert(self.storeMap[store], "Unrecognized store was provided.")
|
|
local info = self.storeMap[store]
|
|
return self:Clone(info.type, info.id, newId, openStore)
|
|
end
|
|
|
|
-- Closes store (if open), then deletes data from archive
|
|
-- Prototype is given opportunity to perform actions using image (usually, to delete other sub stores)
|
|
-- if store type is not registered, then force flag must be set in order to delete data,
|
|
-- to reduce the chance of accidents
|
|
function Archivist:Delete(storeType, id, force)
|
|
do -- arg validation
|
|
self:Warn(force or type(storeType == "string") and self.sv[storeType], "There are no stores to delete.")
|
|
self:Assert(force or self.prototypes[storeType], "Store type should be registered before deleting a store. Call Delete again with arg #3 == true to override this.")
|
|
end
|
|
|
|
if id and storeType and self.sv[storeType] then
|
|
if self.prototypes[storeType] and self.prototypes[storeType].Delete and self.sv[storeType][id] then
|
|
local image = self.activeStores[storeType][id]
|
|
and self:Close(self.activeStores[storeType][id])
|
|
or self:DeArchive(self.sv[storeType][id].data)
|
|
self.prototypes[storeType]:Delete(image)
|
|
end
|
|
self.sv[storeType][id] = nil
|
|
end
|
|
end
|
|
|
|
function Archivist:DeleteStore(store)
|
|
self:Assert(self.storeMap[store], "Unrecognized store was provided.")
|
|
local info = self.storeMap[store]
|
|
return self:Delete(info.type, info.id)
|
|
end
|
|
|
|
-- unpacks data in the archive into an active store object
|
|
-- if store is already active, then returns active store object
|
|
function Archivist:Open(storeType, id, ...)
|
|
do -- arg validation
|
|
self:Assert(type(storeType) == "string" and self.prototypes[storeType], "Store type must be registered before opening a store.")
|
|
self:Assert(type(id) == "string" and (self.sv[storeType][id] or self.activeStores[storeType][id]), "Could not find a store with that ID. Did you mean to call Archivist:Create?")
|
|
end
|
|
|
|
local store = self.activeStores[storeType][id]
|
|
if not store then
|
|
local saved = self.sv[storeType][id]
|
|
local data = self:DeArchive(saved.data)
|
|
local prototype = self.prototypes[storeType]
|
|
-- migrate data...
|
|
if prototype.Update and prototype.version > saved.version then
|
|
local newData = prototype:Update(data, saved.version)
|
|
if newData ~= nil then
|
|
saved.data = self:Archive(newData)
|
|
saved.timestamp = time()
|
|
end
|
|
saved.version = prototype.version
|
|
end
|
|
-- create store object...
|
|
store = prototype:Open(data, ...)
|
|
-- cache it so that we can close it later..
|
|
self.activeStores[storeType][id] = store
|
|
self.storeMap[store] = {
|
|
id = id,
|
|
prototype = self.prototypes[storeType],
|
|
type = storeType
|
|
}
|
|
end
|
|
return store
|
|
end
|
|
|
|
-- DANGEROUS FUNCTION
|
|
-- Your data will be lost. All of it. No going back.
|
|
-- Don't say I didn't warn you
|
|
function Archivist:DeleteAll(storeType)
|
|
if storeType then
|
|
self.sv[storeType] = {}
|
|
for id, store in pairs(self.activeStores[storeType]) do
|
|
self.activeStores[storeType][id] = nil
|
|
self.storeMap[store] = nil
|
|
end
|
|
else
|
|
for id in pairs(self.prototypes) do
|
|
self.sv[id] = {}
|
|
self.activeStores[id] = {}
|
|
end
|
|
self.storeMap = {}
|
|
end
|
|
end
|
|
|
|
-- deactivates store, with one last opportunity to commit data if the prototype chooses to do so
|
|
function Archivist:Close(storeType, id)
|
|
do -- arg validation
|
|
self:Assert(type(storeType) == "string" and self.prototypes[storeType], "Closing a store of an unregistered store type doesn't make sense.")
|
|
self:Warn(type(id) == "string" and self.activeStores[storeType][id], "No store with that ID can be found.")
|
|
end
|
|
|
|
local store = self.activeStores[storeType][id]
|
|
local saved = self.sv[storeType][id]
|
|
if store then
|
|
local image = self.prototypes[storeType]:Close(store)
|
|
if image ~= nil then
|
|
saved.data = self:Archive(image)
|
|
saved.timestamp = time()
|
|
end
|
|
self.activeStores[storeType][id] = nil
|
|
self.storeMap[store] = nil
|
|
end
|
|
end
|
|
|
|
function Archivist:CloseStore(store)
|
|
self:Assert(self.storeMap[store], "Unrecognized store was provided.")
|
|
local info = self.storeMap[store]
|
|
return self:Close(info.type, info.id)
|
|
end
|
|
|
|
function Archivist:CloseAllStores()
|
|
for storeType, prototype in pairs(self.prototypes) do
|
|
for id, store in pairs(self.activeStores[storeType]) do
|
|
local image = prototype:Close(store)
|
|
local saved = self.sv[storeType][id]
|
|
self.activeStores[storeType] = nil
|
|
if image then
|
|
saved.data = self:Archive(image)
|
|
saved.timestamp = time()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- archives an image of the store object
|
|
function Archivist:Commit(storeType, id)
|
|
do -- arg validation
|
|
self:Assert(type(storeType) == "string" and self.prototypes[storeType], "Committing a store of an unregistered store type doesn't make sense.")
|
|
self:Assert(type(id) == "string" and self.activeStores[storeType][id], "No store with that ID can be found.")
|
|
end
|
|
|
|
local store = self.activeStores[storeType][id]
|
|
local image = self.prototypes[storeType]:Commit(store)
|
|
local saved = self.sv[storeType][id]
|
|
if image ~= nil then
|
|
saved.data = self:Archive(image)
|
|
saved.timestamp = time()
|
|
end
|
|
end
|
|
|
|
function Archivist:CommitStore(store)
|
|
self:Assert(self.storeMap[store], "Unrecognized store was provided.")
|
|
local info = self.storeMap[store]
|
|
return self:Commit(info.type, info.id)
|
|
end
|
|
|
|
-- opens or creates a storeType, depending on what is appropriate
|
|
-- this is the main entry point for other addons who just want their saved data
|
|
function Archivist:Load(storeType, id)
|
|
do -- arg validation
|
|
self:Assert(type(storeType) == "string" and self.prototypes[storeType], "Store type must be registered before loading data.")
|
|
self:Assert(id == nil or type(id) == "string", "Store ID must be a string if provided.")
|
|
end
|
|
|
|
if id == nil or not self.sv[storeType][id] then
|
|
return self:Create(storeType, id)
|
|
elseif self.activeStores[storeType][id] then
|
|
return self.activeStores[storeType][id]
|
|
else
|
|
return self:Open(storeType, id)
|
|
end
|
|
end
|
|
|
|
function Archivist:Check(storeType, id)
|
|
do -- arg validation
|
|
self:Assert(type(storeType) == "string", "Expected string for storeType, got %q.", type(storeType))
|
|
self:Assert(type(id) == "string", "Expected string for storeID, got %q.", type(id))
|
|
end
|
|
if self.sv[storeType] and self.sv[storeType][id] then
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
do -- function Archivist:Archive(data)
|
|
local tinsert, tconcat = table.insert, table.concat
|
|
-- serialized string looks like
|
|
-- <obj1>,<obj2>,...,<objN>,<value>
|
|
-- (in most cases <value> will be just &1)
|
|
-- <objN> is a series of 0 or more ^<value>:<value> pairs
|
|
-- the contents of the string between ^ or : and the next magic character is a string,
|
|
-- unless the first char is the magic #, in which case it is a number.
|
|
-- @ becomes boolean true, $ becomes false
|
|
-- &N is a reference to <objN>
|
|
-- when deserializing, the result of <value> is our result
|
|
local function replace(c) return "\\"..c end
|
|
local function serialize(object)
|
|
local seenObjects = {}
|
|
local serializedObjects = {}
|
|
local function inner(val)
|
|
local valType = type(val)
|
|
if valType == "boolean" then
|
|
return val and "@" or "$"
|
|
elseif valType == "number" then
|
|
return "#" .. val
|
|
elseif valType == "string" then
|
|
-- escape all characters that might be confused as magic otherwise
|
|
return (val:gsub("[\\&,^@$#:]", replace))
|
|
elseif valType == "table" then
|
|
if not seenObjects[val] then
|
|
-- cross referencing is a thing. Not to hard to serialize but do be careful
|
|
local index = #serializedObjects + 1
|
|
seenObjects[val] = index
|
|
local serialized = {}
|
|
serializedObjects[index] = "" -- so that later inserts go to the correct spot
|
|
for k,v in pairs(val) do
|
|
local key, value = inner(k), inner(v)
|
|
if key ~= nil and value ~= nil then
|
|
tinsert(serialized, "^" .. inner(k))
|
|
tinsert(serialized, ":" .. inner(v))
|
|
end
|
|
end
|
|
serializedObjects[index] = tconcat(serialized)
|
|
end
|
|
return "&" .. seenObjects[val]
|
|
end
|
|
end
|
|
tinsert(serializedObjects, inner(object))
|
|
-- ensure that serialized data ends with a comma
|
|
tinsert(serializedObjects, "")
|
|
return tconcat(serializedObjects, ',')
|
|
end
|
|
|
|
function Archivist:Archive(data)
|
|
local serialized = serialize(data)
|
|
local compressed = LibDeflate:CompressDeflate(serialized)
|
|
local encoded = LibDeflate:EncodeForPrint(compressed)
|
|
return encoded
|
|
end
|
|
end
|
|
|
|
do -- function Archivist:DeArchive(encoded)
|
|
local escape2unused = {
|
|
["\\"] = "\001",
|
|
["&"] = "\002",
|
|
[","] = "\003",
|
|
["^"] = "\004",
|
|
["@"] = "\005",
|
|
["$"] = "\006",
|
|
["#"] = "\007",
|
|
[":"] = "\008",
|
|
}
|
|
local unused2Escape = tInvert(escape2unused)
|
|
local unused = "[\001-\008]"
|
|
local function unusify(c)
|
|
return escape2unused[c] or c
|
|
end
|
|
local function escapify(c)
|
|
return unused2Escape[c] or c
|
|
end
|
|
local function parse(value, objectList)
|
|
local firstChar = value:sub(1,1)
|
|
local remainder = value:sub(2)
|
|
if firstChar == "@" then
|
|
return true, "BOOL", remainder
|
|
elseif firstChar == "$" then
|
|
return false, "BOOL", remainder
|
|
elseif firstChar == "#" then
|
|
local num, rest = remainder:match("([^\\&,^@$#:]*)(.*)")
|
|
return tonumber(num), "NUMBER", rest
|
|
elseif firstChar == "^" then
|
|
local str, rest = remainder:match("([^:^,]*)(.*)")
|
|
local key = parse(str, objectList)
|
|
return key, "KEY", rest
|
|
elseif firstChar == ":" then
|
|
local str, rest = remainder:match("([^:^,]*)(.*)")
|
|
local val = parse(str, objectList)
|
|
return val, "VALUE", rest
|
|
elseif firstChar == "&" then
|
|
local num, rest = remainder:match("([^\\&,^@$#:]*)(.*)")
|
|
return objectList[tonumber(num)], "OBJECT", rest
|
|
else
|
|
local str, rest = value:match("([^\\&,^@$#:]*)(.*)")
|
|
return str:gsub(unused, escapify), "STRING", rest
|
|
end
|
|
end
|
|
local function deserialize(value)
|
|
-- first, convert escaped magic characters to chars that we'll likely never find naturally
|
|
value = value:gsub("\\([\\&,^@$#:])", unusify)
|
|
-- then, split by comma to get a list of objects
|
|
local serializedObjects = {}
|
|
for piece in value:gmatch("([^,]*),") do
|
|
table.insert(serializedObjects, piece)
|
|
end
|
|
local objects = {}
|
|
-- create one empty object for each object in the list
|
|
for i = 1, #serializedObjects - 1 do
|
|
objects[i] = {}
|
|
end
|
|
for index = 1, #serializedObjects - 1 do
|
|
local str = serializedObjects[index]
|
|
local object = objects[index]
|
|
local mode = "KEY"
|
|
local key
|
|
local newValue, valueType
|
|
while #str > 0 do
|
|
newValue, valueType, str = parse(str, objects)
|
|
Archivist:Assert(valueType == mode, "Encountered unexpected token type while parsing object. Expected %q but got %q.", mode, valueType)
|
|
if valueType == "KEY" then
|
|
key = newValue
|
|
mode = "VALUE"
|
|
else
|
|
mode = "KEY"
|
|
object[key] = newValue
|
|
end
|
|
end
|
|
Archivist:Assert(mode == "KEY", "Encountered end of serialized token unexpectedly.")
|
|
end
|
|
local deserialized, _, remainder = parse(serializedObjects[#serializedObjects], objects)
|
|
Archivist:Assert(#remainder == 0, "Unexpected token at end of serialized string. Expected EOF, got %q.", remainder:sub(1,10))
|
|
return deserialized
|
|
end
|
|
|
|
function Archivist:DeArchive(encoded)
|
|
local compressed = LibDeflate:DecodeForPrint(encoded)
|
|
local serialized = LibDeflate:DecompressDeflate(compressed)
|
|
local data = deserialize(serialized)
|
|
return data
|
|
end
|
|
end
|
|
|