Tabletop Simulator

Tabletop Simulator

Not enough ratings
[Scripting] Identifying Objects - Avoiding GUIDs
By Bone White
For scripters, this explains a complete system for identifying objects which does not rely on GUIDs (and their problems).
   
Award
Favorite
Favorited
Unfavorite
The problem with GUIDs
I cannot stress this enough:

No spawned object is guaranteed to retain the GUID it was saved with.

Yes, every single object. This makes it a unreliable thing to rely on GUIDs to identify your objects, such as using getObjectFromGUID().

See here for more details: https://gtm.steamproxy.vip/sharedfiles/filedetails/?id=2961384735
Triggering a script when an object spawns
onObjectSpawn() is called whenever an object is spawned, very convenient!

function onObjectSpawn(obj) -- do something with obj here end

However it comes with a specific limitation: Object scripts are not run until after the owning object has finished loading

This means that putting onObjectSpawn() in Global will not trigger for any objects that load with the mod, as the Global script is not executed until after all objects are spawned.

This also applies to objects whose script contains onObjectSpawn() as they will also only trigger for objects spawned after they load and their script is executed.

However, there is a very handy workaround. We can manually call onObjectSpawn() for all those objects as soon as Global's (or any object's) script has finished loading:

function onLoad() for _,obj in ipairs(getObjects()) do onObjectSpawn(obj) end end

Combining these two together, we will have a reliable method of triggering a script when an object spawns.
Order of execution of similar triggers
This section is extra reading, and not required to understand the main point of this guide

How are multiple scripts with the same trigger ordered?

From my testing I believe it follows the order of object creation in the game. Global is always first, followed by all the objects in the save file .json in their order they are listed in the .json, then any later loaded objects in the order they were loaded.

All functions in TTS that spawn objects (such as copy/paste, additive load, saved objects, etc), always do so in a specific order. In the case of additive load and saved objects, this is the same as above, the order they are listed in the .json file. For copy/paste this is the order of their selection (putting the hovered unlocked object first when copying, if any).

The main issue with the above is we cannot look at the current game / save file .json to figure this out. However, getObjects() returns a table of all objects in the game, and these are in order of creation. They are also in order of their script executions for similar triggers.

This can be very handy for larger mods with multiple triggers that may require interaction and ordering.
(alternative) Finding Objects by position
Another method you can use to identify your objects is by their position. There are generally three different methods:

1) Use getObjects() to get all Objects in the world, and loop through them looking at their .getPosition()

2) Use a Scripiting Zone by calling .getObjects on the Zone Object to get all Objects with a collider inside of that scripting zone.

3) Use a physics cast by calling Physics.cast() which you can think of as a single-use disposable alternative to method 2, with a few extra options and features.

I strongly recommend method 3. Physics.cast is a function which lets you find Objects based on a physics collision in a defined area. You can use a line, a box, or a sphere, and you can make this move in a straight line if desired.

The API has a reasonable example for this: https://api.tabletopsimulator.com/physics/#cast

-- Example usage -- This function, when called, returns a table of hit data function findHitsInRadius(pos, radius) local radius = (radius or 1) local hitList = Physics.cast({ origin = pos, direction = {0,1,0}, type = 2, size = {radius,radius,radius}, max_distance = 0, debug = true, }) return hitList end
-- Example returned Table { { point = {x=0,y=0,z=0}, normal = {x=1,0,0}, distance = 4, hit_object = objectreference1, }, { point = {x=1,y=0,z=0}, normal = {x=2,0,0}, distance = 5, hit_object = objectreference2, }, }
Identifying objects
Now we have a reliable script to call on every object when it spawns, we need a way of identifying our object. Remembering the GUIDs are volatile and therefore unreliable, what other systems could we use?

Name or Description
obj.getName() and obj.getDescription can be set by the UI, and this can be used identify an object, but as it is also used in the tooltip of an object, is not always ideal.

GM Notes
obj.getGMNotes() allows us to access the GM Notes of an object (accessible through the UI only by sitting in the Black seat). This is a string. It is also persistent (saving and loading of persistant vars is handled automatically by TTS).

Memo
obj.memo is also a persistent string, but is not accessible by the UI.

Tags
The tags system has recently added a convenient method for identifying objects, however as this also has some other interactions with snap points, and calling .getObjects() on scripting zones, you need to be mindful of those when tagging your objects.

.hasTag() or .getTags() being the most useful here.

How do we identify objects in real life?
In a lot of games in real life, we will identify what something is by what type of object it is .type or by a particular image on it (card back, cardboard print, etc). For all of these examples we could use things like .getCustomObject() if using one of the many Custom Objects that come with TTS, even Unity asset bundles.

We'll also sometimes identify an object by where it is, but this is something we'd likely want to check during gameplay, rather than when the object is spawned, but methods such as zone.getObjects() obj.getPosition() or Physics.cast() exist for these purposes - I'm particularly fond of using snap points to generate a physics cast to find objects above it, and identifying their location from that.
Storing objects for future reference
Let's consider our current example:

function onLoad() for _,obj in ipairs(getObjects()) do onObjectSpawn(obj) end end function onObjectSpawn(obj) -- here is the focus of this section end

Now lets apply a simple method of identifying objects by GM Notes, and storing them in a global table.

fruitObjects = {} function onObjectSpawn(obj) local gmNotes = obj.getGMNotes() if gmNotes == "Apple" then fruitObjects["Apple"] = obj elseif gmNotes == "Banana" then fruitObjects["Banana"] = obj end end

Of course these if statements aren't very efficient, but we could combine this with a Tag to know when an object is a fruitObject, and then use the gm notes to identify which fruit it is:

fruitObjects = {} function onObjectSpawn(obj) local gmNotes = obj.getGMNotes() if obj.hasTag("Fruit") then fruitObjects[gmNotes] = obj end end

However, we might have more than one "Apple" object in a mod, so we'd need to turn this into a table of some kind:

fruitObjects = {} function onObjectSpawn(obj) local gmNotes = obj.getGMNotes() if obj.hasTag("Fruit") then if fruitObjects[gmNotes] == nil then fruitObjects[gmNotes] = {} end fruitObjects[obj] = true end end

This makes it quite simple to now loop through all "Apple Fruit" objects:

for obj in pairs(fruitObjects["Apple"]) do -- end

Why store the object as the key rather than the value? See the next section.
Unique object identifiers
One of the desired features about GUIDs was that another object with the same GUID could not exist at the same time. Using these identifiers above, there is no direct way to prevent duplicate identifiers from existing (from, for example, copy/pasting an object).

If you desire this behaviour, you would need to add it yourself in onObjectSpawn(), for a crude example, appending underscores to the end of an object's gm notes

fruitObjects = {} function onObjectSpawn(obj) local gmNotes = obj.getGMNotes() if obj.hasTag("Fruit") then for i = 1,20 do -- use "while true do" at your discretion if fruitObjects[gmNotes] == nil then -- no obj exists with this gm note fruitObjects[gmNotes] = true break else -- add an underscore to the end of the object's gm notes and try again gmNotes = gmNotes.."_" obj.setGMNotes(gmNotes) end end end end
Handling destroyed objects
A common issue with storing object references is that destroyed objects maintain their references, even if they are no longer able to access Object methods. There are three methods of testing these:

Comparing against nil

Importantly, you cannot simply self-evaluate to see if an object still exists if obj then, you must explicitly compare against nil

if obj == nil then -- object has been destroyed in a previous frame end

Using .isDestroyed()

This will return true in the same frame that an object has been queued for destruction, but has not yet been destroyed

if obj.isDestroyed() then -- object has been destroyed, or is queued for destruction this frame. end

Using onObjectDestroy()

function onObjectDestroy(obj) -- object will be destroyed next frame end

Order of execution

The destroy triggers are executed in the following order. ties broken by in object creation order, as Order of execution of similar triggers section above.

  1. onObjectDestroy() -- called on all objects in the world
  2. onDestroy() - called on the object being destroyed itself

Expanding the example

In our fruitObjects example from the previous section, we have a couple of methods of maintaining our object references. This is much simpler if we keep those object references as keys, as we can easily remove them:

Removing the destroyed object references when we loop them

for obj in pairs(fruitObjects["Apple"]) do if obj.isDestroyed() then fruitObjects["Apple"][obj] = nil -- remove this key from the table end -- do something with the remaining objects here end

Removing the destroyed object references when the objects are destroyed

As the object still exists for the frame when onObjectDestroy() is called, we can still get information from the object.

function onObjectDestroy(obj) local gmNotes = obj.getGMNotes() if obj.hasTag("Fruit") then if fruitObjects[gmNotes] == nil then fruitObjects[gmNotes] = {} end fruitObjects[obj] = nil -- remove this key from the table end end
Combining spawn and destruction
This last code section above would require a lot of similar code between onObjectSpawn() and onObjectDestroy so we could combine them with something like this:

function onObjectSpawn(obj) onObjectSpawnDestroy(obj) end function onObjectDestroy(obj) onObjectSpawnDestroy(obj, true) end function onObjectSpawnDestroy(obj, isBeingDestroyed) local resultingObjExistence = true if isBeingDestroyed then resultingObjExistence = nil end -- resultingObjExistence is used to avoid code duplication below local gmNotes = obj.getGMNotes() if obj.hasTag("Fruit") then if fruitObjects[gmNotes] == nil then fruitObjects[gmNotes] = {} end fruitObjects[gmNotes][obj] = resultingObjExistence -- will be set to true if the object has just spawned, or nil (removed from table) if it is being destroyed end end
Working with objects in the same frame they spawn
When an object spawns it works as follows:

Frame 0: Object is spawned in a proxy state. Some functions are available, (most notably not .takeObject() and most physics-based functions)
Frame 1: Object is spawned fully
Frame 2: Object now evaluates its own script, then triggers onLoad() (on self) and onObjectSpawn() in all scripts

However, what if we want to identify our object in frame 0?

The only solution to this is to manually call our function that identifies, which can simply be as follows:

local takenObject = container.takeObject() onObjectSpawn(takenObject)
Taking this further
For truly large mods, metatables and metamethods would probably be the next step to take this, as once you have a reliable reference to your objects, and a method to identify them, everything else is up to you as the mod creator.
Summary
  • Use onObjectSpawn() and onObjectDestroy() as triggers.
  • Use onLoad() to loop through all getObjects() and call onObjectSpawn() on those to find objects that spawned before scripts were run.
  • Use .getGMNotes(), .memo or .getCustomObject() to identify your objects.
  • .getName() and .getDescription() can also be used, but they are also used for tooltips.
  • Tags can also be used, but be aware of their interactions snap points and using .getObjects() on Zones.
  • You can use this system to have unique identifiers, but it requires extra work to do so.

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
Luka 25 Apr, 2024 @ 9:21am 
kinda just scared as an user seeing as i collect and save objects for dnd map making
Bone White  [author] 25 Apr, 2024 @ 5:12am 
@Luka the problem caused is entirely dependent upon how the mod has been created, and how it is used. Using GUIDs to identify objects when that script is loaded is very reliable _unless_ the user is additively loading that mod or importing those scripts (on objects) after another mod has been loaded.

It can also be affected by users cloning objects and putting them int-and-out of containers.

By using a different system (as suggested above) you can pretty much eliminate the risks of this.
Luka 24 Apr, 2024 @ 5:30am 
so does this mean most mods only work by themself consistently as guids seem to be super common in most mods