Tabletop Simulator

Tabletop Simulator

Not enough ratings
[Scripting] Why GUIDs are volatile
By Bone White
This guide explains why GUIDs are volatile, why relying on them causes reliable and unreliable script failures, and better alternatives.

Don't know what a GUID is? This guide is not for you - this guide is aimed at people who script mods for TTS.
   
Award
Favorite
Favorited
Unfavorite
What are GUIDs?
A GUID is a string value that every game object has. These are seemingly unique, and are used a lot in scripting examples in the form of getObjectFromGUID("abc123")

The prevalence of this function in examples causes many modders to assume it is best practice - a reliable method of finding a specific object. It can be a reliable method, but often isn't due to the reasons below.
Why are GUIDs volatile?
Volatile in this case means:
likely to change suddenly and unexpectedly, especially by getting worse
Source: https://dictionary.cambridge.org/dictionary/english/volatile

So why are GUIDs volatile? To better explain this, I'll explain how Tabletop Simulator handles and assigns GUIDs when loading an object:

When an object is about to be spawned: Check the data the object is being spawned from, does it have a GUID? If yes: Does the game room already have an object with this GUID? If yes: Spawn this object with a randomised, unreserved GUID -- path 1 If no: Spawn this object with the GUID in its data, and reserve that GUID -- path 2 end If no: Spawn this object with a randomised, unreserved GUID -- path 3 end end

Let's talk about some examples now. In my examples I will name them, and follow their name with their GUID within square brackets [] for convenience.

Reliable GUID behaviour

Let's see some cases where the GUIDs of objects do not change:

I currently have in my game a single object: a bag with GUID "000000"; Bag [000000].

I spawn a red cube from the objects menu. This object is spawned from data where there is no GUID saved with it, so it is spawned with a randomised, unreserved GUID (path 3 above). This red cube could be assigned any GUID excluding "000000". This red cube is now Red Cube [111111]. GUID "111111" is now reserved.

I spawn another red cube from the objects menu. This object is spawned from data where there is no GUID saved with it, so it is spawned with a randomised, unreserved GUID (path 3 above). This red cube could be assigned any GUID excluding "000000" and "111111". This red cube is now Red Cube [222222]. GUID "222222" is now reserved.

I place Red Cube [111111] into Bag [000000], the red cube is destroyed, and saved as data (including its GUID of "111111") inside of the bag's data. GUID "111111" remains reserved.

I pull Red Cube [111111] out of Bag [000000]. This object is spawned from data where it contains a GUID "111111". No object exists in the game room with this GUID, so it will be spawned with that GUID, and that GUID will be reserved (path 2 above). The fact that we are reserving the GUID again has no effect - "111111" remains reserved.

Unreliable GUID behaviour

Now let's see some cases where the GUIDs of objects do change. In the example below you will see how spawning an object from data that contains a GUID, will not always result in the spawned object having that GUID:

Following on from the example above, I have three objects in my game: Bag [000000] which is empty, Red Cube [111111], and Red Cube [222222].

I copy Red Cube [222222] to my clipboard, then put Red Cube [222222] into Bag [000000], destroying it. The GUID "222222" remains reserved.

I paste Red Cube [222222] from my clipboard into the game room. This object is spawned from data where it contains a GUID "222222". No object exists in the game room with this GUID, so it will be spawned with that GUID, and that GUID will be reserved (path 2 above). The fact that we are reserving the GUID again has no effect - "222222" remains reserved.

I again paste Red Cube [222222] from my clipboard into the game room. This object is spawned from data where it contains a GUID "222222". An object already exists in the game room with this GUID, so it is spawned with a randomised, unreserved GUID (path 1 above). This red cube could be assigned any GUID excluding "000000", "111111", and "222222". This red cube is now Red Cube [333333]. GUID "333333" is now reserved.

We just spawned an object which was saved with the GUID "222222" but it was spawned with the GUID "333333"!

The example above doesn't actually change the GUID of the original Red Cube [222222] so what's the problem? The problem is that even that cube's GUID is unreliable:

More unreliable GUID behaviour

Now for an example of when an object's GUID can be altered from the GUID it was saved with.

I copy Red Cube [333333] to my clipboard, then put Red Cube [333333] into Bag [000000], destroying it. The GUID "333333" remains reserved.

I paste Red Cube [333333] from my clipboard into the game room. This object is spawned from data where it contains a GUID "333333". No object exists in the game room with this GUID, so it will be spawned with that GUID, and that GUID will be reserved (path 2 above). The fact that we are reserving the GUID again has no effect - "333333" remains reserved.

I rename the just-spawned Red Cube [333333] to Red Cube Copy [333333] to differentiate it from the original.

I pull Red Cube [333333] out of Bag [000000]. This object is spawned from data where it contains a GUID "333333". An object already exists in the game room with this GUID, so it is spawned with a randomised, unreserved GUID (path 1 above). This red cube could be assigned any GUID excluding "000000", "111111", "222222", and "333333". This red cube is now Red Cube [444444]. GUID "444444" is now reserved.

I now have five objects in my game:
Bag [000000]
Red Cube [111111]
Red Cube [222222]
Red Cube Copy [333333]
Red Cube [444444]

Importantly, Red Cube [444444] was actually saved with GUID [333333], but got spawned with GUID "444444" despite it being the "original".

if I then delete my "copy", I'll be left with this:
Bag [000000]
Red Cube [111111]
Red Cube [222222]
Red Cube [444444]

Then trying to find my cube 333333 by using getObjectFromGUID("333333") I will get an error. Not ideal.
What does this mean?
Effectively, any time an object is spawned, it is not guaranteed to have the GUID it was saved with. This applies to every single method of loading an object:

  • Removing an object from a container (Deck / Bag / Infinite Bag)
  • Changing the state of an object
  • Spawning from the objects menus (including Saved Objects)
  • Copy/Pasting an object (by user or by script)
  • Cloning an object (by user or by script)
  • Spawning objects from script (regardless of method)
  • Adding objects from another save file by the Search UI
  • Adding objects from another save file by Additive Loading
  • Loading a save file into an empty room (yes, even this)

A save file can be manually edited so that objects have identical GUIDs, so yes, even the last method can suffer from this too.

Any loaded object is not guaranteed to spawn with the GUID it was saved with!
So why don't most mods break then?
Mostly because the users are using the mods in an expected way - they are loading the mod into an empty room, not copy/pasting objects they shouldn't, or using the search import, or additive loading the room into another.

Basically they are not performing any of the steps listed above where it would matter.
Better alternatives to GUIDs
There are effectively many different methods you could use to alleviate or entirely mitigate the problem caused by the volatility of GUIDs, but the most simple answer is to use GM notes to store your object identifier instead.

GM Notes are a string field stored with an object that can only be viewed by the Black seated player, but can be read and set by script.

Rather than using getObjectFromGUID() I will now use a user defined function getObjectFromGMNotes():
function getObjectFromGMNotes(gm_notes) -- takes a single string and returns the first found object that matches this gm_note for _,obj in ipairs(getObjects()) do if obj.getGMNotes() == gm_notes then return obj end end end

For example, instead of writing:
redCube = getObjectFromGUID("333333")
(which in the previous example, now no longer exists) I can now use the following, assuming I have added the GM Note "Red Cube 3" to that red cube:
redCube = getObjectFromGMNotes("Red Cube 3")

No problems now, no matter whether a copy was destroyed or not.

Problems with this method

Firstly, if I have multiple objects with this GM Note, then I'll need a way of handling this, for example instead retrieving _all_ objects that match that GM Note:

function getObjectsFromGMNotes(gm_notes) -- takes a single string and returns a table of all objects that match this gm_note local matchedObjects = {} for _,obj in ipairs(getObjects()) do if obj.getGMNotes() == gm_notes then table.insert(matchedObjects, obj) end end return matchedObjects end

Of course this would need extra steps to handle, but is by far more useful than not being able to find _any_ correct object.

Alternatives to GM Notes

Tags
Another popular alternative is using the Tags system, being able to identify an object (or its behaviours) through which tags it has, but I prefer to use Tags strictly for behaviours relating to user experience - snap points, scripting zone detections, etc.

.memo
GM Notes do have a specific use in TTS in some games/mods, so instead you can use an object's .memo field. This can only be set/read by script but is the most suitable alternative.
Summary
  • Objects are never guaranteed to be loaded with the GUID they were saved with
  • You can probably expect a user to load a save file without issue (unless they additively load, or search import objects, from the save file), so objects that exist in that save file's game room when it was saved will be spawned with reliable GUIDs. Just bear in mind if those objects ever enter or leave a container (cards in a deck, anything in a bag) then their GUID will no longer be reliable.
  • Use a controllable and persistent method of identifying your objects: GM Notes, .memo, Tags, or something else. See Further Reading for more info.

Please post questions or feedback below. I hope to write more guides like this in the future. For deeper discussions, please @ me bonewhite in the #scripting channel of the official TTS Discord server: https://discord.com/invite/tabletopsimulator
3 Comments
The Thurmanator 17 Dec, 2024 @ 10:30pm 
How have 321 people visited this post, and five people favorited it, and I am the first to give an award? This is well-written, ladies and germs... Show some respect for the time spent here... :steamhappy:
Bone White  [author] 25 Apr, 2024 @ 5:12am 
@Luka, yes
Luka 24 Apr, 2024 @ 5:27am 
could i use this to rewrite mods with duplicare Guid errors?