- Joined
- Jul 10, 2009
- Messages
- 534
This resource has been moved here from the old Code forum. You can find the old discussion in this thread.
T = MDTable.create(3) --Creates a table with 3 dimensions.
T[1][2][3] = "Hello World" --Directly read and write to all dimensions. No subtable creation required.
T[key1][key2][key3]
, then you have to make sure that both T[key1]
and T[key1][key2]
hold a subtable.--Maintaining multi-dimensional structures can be tedious:
function setValue(key1,key2,key3,value)
T[key1] = T[key1] or {}
T[key1][key2] = T[key1][key2] or {}
T[key1][key2][key3] = value
end
function getValue(key1,key2,key3)
if T[key1] and T[key1][key2] then
return T[key1][key2][key3]
end
return nil
end
-- MDTable.create(numDimensions)
-- creates a table that allows for direct access of the specified amount of dimensions.
T = MDTable.create(3) --creates a multidimensional table with 3 dimensions
T[1][2][Player(0)] = 42
print(T[1][2][Player(0)]) --prints "42"
print(T[1][2][3]) --prints "nil", because no value has been assigned to the specified set of keys
print(T[1][2][3][4]) --throws an error, because the MDTable has been created with 3 dimensions only.
pairs
-iteration that allows to access all keys from the different dimensions at once.-- for key_1, key_2, ..., key_n, value in pairs(T) do [doStuff] end
T = MDTable.create(3)
T[1]["bla"][1.5] = 42
for key1, key2, key3, value in pairs(T) do
print(key1, key2, key3, value) --prints "1 bla 1.5 42"
end
create
-method allows to specify a default value to be returned instead of nil
-values.-- MDTable.create(numDimensions, defaultValue)
T = MDTable.create(3, "Hello")
print(T[1][2][3]) --prints "Hello". T[1][2][3] will still not contain a value (value isn't persisted).
for key1, key2, key3, value in pairs(T) do
print(key1, key2, key3, value) -- doesn't print anything, because T is still empty.
end
local sumOfKeys = function(key1,key2,key3)
return key1 + key2 + key3
end
T = MDTable.create(3, sumOfKeys)
print(T[1][2][3]) --prints "6"
print(T[1]["bla"][5]) --throws an error, because the specified function wasn't able to deal with the string parameter.
create
-method also allows to specify, whether a default value should be permanently saved into the table upon read access. By default, that is not the case.-- MDTable.create(numDimensions, defaultValue, persistDefault_yn)
T = MDTable.create(3, "Hello", true) --setting third parameter to true will permanently save default in the table upon read access
print(T[1][2][3]) --prints "Hello". T[1][2][3] now contains "Hello"
for key1, key2, key3, value in pairs(T) do
print(key1, key2, key3, value) -- prints "1 2 3 Hello"
end
T[player][levelId]
, which holds a table counting kills and received Gold for each player in any level of a tower defense.function createDatatable(player, levelId)
return {kills = 0, receivedGold = 0, playerName = GetPlayerName(player)}
end
T = MDTable.create(2, createDatatable, true) --2-dimensional table with function-type persisting default
--Imagine we are in round 1 and want to increase the kill count for Player(0) by 1.
T[Player(0)][1].kills = T[Player(0)][1].kills + 1 --works. Table access created the default value, persisted it and immediately allows for manipulating properties.
for player, levelId, dataTable in pairs(T) do
print(dataTable.kills, dataTable.playerName) --prints "1" and the name of Player(0).
end
pairs
-iteration is not synchronous between players in a multiplayer-game. This fact can lead to desyncs, if you change game state inside such a loop.-- MDTable.create(numDimensions, defaultValue, persistDefault_yn, syncedLoop_yn)
-- setting syncedLoop_yn to true makes the pairs-iteration synchronous in a multiplayer-game. This setting requires the SyncedTable-library.
T = MDTable.create(3, "Hello", false, true)
T[1][2][3] = "someValue"
T["bla"][1.5][false] = "whatever"
--Synced Loop
for key1, key2, key3, value in pairs(T) do
[you can change gamestate here]
end
syncedLoop_yn
is always the fourth one). So if you want to create a 3-dimensional multiplayer-synced MDTable without default value, you would need to call T = MDTable.create(3, nil, nil, true)
. If you find this inconvenient, just build your own wrapper around it-- table.nestedSet(t, ... , value)
-- Saves the specified value at the specified keys within t. Creates subtables, if necessary.
--Example
t = {}
--We want to set t[1][2][3][4] = "Hello",
table.nestedSet(t, 1, 2, 3, 4, "Hello") --automatically creates subtables in t[1] and t[1][2] etc., if not already present.
-- table.nestedGet(t, ...)
-- Returns the value that t has stored at the specified keys. Returns nil, if any subtable is nil.
--Example
t = {}
table.nestedSet(t, 1, 2, 3, 4, "Hello") -- t[1][2][3][4] = "Hello"
print(table.nestedGet(t, 1, 2, 3, 4)) --prints "Hello" (the value at t[1][2][3][4])
print(table.nestedGet(t, 5, 6)) --prints "nil", because t[5] doesn't hold a subtable.
--Restrictions
-- table.nestedSet will throw an error, if the specified key-path contains non-table values:
t["error"] = 10 -- t["error"] already stores a value, so table.nestedSet throws an error.
table.nestedSet(t, "error", "cantNestFurther", 20) --error
-- The same holds for table.nestedGet
table.nestedGet(t, "error", "cantNestFurther") --error
nestedSet
and nestedGet
can be used on any table, so they have been incorporated into the table-library.Read and write to multiple table dimensions | ![]() |
Default values | ![]() |
Multidimensional Looping | ![]() |
table.NestedSet and table.NestedGet | ![]() |
if Debug and Debug.beginFile then Debug.beginFile("MDTable") end
--[[-----------------------------------------------------------------------------------------------------------------------------------------------
* -------------------------------
* | Multidimensional Table v1.2 |
* -------------------------------
*
* by Eikonium
* --> https://www.hiveworkshop.com/threads/multidimensional-table.353717/
*
* - Multidimensional tables, short MDTables, are tables that allow for direct access of multiple table dimensions without the need of manual subtable creation.
* - MDTables also provide the option of choosing a default value to be returned upon accessing non-existing keys. That default value is allowed to depend on the keys accessed.
* - Finally, MDTables come with a custom pairs-iteration, which grants access to all combinations of keys that hold a value. See below for further details.
*
* MDTable.create(numDimensions: integer) --> table
* - Standard use:
* Creates an MDTable with the specified number of dimensions. E.g. "T = MDTable.create(3)" will allow you to read from and write to T[key1][key2][key3] for any arbitrary set of 3 keys.
* You can think of it like a tree with constant depth that only allows you to write into the "leaf level" (using exactly the number of keys as dimensions specified).
* - In the example with 3 dimensions, you should only write to T[key1][key2][key3], never to T[key1] or T[key1][key2].
* Reading is fine on every level, but you are probably not interested in the subtable stored in T[key1].
* You can however manually save further tables in T[key1][key2][key3] (at leaf level).
* - MDTables will automatically create subtables on demand, i.e. upon reading from or writing to a combination of keys.
* MDTable.create(numDimensions: integer, defaultValue: function|any) --> table
* - Default Values:
* Reading a nil value from the MDTable will instead return the specified defaultValue (which is nil, if not specified).
* - Function-valued default value:
* If defaultValue is a function, that function's return value will be returned. The function will be called with all requested keys as its arguments.
* E.g. if T is an MDTable with 3 dimensions and defaultValue is a function, accessing an empty slot T[key1][key2][key3] will return defaultValue(key1,key2,key3).
* MDTable.create(numDimensions: integer, defaultValue: function|any, persistDefault_yn: boolean) --> table
* - Persisting default values:
* When MDTables return the default values, they normally don't save it permanently.
* You can make them do so by setting persistDefault_yn to true.
* - Example: Let T be an MDTable with 3 dimensions, defaultValue = function(...) return {...} end, persistDefault_yn = true.
* Now reading a nil-value at T[key1][key2][key3] will permanently save {key1,key2,key3} in that table slot.
* MDTable.create(numDimensions: integer, defaultValue: function|any, persistDefault_yn: boolean, syncedLoop_yn: boolean) --> table
* - Synced Loops:
* Setting the syncedLoop_yn-parameter to true will make the iteration synchronous in multiplayer games (at the cost of some overhead), which prevents desyncs.
* Hence it should be set to true, if and only if you plan to iterate over it in a multiplayer game. See comments on iteration below.
* This setting requires the SyncedTable-library (https://www.hiveworkshop.com/threads/syncedtable.353715/).
*
* for key1, key2, ..., key_n, value in pairs(<MDTable>) do [doStuff] end
* - You can iterate over an MDTable by using Lua's native pairs(). Loop-parameters are all keys plus the value at leaf level.
* - Example:
* local T = MDTable.create(3, 42) --creates an MDTable with 3 dimensions and default value 42.
* T[1][2][3] = "Hello"
* T[1]["bla"][Player(0)] = "World"
* print(T[2][3][4]) --> prints the defaultValue "42", but doesn't add that value to the table. Hence, it will not be iterated below.
* for key1, key2, key3, value in pairs(T) do
* print(key1, key2, key3, value) --will print "1 2 3 Hello" and "1 bla Player(0) World"
* end
* - As all subtables of MDTables up to leaf level are also MDTables, you can iterate over subtables with the same method. Note that subtables have a lower dimension.
* --Following the example from above
* for key1, key2, value in pairs(T[1]) do
* print(key1, key2, value) --> will print "2 3 Hello"
* end
*
* -------------------------
* | nestedSet & nestedGet |
* -------------------------
*
* table.nestedSet(t, ..., value)
* - Saves the specified value at the specified keys within t. Creates subtables, if necessary.
* - E.g. nestedSet(t, 1, 2, 3) is equivalent to t[1][2] = 3 and will save a subtable in t[1], if not already present.
* table.nestedGet(t, ...)
* - Returns the value that t has stored at the specified keys. Returns nil, if any of subtable is nil.
* - E.g. nestedGet(t, 1, 2) returns t[1][2] (or nil, if t[1] is nil).
-------------------------------------------------------------------------------------------------------------------------------------------------]]
-- disable sumneko extension warnings for imported resource
---@diagnostic disable
do
local nestedGet
nestedGet = function(t, k, ...)
if k == nil and select('#', ...) == 0 then
return t
elseif t[k] == nil then
return nil
end
return nestedGet(t[k], ...)
end
---Multidimensional get-operation.
---
---Returns the value from the specified table at the specified set of keys.
---E.g. table.nestedGet(t, 1,2,3) will return t[1][2][3].
---If any of the subtables in the path are nil, table.nestedGet will return nil.
---Using this function frees you from checking every dimension for whether it contains a subtable or not.
---@param t table
---@param ... any keys to access from t
---@return any value
table.nestedGet = function(t, ...) return nestedGet(t,...) end
local nestedSet
nestedSet = function(t, k, v, ...)
if select('#', ...) == 0 then
t[k] = v
else
if t[k] == nil then --don't use or-operator. Might overwrite false-key.
t[k] = {}
end
nestedSet(t[k], v, ...)
end
end
---Multidimensional set-operation.
---
---Sets the table at the specified set of keys to the specified value. Creates subtables, where necessary.
---E.g. table.nestedSet(t, 1, 2, 3, 42) will set t[1][2][3] = 42 and create subtables at t[1] and t[1][2], if not already present.
---Using this function frees you from checking for existing subtables.
---@param t table
---@param ... any keys to access and value to set. The last parameter will be taken as the value.
table.nestedSet = function(t, ...) nestedSet(t, ...) end
MDTable = {}
local unpack = table.unpack
--Prepare table holding meta-information about every Multidimensional table and its subtables.
--Every subtable has a depth - the base table starts at 1 and the value increases for 1 for each nested level.
--The number of dimensions is referred to as maxDepth. It equals the depth at which the actual values are stored (instead of further subtables).
--The user can choose a default value to be returned, when no value has been saved in a certain combination of keys before. Returning a default value will persist it in the table, if specified in the create-method.
--SyncedTables also need to save their old __index and __pairs metamethods to allow the new methods to call those.
local metaInfo = {} ---@type table<table,{depth:integer,maxDepth:integer,default:any,persistDefault:boolean,oldIndex:function,oldPairs:function}>
----------------------------------------
--| Implementation for normal tables |--
----------------------------------------
-- MDTables have a different implementation for normal tables and SyncedTables. Normal tables use the class itself as their metatable. SyncedTables manipulate the metatable they already have.
--The index-metamethod creates new subtables or returns the default-value, depending on which nested level the read access has been conducted.
--As the same metatable is applied to all subtables, the parameter t can be any subtable on any level.
MDTable.__index = function(t, key)
local tMetaInfo = metaInfo[t]
if tMetaInfo.depth == tMetaInfo.maxDepth then
local default = tMetaInfo.default
tMetaInfo[tMetaInfo.depth] = key --add current accessed key to the table to add it to the unpacking. This will be overwritten on every call, but doesn't matter.
local defaultVal = (type(default) == 'function' and default(unpack(tMetaInfo, 1, tMetaInfo.depth))) or default --return default-value for full level read access
if tMetaInfo.persistDefault then
t[key] = defaultVal
end
return defaultVal
else
local newDim = {} --create new subtable on read access below full level
t[key] = newDim --save subtable to requested key
--prepare meta info for newDim
local newMetaInfo = {
default = tMetaInfo.default --pass default value from t to its new subtable
, depth = tMetaInfo.depth + 1 --depth of subtable is 1 higher than depth of t (obviously)
, maxDepth = tMetaInfo.maxDepth --pass max depth from t to subtable
, persistDefault = tMetaInfo.persistDefault --pass information on whether to persist default value to subtable
}
--copy path of t to subtable
for i = 1, tMetaInfo.depth - 1 do
newMetaInfo[i] = tMetaInfo[i]
end
newMetaInfo[tMetaInfo.depth] = key --last key of the "path" to the new subtable
metaInfo[newDim] = newMetaInfo
setmetatable(newDim, MDTable) --apply same metatable to subtable
return newDim --return new subtable, so the read access can continue as normal (and probably involves further nested calls of __index)
end
end
--The pairs-metamethod is supposed to work for any number of dimensions, so we need recursion to get to a full level set of keys holding a value on max level.
local loopRecursion
---Recursive loop for Nested Tables used by the iterator function for Multidimensinal Table.
---The loop reads all combinations of keys that hold a value on max level, returns that combination to the iterator and pauses the loop using coroutines, until the iterator resumes it.
---@param t table t can be any subtable, it's a recursion after all
---@param syncedLoop_yn boolean true, if loop is conducted on a multidimensional SyncedTable
---@param returnVals table holds all keys that the recursion has already gone through, so that they can be passed via table.unpack on demand
---@param loopLevel integer dimension at which the recursion is currently operating. Equals the subtable depth in case the pairs-function was used on the base table.
loopRecursion = function(t, syncedLoop_yn, returnVals, loopLevel)
local tMetaInfo = metaInfo[t]
local iter, state
--iterator to be used depends on whether we deal with a normal table or a SyncedTable.
if syncedLoop_yn then
iter, state = tMetaInfo.oldPairs()
else
iter, state = next, t
end
local k, v = iter(state) --first key-value-pair to loop through at the current loop level
--if t sits below max level, we loop through all (key,subtable)-pairs of t.
if tMetaInfo.depth < tMetaInfo.maxDepth then
while k ~= nil do --"while k do" won't do it - k can be "false".
returnVals[loopLevel] = k --remember the key in the returnVals table
loopRecursion(v, syncedLoop_yn, returnVals, loopLevel + 1) --every subtable must conduct a full loop itself
k, v = iter(state, k) --get next key-subtable-pair
end
else
--if t sits at max level, we loop through all (key,value)-pairs of t.
while k ~= nil do
returnVals[loopLevel] = k --remember the key in the returnVals table
returnVals[loopLevel+1] = v --at max level, also remember the value
coroutine.yield(unpack(returnVals, 1, loopLevel+1)) --return the complete set of keys plus final value to the iterator and pause the loop until further request
k, v = iter(state, k) --get next key-value-pair
end
end
--when the first call of the recursive loop ends, all inner recursion has already finished -> fo a final yield on the coroutine to put it to dead state
if loopLevel == 1 then
coroutine.yield()
end
end
--Define behaviour of the pairs-function for Multidimensional tables based on normal tables.
--The pairs-function is supposed to loop through all combinations of keys that hold a value at max level.
--It enables the following syntax: for key1, key2, ..., key_n, value in pairs(T) do ... end
MDTable.__pairs = function(t)
local cor = coroutine.create(loopRecursion) --we need a coroutine to pause the loop recursion between every request of the iterator function
local returnVals = {} --table to hold all return values for this specific loop. Passed to the recursion.
return function()
--successful coroutine calls return true plus the yielded values, so we need to return everything after the first value
return select(2,coroutine.resume(cor, t, false, returnVals, 1))
end, t, nil
end
--Allows to call the Multidimensional table directly via T(key1,key2,...,key_n)
MDTable.__call = function(t, ...)
return table.nestedGet(t, ...)
end
---------------------------------------
--| Implementation for SyncedTables |--
---------------------------------------
--SyncedTables need special treatment, as they all have their own individual metatable with __index and __pairs already defined.
--We basically define new index and pairs for the SyncedTable by using the old instance.
local prepareSyncedTable
local syncedIndex = function(t, key)
local tMetaInfo = metaInfo[t]
local oldIndexVal = tMetaInfo.oldIndex(t, key)
--if the SyncedTable holds a value for the given key, return that value
if oldIndexVal then
return oldIndexVal
--return default value, if there is no stored value and the user is reading at max level
elseif tMetaInfo.depth >= tMetaInfo.maxDepth then
local default = tMetaInfo.default
tMetaInfo[tMetaInfo.depth] = key --add current accessed key to tMetaInfo to add it to the unpacking below. The key at full depth will be overwritten on every call, but doesn't matter.
local defaultVal = (type(default) == 'function' and default(unpack(tMetaInfo, 1, tMetaInfo.depth))) or default
if tMetaInfo.persistDefault then
t[key] = defaultVal --triggers SyncedTable newIndex metamethod
end
return defaultVal
--create sub-SyncedTable, if not already present and reading below max level
else
local newDim = SyncedTable.create()
t[key] = newDim --don't use rawset. This must trigger SyncedTable __newindex to work.
local newMetaInfo = {
default = tMetaInfo.default
, depth = tMetaInfo.depth + 1
, maxDepth = tMetaInfo.maxDepth
, persistDefault = tMetaInfo.persistDefault
}
metaInfo[newDim] = newMetaInfo
prepareSyncedTable(newDim) --again manipulate metatables of new SyncedTable
--copy path from tMetaInfo to newMetaInfo
for i = 1, tMetaInfo.depth - 1 do
newMetaInfo[i] = tMetaInfo[i]
end
newMetaInfo[tMetaInfo.depth] = key
return newDim
end
end
--Define multidimensional pairs-function. Uses the same loopRecursion as for MDTables based on normal tables.
local syncedPairs = function(t)
local cor = coroutine.create(loopRecursion)
local returnVals = {}
return function()
--successful coroutine calls return true plus the yielded values, so we need to return everything after the first value
return select(2,coroutine.resume(cor, t, true, returnVals, 1))
end, t, nil
end
---Manipulates the metatable of a SyncedTable in a way that it allows for nested access.
---Writes into metaInfo[t], so requires that table to already exist.
---@param t table actually SyncedTable, but we don't mention the true type to prevent "undefined type" errors, when SyncedTables is not being used (optional dependency).
prepareSyncedTable = function(t)
local mt = getmetatable(t) --original metatable of the SyncedTable to be manipulated
local tMetaInfo = metaInfo[t]
tMetaInfo.oldIndex = mt.__index --original __index
tMetaInfo.oldPairs = mt.__pairs --original __pairs-function must be saved somewhere to be accessible from within the loop recursion
--SyncedTable __index triggers on every read action, because the table itself is kept empty by design. That means we must also deal with the case where the SyncedTable actually holds a value for the requested key.
mt.__index = syncedIndex
mt.__pairs = syncedPairs
mt.__call = MDTable.__call
end
---Create a new Multidimensional Table.
---@param numDimensions integer number of dimensions accessible on the new Multidim Table
---@param defaultValue? function|any default: nil. Default value to return at leaf level access, when the requested combination of keys has no value assigned. If function: calls that function and uses its return value.
---@param persistDefault_yn? boolean default: false. set to true to persist the default value in the table at the requested combination of keys upon read access, when there wasn't yet any value assigned.
---@param syncedLoop_yn? boolean default: false. set to true to allow for multiplayer-synced pairs-loop. Requires SyncedTable. Set to false, if you create a singleplayer game or if you don't plan to loop over this table.
---@return table
MDTable.create = function(numDimensions, defaultValue, persistDefault_yn, syncedLoop_yn)
local newTable
--If user desires synced loop and the SyncedTable-library is present, create SyncedTable and manipulate its metatables.
if syncedLoop_yn and SyncedTable then
newTable = SyncedTable.create()
metaInfo[newTable] = {}
prepareSyncedTable(newTable) --manipulating existing metatable instead of assigning the class as metatable.
else
--If user desires normal loop or no loop, create normal table and set standard metatable.
newTable = {}
metaInfo[newTable] = {}
setmetatable(newTable, MDTable)
end
metaInfo[newTable].depth = 1 --the base table has depth 1
metaInfo[newTable].maxDepth = numDimensions --maxdepth is the dimension chosen by the user
metaInfo[newTable].default = defaultValue --default value for max level access
metaInfo[newTable].persistDefault = persistDefault_yn --tells, if the default value shall be saved upon read access
return newTable
end
end
if Debug and Debug.endFile then Debug.endFile() end
Read and write to multiple table dimensions | ![]() |
Default values | ![]() |
Multidimensional Looping | ![]() |
table.NestedSet and table.NestedGet | ![]() |
if Debug and Debug.beginFile then Debug.beginFile("MDTable") end
--[[-----------------------------------------------------------------------------------------------------------------------------------------------
* --------------------------------------
* | Multidimensional Table (Lite) v1.2 |
* --------------------------------------
*
* by Eikonium
* --> https://www.hiveworkshop.com/threads/multidimensional-table.353717/
*
* - Multidimensional tables, short MDTables, are tables that allow for direct access of multiple table dimensions without the need of manual subtable creation.
* - MDTables also provide the option of choosing a default value to be returned upon accessing non-existing keys. That default value is allowed to depend on the keys accessed.
* - The Lite-version doesn't enable a multidimensional pairs loop, but it offers much shorter code.
*
* MDTable.create(numDimensions: integer) --> table
* - Standard use:
* Creates an MDTable with the specified number of dimensions. E.g. "T = MDTable.create(3)" will allow you to read from and write to T[key1][key2][key3] for any arbitrary set of 3 keys.
* You can think of it like a tree with constant depth that only allows you to write into the "leaf level" (using exactly the number of keys as dimensions specified).
* - In the example with 3 dimensions, you should only write to T[key1][key2][key3], never to T[key1] or T[key1][key2].
* Reading is fine on every level, but you are probably not interested in the subtable stored in T[key1].
* You can however manually save further tables in T[key1][key2][key3] (at leaf level).
* - MDTables will automatically create subtables on demand, i.e. upon reading from or writing to a combination of keys.
* MDTable.create(numDimensions: integer, defaultValue: function|any) --> table
* - Default Values:
* Reading a nil value from the MDTable will instead return the specified defaultValue (which is nil, if not specified).
* - Function-valued default value:
* If defaultValue is a function, that function's return value will be returned. The function will be called with all requested keys as its arguments.
* E.g. if T is an MDTable with 3 dimensions and defaultValue is a function, accessing an empty slot T[key1][key2][key3] will return defaultValue(key1,key2,key3).
* MDTable.create(numDimensions: integer, defaultValue: function|any, persistDefault_yn: boolean) --> table
* - Persisting default values:
* When MDTables return the default values, they normally don't save it permanently.
* You can make them do so by setting persistDefault_yn to true.
* - Example: Let T be an MDTable with 3 dimensions, defaultValue = function(...) return {...} end, persistDefault_yn = true.
* Now reading a nil-value at T[key1][key2][key3] will permanently save {key1,key2,key3} in that table slot.
*
* -------------------------
* | nestedSet & nestedGet |
* -------------------------
*
* table.nestedSet(t, ..., value)
* - Saves the specified value at the specified keys within t. Creates subtables, if necessary.
* - E.g. nestedSet(t, 1, 2, 3) is equivalent to t[1][2] = 3 and will save a subtable in t[1], if not already present.
* table.nestedGet(t, ...)
* - Returns the value that t has stored at the specified keys. Returns nil, if any of subtable is nil.
* - E.g. nestedGet(t, 1, 2) returns t[1][2] (or nil, if t[1] is nil).
-------------------------------------------------------------------------------------------------------------------------------------------------]]
-- disable sumneko extension warnings for imported resource
---@diagnostic disable
do
---------------
--| MDTable |--
---------------
MDTable = {}
MDTable.__name = 'MDTable'
local unpack = table.unpack
--Prepare table holding meta-information about every Multidimensional table and its subtables.
--Every subtable has a depth - the base table starts at 1 and the value increases for 1 for each nested level.
--The number of dimensions is referred to as maxDepth. It equals the depth at which the actual values are stored (instead of further subtables).
--The user can choose a default value to be returned, when no value has been saved in a certain combination of keys before. Returning a default value will persist it in the table, if specified in the create-method.
local metaInfo = {} ---@type table<table,{depth:integer,maxDepth:integer,default:any,persistDefault:boolean,oldIndex:function,oldPairs:function}>
--The index-metamethod creates new subtables or returns the default-value, depending on which nested level the read access has been conducted.
--As the same metatable is applied to all subtables, the parameter t can be any subtable on any level.
MDTable.__index = function(t, key)
local tMetaInfo = metaInfo[t]
if tMetaInfo.depth == tMetaInfo.maxDepth then
local default = tMetaInfo.default
tMetaInfo[tMetaInfo.depth] = key --add current accessed key to the table to add it to the unpacking. This will be overwritten on every call, but doesn't matter.
local defaultVal = (type(default) == 'function' and default(unpack(tMetaInfo, 1, tMetaInfo.depth))) or default --return default-value for full level read access
if tMetaInfo.persistDefault then
t[key] = defaultVal
end
return defaultVal
else
local newDim = {} --create new subtable on read access below full level
t[key] = newDim --save subtable to requested key
--prepare meta info for newDim
local newMetaInfo = {
default = tMetaInfo.default --pass default value from t to its new subtable
, depth = tMetaInfo.depth + 1 --depth of subtable is 1 higher than depth of t (obviously)
, maxDepth = tMetaInfo.maxDepth --pass max depth from t to subtable
, persistDefault = tMetaInfo.persistDefault --pass information on whether to persist default value to subtable
}
--copy path of t to subtable
for i = 1, tMetaInfo.depth - 1 do
newMetaInfo[i] = tMetaInfo[i]
end
newMetaInfo[tMetaInfo.depth] = key --last key of the "path" to the new subtable
metaInfo[newDim] = newMetaInfo
setmetatable(newDim, MDTable) --apply same metatable to subtable
return newDim --return new subtable, so the read access can continue as normal (and probably involves further nested calls of __index)
end
end
--Allows to call the Multidimensional table directly via T(key1,key2,...,key_n)
MDTable.__call = function(t, ...)
return table.nestedGet(t, ...)
end
---Create a new Multidimensional Table.
---@param numDimensions integer number of dimensions accessible on the new Multidim Table
---@param defaultValue? function|any default: nil. Default value to return instead of nil-values. If function: calls that function and uses its return value.
---@param persistDefault_yn? boolean default: false. Set to true to persist any returned default value in the table after read access.
---@return table
MDTable.create = function(numDimensions, defaultValue, persistDefault_yn)
local newTable = {}
setmetatable(newTable, MDTable)
metaInfo[newTable] = {
depth = 1 --the base table has depth 1
, maxDepth = numDimensions --maxdepth is the dimension chosen by the user
, default = defaultValue --default value for max level access
, persistDefault = persistDefault_yn --tells, if the default value shall be saved upon read access
}
return newTable
end
-------------------------------
--| NestedSet and NestedGet |--
-------------------------------
local nestedGet
nestedGet = function(t, k, ...)
if k == nil and select('#', ...) == 0 then
return t
elseif t[k] == nil then
return nil
end
return nestedGet(t[k], ...)
end
---Multidimensional get-operation.
---
---Returns the value from the specified table at the specified set of keys.
---E.g. table.nestedGet(t, 1,2,3) will return t[1][2][3].
---If any of the subtables in the path are nil, table.nestedGet will return nil.
---Using this function frees you from checking every dimension for whether it contains a subtable or not.
---@param t table
---@param ... any keys to access from t
---@return any value
table.nestedGet = function(t, ...) return nestedGet(t,...) end
local nestedSet
nestedSet = function(t, k, v, ...)
if select('#', ...) == 0 then
t[k] = v
else
if t[k] == nil then --don't use or-operator. Might overwrite false-key.
t[k] = {}
end
nestedSet(t[k], v, ...)
end
end
---Multidimensional set-operation.
---
---Sets the table at the specified set of keys to the specified value. Creates subtables, where necessary.
---E.g. table.nestedSet(t, 1, 2, 3, 42) will set t[1][2][3] = 42 and create subtables at t[1] and t[1][2], if not already present.
---Using this function frees you from checking for existing subtables.
---@param t table
---@param ... any keys to access and value to set. The last parameter will be taken as the value.
table.nestedSet = function(t, ...) nestedSet(t, ...) end
end
if Debug and Debug.endFile then Debug.endFile() end
Read and write to multiple table dimensions | ![]() |
Default values | ![]() |
Multidimensional Looping | ![]() |
table.NestedSet and table.NestedGet | ![]() |
table.createMD(numDimensions)
instead of MDTable.create(numDimensions)
.if Debug and Debug.beginFile then Debug.beginFile("MDTable") end
--[[-----------------------------------------------------------------------------------------------------------------------------------------------
* ----------------------------------------
* | Multidimensional Table (X-Lite) v1.2 |
* ----------------------------------------
*
* Creator: Eikonium
* Contributors: Bribe
* --> https://www.hiveworkshop.com/threads/multidimensional-table.353717/
*
* - Multidimensional tables are tables that allow for direct access of multiple table dimensions without the need of manual subtable creation.
* - This X-Lite-version only offers the ability to create multi-dimensional tables, without any additional API perks of the standard MDTable or even MDTable Lite.
*
* table.createMD(numDimensions: integer) --> table
* Creates an MDTable with the specified number of dimensions. E.g. "T = table.createMD(3)" will allow you to read from and write to T[key1][key2][key3] for any arbitrary set of 3 keys.
* You can think of it like a tree with constant depth that only allows you to write into the "leaf level" (using exactly the number of keys as dimensions specified).
* - In the example with 3 dimensions, you should only write to T[key1][key2][key3], never to T[key1] or T[key1][key2].
* Reading is fine on every level, but you are probably not interested in the subtable stored in T[key1].
* You can however manually save further tables in T[key1][key2][key3] (at leaf level).
* - MDTables will automatically create subtables on demand, i.e. upon reading from or writing to a combination of keys.
-------------------------------------------------------------------------------------------------------------------------------------------------]]
-- disable sumneko extension warnings for imported resource
---@diagnostic disable
do
local dimensionStorage = setmetatable({}, {__mode = 'k'})
local repeater = {__index = function(self, key)
local new = dimensionStorage[self] > 2 and table.createMD(dimensionStorage[self] - 1) or {}
rawset(self, key, new)
return new
end}
---Create a new Multidimensional Table.
---@param numDimensions integer
function table.createMD(numDimensions)
local newTable = {}
dimensionStorage[newTable] = numDimensions
return setmetatable(newTable, repeater)
end
end
if Debug and Debug.endFile then Debug.endFile() end
Debug.beginFile
and Debug.endFile
statements for local file recognition with Debug Utils.table.nestedSet
and table.nestedGet
. Those function no longer create a temporary table with every call.