Tabletop Playground

Tabletop Playground

Not enough ratings
Tabletop Playground API Primer
By Teacup
A simple primer/guide for those coming from modding TTS.
   
Award
Favorite
Favorited
Unfavorite
Tabletop Playground API Primer
Honestly, I came to this game for the promise of scripting. I was baited and got caught in the trap without complaint. One of my biggest issues with TTS was the very poorly lacking Lua API that could've been so much better and more intuitive than it was. I know this, because I worked with it plenty myself and I've worked with my own MoonSharp implementation. C# is not an amazing language for implementing a Lua API, but nonetheless it was sorely lacking.

Despite the fact that their game basically lived off of user-generated content, the developers did little to improve it. It has gotten a little better in recent times, partially because new developers (from the community) were brought on board. Even then, it's not that great and still has plenty of shortcomings.

TTP is a new, clean slate with much promise in more than just scripting, and has the potential to be much better. It without a doubt does and will have its own issues, but such things can be worked out over time, especially with a developer that communicates and listens to feedback regularly.

For some basic information on the API(read these before continuing):
https://tabletop-playground.com/knowledge-base/scripting-basics/
https://tabletop-playground.com/knowledge-base/scripting-101-how-to-get-started/
What's the difference?
Well, to begin with, the API is an implementation of JavaScript instead of Lua. This primer isn't really meant to teach you JavaScript, but you'll find some links provided below on where you can read up on the proper syntax & default functionality.

If you're familiar with any other scripting languages you should be able to pick up the language fairly quickly after looking up documentation on the specifics of JS and the differences compared to other languages.

The JavaScript V8 engine(No, not an eight-cylinder piston engine) which is what TTP is using for the API, is a (very) fast, open-source JS engine that will be doing all of the processing for your code and making it tick. You can read more about the engine here https://v8.dev/docs.

Not only is JavaScript fast and pretty reliable, but it also has a vast amount of online resources related to it. After all, this is a language used all over the internet for functionality in websites, while Lua is a little more niche. It does of course, have its own limitations and differences compared to other languages, so it's certainly not perfect.

Be sure to read up on the differences of how variables are defined, scoping, comparison operators, how classes work and what they are, etc.

https://developer.mozilla.org/en-US/docs/Learn/Getting_started_with_the_web/JavaScript_basics
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Introduction
https://stackoverflow.com/a/1022683/subtle-differences-between-javascript-and-lua

The first two websites are for the Firefox JavaScript implementation, but almost everything in the basic guide for JavaScript is relevant to what you'll be doing in TTP. It's the same language after all, just with some classes/functions missing.

For the more advanced users that just want a reference for JS and quickly see what you can do with it;

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar

Another good guide on the language is this great book by Axel Rauschmayer, shared in the discord by Omniraptor. This will explain much of what you need to know if you're just getting started in JS.

https://exploringjs.com/impatient-js/toc.html
I require your assistance
To get functionality or definitions from another script, you can use require. This will allow you to define functionality in a script that can then be called on and used by any other script in your package. See more details here:

https://nodejs.org/en/knowledge/getting-started/what-is-require/

Simple example, lets say this is a file named "myscript.js" in your Scripts folder:
module.exports.wow = function wow() {console.log("wow we did something")} function huh() { console.error("huh") } module.exports.huh = huh
In another file:
let stuff = require("./myscript") stuff.huh() // Prints "huh" as an error stuff.wow() // Prints "wow we did something"

The root folder for where require will look for scripts is in your package's Scripts folder.
TabletopPlayground\TabletopPlayground\PersistentDownloadDir\<PACKAGE_NAME>\Scripts\
This is the folder you'll be doing all your work in for a given package, as well as installing node packages if that's what you wish to do.
You can also require scripts in subfolders like so `require("utils/myscript").

You can also 'cherry pick" things from a required script.

Say you define a module like so:
module.exports.wow = function wow() {console.log("wow we did something")} module.exports.nothing = function() { } module.exports.meaningOfLife = 42 module.exports.what = "hello"

You can now pick specific things to use if you so desire.
const {wow, nothing} = require("./myscript") wow() -- Would log "wow we did something" nothing() -- Does nothing console.log(meaningOfLife) -- undefined. Would throw an error

As you can see, require is quite powerful and useful. As for cherry picking definitions from modules, there is no real performance benefit to it or anything, but it can be convenient and more readable.

As for the TTP API itself, you may have noticed if you looked at the knowledge base that the examples show that you should require the API definitions.

const {world} = require("@tabletop-playground/api") world.startDebugMode()

This technically isn't needed due to everything already being defined in the global scope, but it can add some more readability to your script, and if you're using an editor like VSCode, "requiring" the API this way will allow the editor to grab the typescript definitions and display autocompletions and function descriptions/info for you.

Like so:
Obviously, you won't be able to find these definitions in your Scripts folder. This is actually a node package, one you can require as a dependency for any packages related to TTP you publish. We will not go into that for this guide though.

You can find the file used for these definitions at
TabletopPlayground/node_modules/@tabletop-playground/api/index.d.ts
.
Module caching

You may notice that if you're developing a module for use in your scripts that your changes to these modules aren't reflected when you require it. This is because your module is automatically cached when you require it, so that subsequent requires won't be re-reading the same file over and over again.

To avoid this issue during development, you can use the following code above your require
purge_modules() // Invalidate the module cache gc() // Garbage collect require("./somethingorother")

This will flush the module cache and allow your modifications to take effect. An alternative is to simply use "Reset Scripting" instead of "Reload Scripts", as this resets the scripting environment rather than just re-running the existing scripts. This might not be desirable in some cases, so you have the method above.
Using Events
Events are defined a bit differently here, but work practically the same.

There currently aren't a whole lot of events to use right now, but here's a simple comparison on how to define them.

TTS:
function onObjectSpawn(obj) print("created an object " .. obj.guid) end

TTP:
const {globalEvents} = require("@tabletop-playground/api") // https://api.tabletop-playground.com/classes/__tabletop_playground_api_.globalscriptingevents.html#onobjectcreated // https://api.tabletop-playground.com/classes/__tabletop_playground_api_.gameobject.html#getid https://api.tabletop-playground.com/classes/__tabletop_playground_api_.multicastdelegate.html#add globalEvents.onObjectCreated.add(function(obj){ console.log("created an object ", obj.getId()) }) // or function createdSomething(obj) { console.log("created an object ", obj.getId()) } globalEvents.onObjectCreated.add(createdSomething)

All events are a MulticastDelegate, and will allow you to .add multiple callbacks.
Callbacks can be removed with MulticastDelegate.remove(), and all callbacks on one delegate can be removed with MulticastDelegate.clear()

So:
const {globalEvents} = require("@tabletop-playground/api") // https://api.tabletop-playground.com/classes/__tabletop_playground_api_.globalscriptingevents.html#ontick // https://api.tabletop-playground.com/classes/__tabletop_playground_api_.multicastdelegate.html#remove // https://api.tabletop-playground.com/classes/__tabletop_playground_api_.multicastdelegate.html#clear function test() { console.log("tick") globalEvents.onTick.remove(test) // Remove the "test" function from callbacks // globalEvents.onTick.clear() // This will do the same thing, but also clear every OTHER callback from this event. Use carefully in an environment shared with other scripts. } // This will print "tick" once. globalEvents.onTick.add(test)

Adding event callbacks to objects

To add event callback to objects, it's much the same, just different events and on a GameObject reference.

For example, in a global script:
const {world} = require("@tabletop-playground/api") // https://api.tabletop-playground.com/classes/__tabletop_playground_api_.gameworld.html#getobjectbyid // https://api.tabletop-playground.com/classes/__tabletop_playground_api_.gameobject.html#ondestroyed let gameObject = world.getObjectById("vp3") // Get an object in the world by its UID. // When this object gets removed from the world, it'll print "i got destroyed". gameObject.onDestroyed.add(function(obj) { console.log("i got destroyed") })

Or, in a script attached to an object
const {refObject} = require("@tabletop-playground/api") // refObject is a reference to the GameObject that this script is attached to. This is undefined in the global script. // When this object gets removed from the world, it'll print "i got destroyed". refObject.onDestroyed.add(function(obj) { console.log("i got destroyed") })

A useful alternative for defining one-off callback methods like this is arrow functions.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
Wait
For Wait functions in TTS, the basic equivalent in javascript is setTimeout and setInterval. There is no function for a conditional wait, but that's easy to replicate.

For better examples on setTimeout & setInterval usage, see here:
https://alligator.io/js/settimeout-setinterval/

TTS:

local dostuff = true Wait.time(function() print"wow" end, 1) Wait.time(function() print"wow infinite" end, 1, -1) Wait.condition(function() print"wow until dostuff" end, function() return dostuff end) -- Will print "wow" if dostuff is true

TTP:
let dostuff = true setTimeout(function() { console.log("wow") }, 1000) setInterval(function() { console.log("wow infinite") }, 1000) var interval = setInterval(function() { // Will run every second until dostuff is true. if (dostuff) { console.log("wow") clearInterval(interval) // We've done what we wanted to do, so we're going to kill this interval } }, 1000)
Coroutines & Promises
// I hardly use these features right now so instead I'll link some very useful resources for understanding them.

https://x.st/javascript-coroutines/

https://alligator.io/js/promises-es6/
https://alligator.io/js/async-functions/

For a very simple example on how promises & await can be used, you can see my SimpleButtons script here; https://tabletopplayground.mod.io/simplybuttons. See global.js for usage.
JSON
JavaScript has nice JSON support built-in.

https://www.w3schools.com/js/js_json.asp

https://javascript.info/json

In short, JSON.parse & JSON.stringify are the functions you need.

(Kind of) Configuration Files

Another way you can load JSON into your script is by requiring a json file.
You normally can't read files from the filesystem, but you can require a JSON file like you would a normal script and it'll read it as if you had copied the text into JSON.parse.

For example, lets say we have this JSON text inside of a file called config.json
{ "name": "John Doe", "money": 9001, "hatesMoney": false }

Then, we can load it from our script
const json = require("./config.json") console.log(JSON.stringify(json)) // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Conditional_Operator // If json.hatesMoney is true, we set the string to "hate it!", otherwise it's "love it!" // This is basically equivalent to a shorthand if statement. let str = json.hatesMoney && "hate it!" || "love it!" // With the above json file, this will print "My name is John Doe. I have $9001 and I love it!" console.log("My name is " + json.name + ". I have $" + json.money + " and I " + str)

Feel free to try this out yourself.

This can of course be used for more than just configuration files, and it's up to you what you do with it, if anything at all. Do note that if you plan to use this method to configure a script and upload your package to mod.io, your configuration file (if included in the package), will be overridden automatically whenever your package is updated. You can't automatically create this file either, so it's either reset it every update or have the user create the configuration file manually and don't depend on it in the script.

As mentioned above in the require section, If you're changing this file while reloading your scripts, you will not see your changes reflected in-game. This is because when your script requires something, that file is then cached so it doesn't have to re-read it for every subsequent require. You can see solutions for the issue above.
Interacting with other scripts
One of many issues in TTS is the fact that communication & interaction between scripts is a bit cumbersome. You could call functions and get/set variables on an object with the provided functions, but it wasn't very intuitive and was prone to error when passing certain types of data to a different script.

There isn't really any such limitation here; If you define a variable on a GameObject in one script you will be able to access it from any other script as long as you have a reference to the game object it was set on.

For example, lets say you had 2 objects on a table.
You want to call a function on another object that will print some text.

On one object:
const {refObject} = require("@tabletop-playground/api") refObject.aNumber = 9001 refObject.test = function(txt) { console.log("Wow look at that, " + txt ) }

On a different object:
const {world, refObject} = require("@tabletop-playground/api") // Get the object we want let obj = world.getObjectById("object_uid_here") // When this object is grabbed, call the .test() function on the object we grabbed with a variable defined on that object as an argument. // This will print "Wow look at that, 9001" refObject.onGrab.add(function() { obj.test(obj.aNumber) })

This really isn't anything special, but it isn't possible to do in TTS.

You may also notice that you're able to define variables on an existing object defined by the game. You can't do this in TTS either. In the TTS Lua implementation, if you try to set a variable on an object defined in .NET, it'll throw an error.

This code will error out in TTS.
Player["White"].thisGuyIsCool = true
Debugging
Debugging and interacting with scripts during runtime is much easier to do in TTP. The easiest and most comprehensive way to do it is by using the devtools in Chrome/Chromium

Simply call world.startDebugMode[api.tabletop-playground.com] in a script or in the console chat window, and then follow this link in your chromium browser.

devtools:/devtools/bundled/inspector.html?experiments=true&v8only=true&ws=localhost:9229

You can now use the chromium console like you would normally, but in TTP. The port can be specified.

By adding a debug configuration in Visual Studio Code, you can also debug there.
{ "name": "Inspector", "type": "node", "protocol": "inspector", "request": "attach", "address": "localhost", "port": 9229 }

Other editors could support remote debugging too, so google the choices for your editor of choice.

If you don't wish to do any sort of remote debugging, the in-game console tab is functional for reading logs, running small bits of code and working as an evaluator.
UI
Unfortunately there is currently no way to define UI elements in the world or on screen right now. Such things are planned, but not currently implemented.

This section will be updated once such a system exists.
Buttons
As mentioned above, there is currently no way to create any sort of UI elements, interactable or otherwise. I've included a copy of simplybuttons.js in this repo, which you can find here.

This script will allow you to create some semblance of buttons, which will run given callbacks and prevent players from moving the buttons. You cannot override the onGrab/onReleased events on any buttons, but that's what the callbacks are for.

Example Usage:

const {world} = require("@tabletop-playground/api") const buttons = require("./simplybuttons") // require script and directly use functions with makeButton/makeButtonByID // const {makeButton, makeButtonByID} = require("./simplybuttons") // Make a button with only the uid; Equivalent to getting an object with world.getObjectById("UID_HERE") and passing it to makeButton // Will print "button grabbed" when pressing the button, and "button released" when letting go. Second function is unneeded for a basic toggle button. buttons.makeButtonByID("UID_HERE", function(gameObject, player) { console.log("button pressed") }, function() { console.log("button released") }) // Make a button with a reference to a GameObject. Will print "button grabbed" when pressing the button, and "button released" when letting go. Second function is unneeded for a basic toggle button. buttons.makeButton(refObject, function(gameObject, player) { console.log("button pressed") }, function() { console.log("button released") })
Node Packages
I'm not going to go into this too much because there's tons of resources for it online and it's kind of a pain to explain with my limited understanding of the node package environment. Perhaps somebody else could expand on this.

NOTE: This section is assuming you have Node.js installed on your system and have very basic knowledge on how to operate a command prompt/terminal. This applies to linux too when those builds for the game are available.

To install/get the binaries for Node.js see https://nodejs.org/en/

You are able to install packages from the node package registry into your package's Scripts folder and use the installed package like you would in a node.js script.

For example, say you're familiar with lodash and want to use it in TTP. You can do that! You could either download one of the builds from https://lodash.com/ and drop it in your Scripts folder, or you could install the node package for it.

To install a node package, first create a node_modules folder inside of your package's Scripts folder if it doesn't already exist(it should). This'll prevent your package from being installed into TTP's node_modules folder instead of the intended Scripts folder.

Open a command prompt and navigate to your package's Scripts folder.

Type this command:
npm install lodash

This will install lodash into your node_modules folder. If you've made your game package into a node package, it'll automatically add it as a dependency. This has no effect in-game, if you don't know what it means don't worry about it

To use your snazzy new lodash package, all you have to do is require it in one of your scripts.
const _ = require("lodash")

You can see the package online too. https://www.npmjs.com/package/lodash

Many packages can be used in this way, though not all will work, considering various differences between TTP's implementation and node.js.

My SimpleButtons mod.io package mentioned above is also available as a node package. You can install it & use it with the same process.
npm install @salami-ttp/simplybuttons
const buttons = require("@salami-ttp/simplybuttons")

The "@salami-ttp" part of the package name is a "scope". This separates the package name "simplybuttons" from the rest of the npm package registry.

Packages installed this way can be updated with
npm update

The TTP typescript definitions for the API are also on the package registry, under the name
@tabletop-playground/api

For more information on node packages, how to create your own, publish them, share them, etc, use google to look up on it. There's a lot of documentation for it.

If you do publish a node package for TTP that others can use, I personally recommend putting it under your own user-scope or with this prefix if putting it under the global scope
ttp-
Scripts using TTP functionality, although the definitions for them are available on the registry, cannot actually be used outside of the game, which is not something that is readily available unless you pay for it. Therefore, unless it's under your own user-scope, having the package prefixed can make it easier for other users to identify what your package is.

Suggestions
This is a guide I threw together based off of an old version of this same primer I made & posted on github 2 months ago.

If you've got suggestions for new/revised sections, or you see something here that is wrong/misleading, feel free to leave a comment.
1 Comments
Keisha Jan 15, 2022 @ 12:02am 
is it safe to Sign into mod.io? thank you