The method is usually called “reconciling” in programming, and it is as simple as filling in missing values:
for key, value in template do
if original[key] == nil then
original[key] = value
end
end
Since we are dealing with nested tables here, we would need to perform a deep reconciliation via recursion making sure we clone values that are tables for a different memory address, or else changing a data value will also change the template value.
Code
-- performs a deep table clone operation
local function clone_deep(t)
local cloned = {}
for key, value in t do
if typeof(value) == "table" then
cloned[key] = clone_deep(value)
else
cloned[key] = value
end
end
return cloned
end
local function reconcile(original, template)
-- iterate through our template key value pairs
for key, defaultValue in template do
-- check if our original data does not contain 'key'
if original[key] == nil then
if typeof(defaultValue) == "table" then
-- if the 'defaultValue' is a table, we assign a deep clone of it
original[key] = clone_deep(defaultValue)
else
-- otherwise, we assign as is
original[key] = defaultValue
end
end
end
end
Example
local Template = {
Level = 1,
Experience = 0,
Gold = 100,
VIP = false,
Inventory = {
Items = {},
Equipped = {
Primary = "",
Secondary = ""
}
}
}
local LoadedData = {
Level = 50,
Experience = 777
}
reconcile(LoadedData, Template)
print("Reconciled data:", LoadedData)
--[=[
Reconciled data: ▼ {
["Experience"] = 777,
["Gold"] = 100,
["Inventory"] = ▼ {
["Equipped"] = ▼ {
["Primary"] = "",
["Secondary"] = ""
},
["Items"] = {}
},
["Level"] = 50,
["VIP"] = false
}
]=]
Keep in mind: from the example template above, what if we change the “VIP” key to be a number
, 0 being not VIP and higher number = higher status instead of being a boolean
. Now you run into the problem where the old data will contain key value pairs of the wrong type and can bring runtime errors. To account for that, it is often a good idea to simply check if the types are not the same and to replace it.
Code
local function clone_deep(t)
local cloned = {}
for key, value in t do
if typeof(value) == "table" then
cloned[key] = clone_deep(value)
else
cloned[key] = value
end
end
return cloned
end
local function reconcile(original, template)
for key, defaultValue in template do
--if original[key] == nil then
if typeof(original[key]) ~= typeof(defaultValue) then
if typeof(defaultValue) == "table" then
original[key] = clone_deep(defaultValue)
else
original[key] = defaultValue
end
end
end
end
Example
local Template = {
VIP = 0
}
local LoadedData = {
VIP = true
}
reconcile(LoadedData, Template)
print("Reconciled data:", LoadedData)
--[=[
Reconciled data: ▼ {
["VIP"] = 0
}
]=]
Automatically removing keys are, in my opinion, very dangerous and can lead to the loss of data if not handled perfectly. People tend to just leave it in an event that a removed feature might get reinstated in the future or manually remove keys.
You might think the solution is as simple as “oh, let’s simply check the template if it doesn’t contain the key”, and while this is on the right track, can lead to some problems.
Code
local function clone_deep(t)
local cloned = {}
for key, value in t do
if typeof(value) == "table" then
cloned[key] = clone_deep(value)
else
cloned[key] = value
end
end
return cloned
end
-- removes missing keys with a deep operation
local function removeMissingKeys(original, template)
for key in original do
if template[key] == nil then
original[key] = nil
elseif typeof(original[key]) == "table" and typeof(template[key]) == "table" then
removeMissingKeys(original[key], template[key])
end
end
end
local function reconcile(original, template)
-- matching values
for key, defaultValue in template do
if typeof(original[key]) ~= typeof(defaultValue) then
if typeof(defaultValue) == "table" then
original[key] = clone_deep(defaultValue)
else
original[key] = defaultValue
end
end
end
-- removing missing keys
removeMissingKeys(original, template)
end
Example
local Template = {
Level = 1,
Experience = 0,
Gold = 0,
Equipped = {
Primary = "",
Secondary = ""
}
}
local LoadedData = {
Level = 10,
Experience = 100,
Cash = 1000,
Equipped = {
Primary = "",
Secondary = "",
Pet = ""
}
}
reconcile(LoadedData, Template)
print("Reconciled data:", LoadedData)
--[=[
Reconciled data: ▼ {
["Equipped"] = ▼ {
["Primary"] = "",
["Secondary"] = ""
},
["Experience"] = 100,
["Gold"] = 0,
["Level"] = 10
}
]=]
This may look fine. However, let’s introduce a new Items
table containing a table data of their items.
Example
local Template = {
Level = 1,
Experience = 0,
Gold = 0,
Equipped = {
Primary = "",
Secondary = "",
Items = {}
}
}
local LoadedData = {
Level = 10,
Experience = 100,
Cash = 1000,
Equipped = {
Primary = "",
Secondary = "",
Pet = "",
Items = {
Sword = 1,
Apple = 64
}
}
}
reconcile(LoadedData, Template)
print("Reconciled data:", LoadedData)
--[=[
Reconciled data: ▼ {
["Equipped"] = ▼ {
["Items"] = {},
["Primary"] = "",
["Secondary"] = ""
},
["Experience"] = 100,
["Gold"] = 0,
["Level"] = 10
}
]=]
Running the reconcile function completely wiped their items since in the original template, “Sword” and “Apple” does not exist. A way to fix this is to let the code assume (and for you, the developer, to follow) the rule that empty template tables and it’s subsequent key value pairs will get ignored. Do keep in mind that this is not a one size fits all solution, as not all data tables will want to get treated under this assumption.
Code
local function clone_deep(t)
local cloned = {}
for key, value in t do
if typeof(value) == "table" then
cloned[key] = clone_deep(value)
else
cloned[key] = value
end
end
return cloned
end
local function removeMissingKeys(original, template)
if #template > 0 then
for key in original do
if template[key] == nil then
original[key] = nil
elseif typeof(original[key]) == "table" and typeof(template[key]) == "table" then
removeMissingKeys(original[key], template[key])
end
end
end
end
local function reconcile(original, template)
for key, defaultValue in template do
if typeof(original[key]) ~= typeof(defaultValue) then
if typeof(defaultValue) == "table" then
original[key] = clone_deep(defaultValue)
else
original[key] = defaultValue
end
end
end
removeMissingKeys(original, template)
end
Example
local Template = {
Level = 1,
Experience = 0,
Gold = 0,
Equipped = {
Primary = "",
Secondary = "",
Items = {}
}
}
local LoadedData = {
Level = 10,
Experience = 100,
Cash = 1000,
Equipped = {
Primary = "",
Secondary = "",
Pet = "",
Items = {
Sword = 1,
Apple = 64
}
}
}
reconcile(LoadedData, Template)
print("Reconciled data:", LoadedData)
--[=[
Reconciled data: ▼ {
["Cash"] = 1000,
["Equipped"] = ▼ {
["Items"] = ▼ {
["Apple"] = 64,
["Sword"] = 1
},
["Pet"] = "",
["Primary"] = "",
["Secondary"] = ""
},
["Experience"] = 100,
["Gold"] = 0,
["Level"] = 10
}
]=]
Edited for readability for future readers