- Joined
- Jul 10, 2009
- Messages
- 534
![]() | This resource has been moved to a new page in the Spells and Systems forum. The old page will not be further maintained. |
Overview
Code
Restrictions
Description
In general, iterating through a Lua-table via
The SyncedTable-"library" (well, it only provides two functions) offers the possibility to create synchronous tables, for which the
Apart from the synchronized pairs-iteration, SyncedTables behave just as normal tables. They can contain any (key, value)-pair, you can write
Note that iterating over a SyncedTable is pretty fast, but the implementation adds a small overhead to adding, changing and removing elements instead. Thus, only use a SyncedTable, if you actually plan to iterate over it.
Installation
Just copy the code from the "Code" tab into your map and you are ready to go.
How to use
To create a SyncedTable, simply write
After creation, the SyncedTable behaves just as any other table, apart from some minor restrictions, see "Restrictions" tab.
You can even specify to create the SyncedTable from an existing Table. This allows convenient table constructor usage, such as
Secondly, the library offers the function
Example Code
In general, iterating through a Lua-table via
pairs()
is not multiplayer-safe, because iteration order is not guaranteed to be the same for different players. Thus, manipulating game state inside a pairs()
-loop can lead to desyncs.The SyncedTable-"library" (well, it only provides two functions) offers the possibility to create synchronous tables, for which the
pairs()
-iteration is guaranteed to have the same order for every player in a multiplayer game, no matter which keys you use.Apart from the synchronized pairs-iteration, SyncedTables behave just as normal tables. They can contain any (key, value)-pair, you can write
table[key] = nil
to remove an element, table[key] = value
to add or change an element, and you can use it in any function that takes a table, such as table.sort()
or ipairs()
.You can find the pseudo-solution orderedPairs on lua-users.org. It basically sorts all keys in the given table before every loop to make it synchronous. That "solution" doesn't only add much overhead to every loop (because it sorts before every loop), but it also doesn't work on tables using a keyset that can't be entirely sorted via the "<"-relation.
That means, it doesn't work for tables with mixed keys (e.g.
Yes, the same website offers another multitype-compatible orderedKeys option, but that only works in singleplayer (where it guarantees to have the same iteration order in every iteration), but not in multiplayer (where it still desyncs, because the
That means, it doesn't work for tables with mixed keys (e.g.
{[1] = true, x = true}
) and it also doesn't work, if you use keys other than strings and numbers. Unfortunately, this happens quite quickly in Wc3, when we use units, players, etc. as keys for a table.Yes, the same website offers another multitype-compatible orderedKeys option, but that only works in singleplayer (where it guarantees to have the same iteration order in every iteration), but not in multiplayer (where it still desyncs, because the
tostring()
-function yields different results for different players).Note that iterating over a SyncedTable is pretty fast, but the implementation adds a small overhead to adding, changing and removing elements instead. Thus, only use a SyncedTable, if you actually plan to iterate over it.
Installation
Just copy the code from the "Code" tab into your map and you are ready to go.
How to use
To create a SyncedTable, simply write
<varName> = SyncedTable.create()
or even shorter, <varName> = SyncedTable()
.After creation, the SyncedTable behaves just as any other table, apart from some minor restrictions, see "Restrictions" tab.
You can even specify to create the SyncedTable from an existing Table. This allows convenient table constructor usage, such as
a = SyncedTable{'Hello World', [3] = true, n = 10}
. However, using an existingTable requires all of its keys to be strings or numbers (or any object having the __lt-metamethod defined), i.e. anything with a natural order. If you want to add any other key to a SyncedTable, you have to do it after creation (and it will work just fine).Secondly, the library offers the function
SyncedTable.isSyncedTable(anyObject) -->true or false
to check, if anyObject
is a SyncedTable or not.Example Code
Lua:
--Example 1:
local playersGold = SyncedTable.create() --create a SyncedTable. From now on, it can be used just like any other table.
for i = 0, 23 do
if GetPlayerSlotState(Player(i)) == PLAYER_SLOT_STATE_PLAYING then
playersGold[Player(i)] = 100
end
end
for player, gold in pairs(playersGold) do --iteration order is the same for every player
SetPlayerStateBJ( player, PLAYER_STATE_RESOURCE_GOLD, gold)
end
--Example 2: Using standard table constructor
local alphabet = SyncedTable{'a', 'b', 'c', a = 1, b = 2, c = 3} --Creating a SyncedTable from an existingTable is allowed, when all keys are strings or numbers.
alphabet.d = 4 --You can still add more elements afterwards (of course, it's a table afterall...), and the keys can have any type.
alphabet[4] = 'd'
for k,v in pairs(alphabet) do --iteration order is the same for every player. Granted, the print function wouldn't desync even for asynchronous iteration ;)
print(k,v)
end
Lua:
--SyncedTable v1.0 by Eikonium. https://www.hiveworkshop.com/threads/lua-syncedtable.332894/
do
--comparison function that allows sorting a set of objects that already have a natural order.
local function comparisonFunc(a,b)
local t1,t2 = type(a), type(b)
if t1 == t2 then
return a<b
else
return t1 < t2
end
end
---@class SyncedTable
SyncedTable = {}
---Creates a table with a multiplayer-synchronized pairs-function, i.e. you can iterate over it via pairs(table) without fearing desyncs.
---After creation, you can use it like any other table.
---The implementation adds overhead to creating the table, adding and removing elements, but keeps the loop itself very performant. So you obviously should only used syncedTables, if you plan to iterate over it.
---You are both allowed to add and remove elements during a pairs()-loop.
---Specifying an existing table as input parameter will convert that table to a syncedTable. This only works for input tables, where all keys are sortable via the "<"-relation, i.e. numbers, strings and objects listening to some __lt-metamethod.
---@param existingTable? table any lua table you want to convert to a syncedTable. The table is required to only contain keys that can be sorted via the '<'-relation. E.g. you might write SyncedTable.create{x = 10, y = 3}.
---@return SyncedTable
function SyncedTable.create(existingTable)
local new = {}
local metatable = {class = SyncedTable}
local data = {}
--orderedKeys and keyToIndex don't need to be weak tables. They reference keys if and only if those keys are used in data.
local orderedKeys = {} --array of all keys, defining loop order.
local keyToIndex = {} --mirrored orderedKeys, i.e. keyToIndex[key] = int <=> orderedKeys[int] = key. This is used to speed up the process of removing (key, value)-pairs from the syncedTable (to prevent the need of searching the key in orderedKeys).
local numKeys = 0
--If existingTable was provided, register all keys from the existing table to the keyToIndex and orderedKeys help tables.
if existingTable then
--prepare orderedKeys array by sorting all existing keys
for k,v in pairs(existingTable) do
numKeys = numKeys + 1
orderedKeys[numKeys] = k --> the resulting orderedKeys is asynchronous at this point
data[k] = v
end
table.sort(orderedKeys, comparisonFunc) --result is synchronous for all players
--fill keyToIndex accordingly
for i = 1, numKeys do
keyToIndex[orderedKeys[i]] = i
end
end
--Catch read action
metatable.__index = function(t, key)
return data[key]
end
--Catch write action
metatable.__newindex = function(t, key, value)
--Case 1: User tries to remove an existing (key,value) pair by writing table[key] = nil.
if data[key]~=nil and value == nil then
--swap last element to the slot being removed (in the iteration order array)
local i = keyToIndex[key] --slot of the key, which is getting removed
keyToIndex[orderedKeys[numKeys]] = i --first set last slot to i
keyToIndex[key] = nil --afterwards nil current key (has to be afterwards, when i == numKeys)
orderedKeys[i] = orderedKeys[numKeys] --i refers to the old keyToIndex[key]
orderedKeys[numKeys] = nil
numKeys = numKeys - 1
--Case 2: User tries to add a new key to the table (i.e. table[key] doesn't yet exist and both key and value are not nil)
elseif data[key]==nil and key ~= nil and value ~= nil then
numKeys = numKeys + 1
keyToIndex[key] = numKeys
orderedKeys[numKeys] = key
end
--Case 3: User tries to change an existing key to a different non-nil value (i.e. table[existingKey] = value ~= nil)
-- -> no action necessary apart from the all cases line
--Case 4: User tries to set table[nil]=value or table[key]=nil for a non-existent key (would be case 1 for an existent key)
-- -> don't do anything.
--In all cases, do the following:
data[key] = value --doesn't have any effect for case 4.
end
--Define, how the pairs iteration works
metatable.__pairs = function(t)
local i = 0
local latestKey
return function()
if latestKey == orderedKeys[i] then
i = i+1 --only increase i, when the last iterated key is still part of the table. Otherwise use the same i again. This allows the removal of (key,value)-pairs inside the pairs()-iteration.
end
latestKey = orderedKeys[i]
return orderedKeys[i], data[orderedKeys[i]]
end, t, nil
end
setmetatable(new, metatable)
return new
end
---Returns true, if the input argument is a SyncedTable, and false otherwise.
---@param anyObject any
---@return boolean isSyncedTable
SyncedTable.isSyncedTable = function(anyObject)
local metatable = getmetatable(anyObject)
return metatable and metatable['class'] == SyncedTable
end
--Allows writing SyncedTable() instead of SyncedTable.create().
setmetatable(SyncedTable, {__call = function(func, t)
return SyncedTable.create(t)
end})
end
The following minor limitations apply, when using SyncedTables:
- Setting a new metatable for any SyncedTable will stop it from working. You are however allowed to edit any SyncedTable's existing metatable, as long as you don't change
__index
,__newindex
and__pairs
. - Removing elements from a SyncedTable inside a
pairs()
-loop viatable[key] = nil
is allowed as long as you only remove the current loopElement.
Lua:--Let t be a SyncedTable with any elements. --Allowed: for k,v in pairs(t) do t[k] = nil end --Not Allowed: for k,v in pairs(t) do t[anyKey] = nil --where anyKey ~= k end
- Adding elements to a SyncedTable inside a
pairs()
-loop is allowed, but new elements will also be looped over. This might lead to an endless loop, if done poorly. Remember that the same rule holds for normal tables, so this is not really a restriction.
Lua:--The following code will lead to an endless loop (well, it will maybe end at max integer range) t = SyncedTable{1} for k,v in pairs(t) do t[k+1] = true end
- Creating a SyncedTable from an existing table via
<varName> = SyncedTable.create(existingTable)
only works, if all keys inexistingTable
are either numbers or strings (or have the __lt-metamethod defined in their metatable). Keys of any other type have to be added after creation. - You can't use
next(S)
on a SyncedTableS
to check, whether or not it is empty. You can however use the iterator returned bypairs
to achieve the same result. A SyncedTable is empty, ifpairs(S)(S) == nil
(yes, the double brackets look confusing, but it's absolutely fine this way. Note that for a normal table,pairs(T)
just returnsnext
, so the checknext(T) == nil
is equivalent topairs(T)(T) == nil
anyway).
Last edited: