TPN Script Reference
TPN script is a “scripting language” of sorts that I am using in my Twitch chat.
This syntax was initially devised for Twitch Plays Noita, so the following reference will use things like pressing keys and mouse movement in examples.
Command structure
A single simple command looks like this:
any text command~ any other text
A command that accepts arguments can look like this:
some more text here too command:arg1:arg2~
The standard format for commands is a ~
suffix. Historically, this has
happened because I “didn’t want to be like the rest”. Almost immediately, a +
prefix was also introduced for mobile users, and there was this whole thing
about collecting statistics on which version was used more, but that idea was
eventually scrapped.
A bit later, the lenient command parser was introduced, which basically allows most typical symbols to be command prefixes.
Here’s a list of acceptable syntaxes, there’s no difference between them:
kawaii~
- the initial one+crusader
- the anti-weeb one~prefix
- because people sometimes forgot ¯\_(ツ)_/¯!normie
- to pretend to be any other bot?question-mark
- I’ve seen some bots use that..#hashtag
- ..and thatsilent:arg1
- if there’s at least one argument, meaning:
is present, you can just drop the symbol altogether actually.
Another small quirk is commands of the form command123~
would first try to
run a command or macro named command123
, but if neither exist it would also
try command:123~
, making the number into a first argument.
This is a shorthand to avoid typing :
as commands having a single numerical
argument were quite common in TPN.
Sequential commands
So commands can occur anywhere in the message and use this weird argument syntax for a reason: a message can have multiple commands, they will be executed one after the other, in parallel with all other messages(!).
This matters because certain commands take some time to execute, an obvious
example being wait~
, which does nothing for 500ms.
As an example, you can do this:
left~ wait~ wait~ right~
Which will hold the a button for 500ms (the default), wait for a second and then hold d for another 500ms.
Parallel commands
What if you want to hold the d key and tap the w key at the same time to levitate? Or to climb an obstacle? Run and shoot?
All command sequences separated with a |
symbol start and execute in
parallel.
The /
symbol would also work, it was added for mobile users.
So tap-levitating while moving to the right could look something like this (note that I haven’t tested those particular timings):
up:100~ wait:400~ up:100~ wait:400~ up:100~ wait:400~ | right~ right~ right~
As a general rule, commands that need or can wait for a variable amount of
time, so holding down keys, wait~
etc, accept an optional argument for the
number of milliseconds with default usually being 500.
Commands after the |
start executing immediately, in parallel with the
commands before it - so in this example you press d for 1.5 seconds,
while tapping w 3 times every 0.5 seconds for 0.1 seconds each.
You can have any number of commands and any number of parallel commands, the only limit is the 500-character Twitch limit.
Advanced
Scope creep happened, and during TPN I added countless requested features, starting with macros and loops and ending with variable substitution and math.
In the following examples I use echo
excessively, but in my twitch chat
echo
is something only mods can use because it straight up makes the bot
say whatever you give it, with no indication that it’s in response to
your echo commands.
You can replace any echo invocation with something like
let:x:"whatever" get:x
if you want to debug things or something.
I need to add a viewer-available debug print command, or make a global macro for that let-get thing I just suggested at least..
All command parameters are strings. You can do something like
repeat:10:" echo:\"hello world\" "
and as you can see, the repeat
command evaluates the string given to it in
the second parameter, calling any commands contained within N times.
Another thing you can see, is that nested strings require those ugly escapes
(\"
) for it to work properly.
Brace syntax
So another string syntax exists, this time using balanced braces:
repeat:10:{ echo:{ hello world } }
Two important points:
- The second argument given to repeat is still just a string.
- Brace strings are trimmed, that is,
{ hello }
is equivalent to"hello"
, not" hello "
.
Variable substitution
You can set persistent personal variables using the set
command, or temporary
ones only visible inside of the current message with the let
command.
There are a couple more variable manipulation commands in the full list below.
In any string, if you have a bit that looks like %name
, it would be replaced
by the contents of that variable - or by nothing if the variable was not set.
let:name:necauqua echo:{ Hello, %name! }~
Repeats and loops expose a variable named i
to the script they’re evaluating:
repeat:10:{ echo:{ countdown %i } }
Macros expose the arguments they are given when called as numbered variables:
macro-record:test:{ echo:{ hello, %1 } } test:world
There is also a syntax for default values in case the variable does not exist:
echo:{ Hello, %(name:stranger)! }
The default value (stranger
) in this case, also gets variable expansion,
recursively:
echo:{ Hello, %(1:%(default-name:stranger)) }
That last example first tries to use the variable 1
(first macro argument),
then the variable default-name
and then defaults to stranger
.
Math expressions
There is actually a third string syntax, this time using parenthesis:
echo:(2 + 2 * 2) <- this will print 6
This syntax forces the given expression to be evaluated as if the command expected a number, which means it would be coersed to a number (which includes passing it through a simple calculator) if possible, and error otherwise.
This works exceedingly well with string substitution:
repeat:10:{ let:double:(%i * 2) echo:%double }
For example to make an increasing counter:
macro-record:count:{ set:counter:(%(counter:0) + 1) echo:{ counted %counter times } }
count~
count~
count~
Syntax debug
Using the exact same code that the bot uses to parse commands (thanks to WASM), I made a small website that parses commands and shows you which of them and in which order with what substitutions the bot will see.
The site has no knowledge of any existing commands, it just checks the syntax, and if a string parameter looks like it contains commands, it will be parsed out and shown in a tree, its quite neat.
Play with it at https://uq.rs/lexer - hosted on my other server that’s not in Ukraine like this site, because Ukraine blocks traffic from one particular country for free, I appreciate the feature, but some people have had the critical lack of luck to be born there sadly.
The list of all commands
This is is auto-generated, so it is up to date, there are no hidden commands or parameters 😉
As of right now, this list still includes TPN-specific commands, notably ones for controlling Noita or OBS.
Such commands are obviously disabled and would do nothing, at least until I run TPN 2.0 in a year or so.
The question mark after the argument name means that it is optional.
Shortcodes are alternative short names for certain commands.
The >= thingie means that the command requires that permission level or above.
Permission levels
I am obviously just looking at the message badges lmao- (7) Caster - Only the broadcaster can use this command.
- (6) Moderator - Channel moderators and above can use this command.
- (5) TwitchAdmin - Twitch administrators and above can use this command.
- (4) TwitchStaff - Twitch staff and above can use this command.
- (3) Vip - Channel VIPs and above can use this command.
- (2) Verified - Verified users (partners) and above can use this command.
- (1) Subscriber - Channel subscribers and above can use this command.
- (0) Viewer - Anyone can use this command.
Data
-
seed~ >=Vip
Read the current seed
Global timeout: 15s
-
death-count~ shortcode: deaths
Read the current death count
Global timeout: 15s
-
kicks~
The amount of kicks registered by the game in the current run
Global timeout: 15s
-
perks:top-n?~
Read the currently picked up perks. Look ma, streamer wands at home!
Global timeout: 5s
Arguments:
- top-n: a non-negative number, optional
-
damage-multipliers~ >=Vip
Read the current non-1 damage multipliers of the player entity.
Global timeout: 5s
-
check-flag:flag~
Checks if the persistent flag was set in the running save.
Global timeout: 5s
Arguments:
- flag: string
-
pillar-progress~
Shows the amount of completed pillars vs total.
Global timeout: 5s
-
pillar-todo:top-n?~
Shows all the pillar achievements not yet completed in the running save.
Global timeout: 5s
Arguments:
- top-n: a non-negative number, optional
-
pillars-done:top-n?~
Shows all the pillar achievements already completed in the running save.
Global timeout: 5s
Arguments:
- top-n: a non-negative number, optional
-
spell-progress~
Shows the amount of unique spells ever cast vs total.
Global timeout: 5s
-
spell-todo:top-n?~
Shows all the spells that were never cast in the running save.
Global timeout: 5s
Arguments:
- top-n: a non-negative number, optional
-
spells-done:top-n?~
Shows all the spells that were already cast in the running save.
Global timeout: 5s
Arguments:
- top-n: a non-negative number, optional
-
player-pos~ >=Moderator
Prints the current player position in pixels
-
entity-count:tags~ >=Subscriber
Count the amount of loaded entities.
Can be filtered down by tags, for example
entity-count:gold_nugget~
.Per-user timeout: 5s
Arguments:
- tags: the rest of the arguments
-
entity-tag-counts~ >=Vip
Aggregate a top list of tags that mark loaded entities.
Per-user timeout: 5s
Economy
-
balance~
Check your current balance of charges
Per-user timeout: 3s
-
unleash-me~ cost: 1⚡︎
Spend a charge to remove all of your current timeouts.
-
transfer:target:amount~ cost: 0.004⚡︎
Transfer some of your charges to another user
Per-user timeout: 3s
Arguments:
- target: a user login
- amount: a number of charges, in form of a number with up to 3 decimal places
Macros
-
macro-record:name:script~ shortcode: mr
Stores a string as a personal macro.
Commands can accept complicated strings if you put them in quotes like so:
macro-record:hop:"wait~ up~ wait~ up~ wait~ up~ wait~ up~"~
Arguments:
- name: string
- script: a script string that will not have it’s vars expanded
-
macro-delete:name~ shortcode: md
Deletes a macro created with
macro-record~
.Arguments:
- name: string
-
global-macro-record:name:script~ >=Moderator shortcode: gmr
Stores a string as a global macro, meaning it can be used by everyone.
Arguments:
- name: string
- script: a script string that will not have it’s vars expanded
-
global-macro-delete:name~ >=Moderator shortcode: gmd
Deletes a macro created with
global-macro-record~
.Arguments:
- name: string
-
macro-print:name:chatter?~ shortcode: mp
Replies with the stored macro.
Can peek at other users macros if their login is specified, they’re all public here.
Per-user timeout: 5s
Arguments:
- name: string
- chatter: a user login, defaults to you
-
global-macro-print:name~ shortcode: gmp
Replies with the stored global macro.
Per-user timeout: 5s
Arguments:
- name: string
-
macro-list:chatter?~ shortcode: ml
List macros you/given chatter has recorded.
Per-user timeout: 5s
Arguments:
- chatter: a user login, defaults to you
-
global-macro-list~ shortcode: gml
List global macros recorded.
Per-user timeout: 5s
-
yoink:name:chatter?:rename?~
Copy someones macro to yourself.
Arguments:
- name: string
- chatter: a user login, defaults to you
- rename: string, optional
-
macro:name:chatter?:rest~ shortcode: q
Run the macro.
Macros can call other macros, but there is a recursion limit!
Also you can run a macro recorded by someone else by appending their login as the second argument.
Arguments:
- name: string
- chatter: a user login, defaults to you
- rest: the rest of the arguments
-
repeat:times:script~
Execute a given string several times.
jump five times: repeat:5:" wait~ up~ "~
The total limit of repetitions in a given message is 1000! This counts all repetitions, nested in each other or inside of macros etc etc. Once the limit is reached, the command will error out.
Arguments:
- times: a number in range from 0 to 1000
- script: a script string that will not have it’s vars expanded
-
group:script:custom-status-name?~ shortcode: g
Executes a given string, basically identical to
repeat:1:"script"
.This is useful to group together parallel actions, for example:
wait:5s~ group:" up~ | left~ "~
Additionally, there is an optional name that you can attach to the group to have it shown on the status wall.
Arguments:
- script: a script string
- custom-status-name: string, optional
-
try:script~
Similarly to
group
, executes a given string without counting towards limits.The difference is that any script errors are ignored, and this command always succeeds, without preventing the repeats from continuing or setting last-error.
Arguments:
- script: a script string
-
lock:script~ shortcode: b
Similarly to
group
, executes a given string without counting towards limits.The difference is that only one
lock
script can run at a time, if one is already running this command does nothing.Arguments:
- script: a script string
-
loop:script~ >=Subscriber
Repeatedly execute a given script indefinitely - until an interrupt is sent.
A special condition: a single loop iteration must take at least 100ms, otherwise after the first iteration the command will error out.
Unless you have a repeat inside of the loop that exceeds it, the loop is not limited by the repeat limit.
The only way to stop a running loop is
interrupt~
, and putting a loop inside of a loop is obviously pointless.stalling: repeat:5:" wait~ up~ "~
Arguments:
- script: a script string that will not have it’s vars expanded
-
math:value~
Evaluates and prints a given math expression. Mostly useful for debugging my calculator bugs :(
Per-user timeout: 3s
Arguments:
- value: a number
-
set:name:value?~
Stores text that will be replaced in any commands you call (including macros) if you reference it as
%name
.Note that while this command accepts text, an argument wrapped in parenthesis - without quotes or braces - will be passed through the calculator first as if the command expected a number.
Arguments:
- name: string
- value: string, optional
-
let:name:value?~
Similar to
set~
, but the text is only stored for the chat message being executed.Arguments:
- name: string
- value: string, optional
-
del:names~
Deletes a variable. Can delete more than one at once.
Arguments:
- names: the rest of the arguments
-
list-vars~
Lists all of your variables.
Per-user timeout: 5s
-
get:name~
A debug command that replies with the value of the given variable.
Per-user timeout: 5s
Arguments:
- name: string
-
clear~
Clears all of your variables.
-
if-eq:a:b:then:else?~
Runs the
then
script if given values are equal, otherwise runs theelse
script if given.Arguments:
- a: string
- b: string
- then: a script string
- else: a script string, optional
-
if-ne:a:b:then:else?~
Runs the
then
script if given values are not equal, otherwise runs theelse
script if given.Arguments:
- a: string
- b: string
- then: a script string
- else: a script string, optional
-
if-ge:a:b:then:else?~
Runs the
then
script if the first number is greater than or equal to the second, otherwise runs theelse
script if given.Arguments:
- a: a number
- b: a number
- then: a script string
- else: a script string, optional
-
if-gt:a:b:then:else?~
Runs the
then
script if the first number is greater than the second, otherwise runs theelse
script if given.Arguments:
- a: a number
- b: a number
- then: a script string
- else: a script string, optional
-
if-le:a:b:then:else?~
Runs the
then
script if the first number is less than or equal to the second, otherwise runs theelse
script if given.Arguments:
- a: a number
- b: a number
- then: a script string
- else: a script string, optional
-
if-lt:a:b:then:else?~
Runs the
then
script if the first number is less than the second, otherwise runs theelse
script if given.Arguments:
- a: a number
- b: a number
- then: a script string
- else: a script string, optional
Moderation
-
banish:chatter:duration?~ >=TwitchStaff
Instantly banish a user to the shadow realm.
Can be temporary if a duration is provided.
Arguments:
- chatter: a user login
- duration: duration in freeform format (using
humantime
Rust library), so10s
, or2 minutes
or1h30m
etc, optional
-
unbanish:chatter~ >=Moderator
Restore users ability to use the bot, bringing them back from the shadow realm regardless of their crimes.
Arguments:
- chatter: a user login
-
banished:chatter~
Check if a user was yeeted into the shadow realm.
If you are yeeted, the bot ignores you utterly, so this won’t work ¯\_(ツ)_/¯.
Per-user timeout: 15s
Arguments:
- chatter: a user login
Sounds
-
tts:msg~ cost: 0.5⚡︎ / >=Subscriber global macro exempt
Say something on stream through the TTS.
Per-user timeout: 1m
Arguments:
- msg: string
-
play-sound:sound-id~ >=Caster global macro exempt
Play a sound on stream. Sounds ids are secret, but look through
global-macro-list~
for macros that use this command to get an idea of what sounds are available.Sender gate is at least 10 seconds for everything, but individual sounds can have their own dynamic cooldowns (currently all sounds at 1 minute).
Per-user timeout: 10s
Arguments:
- sound-id: string
-
now-playing~ shortcode: np
Get the title of the song that’s currently playing on stream, if any.
Global timeout: 5s
-
skip~ cost: 1⚡︎ / >=Vip
Skips the song that’s currently playing on stream, if any.
Global timeout: 15s
-
song-request:url-or-id~ shortcode: sr
Adds the given song to the YouTube Music queue.
This only accepts YouTube links or IDs.
Note that the song will be added to the front of the queue - this is because usually the queue is full of songs from my stream playlist and the point of the command is to show me a song you think I wont insta-skip :)
Per-user timeout: 1m
Arguments:
- url-or-id: string
-
volume:volume~ >=Vip
Set YouTube Music volume.
Per-user timeout: 3s
Arguments:
- volume: a number in range from 0 to 100
Stats
-
stat:chatter?:word?~
Count the amount of messages typed by a chatter.
If given a word, counts the messages containing it (given string can give multiple words separated by spaces).
Login defaults to the sender (you can do
stat::word~
too).Per-user timeout: 3s
Arguments:
- chatter: a user login, defaults to you
- word: string, optional
-
stat-global:word?~
Similar to
stat
except works across all of chat.Without an argument counts all messages ever typed in chat since I started archiving it (which is way before the Twitch Plays Noita happened).
With an argument filters messages by the given word(s), just like
stat
.Per-user timeout: 3s
Arguments:
- word: string, optional
-
first-message:chatter?~
Get the first message sent by a user (or you) in chat.
Per-user timeout: 3s
Arguments:
- chatter: a user login, defaults to you
-
last-message:chatter?~
Get the last message sent by a user (or you) in chat.
Per-user timeout: 3s
Arguments:
- chatter: a user login, defaults to you
-
top:n?~
Get a list of top-N chatters of all time, by number of sent messages.
Per-user timeout: 10m
Arguments:
- n: a number in range from 1 to 15, optional
-
rank:chatter?~
Get the place of the chatter (or you) in the “leaderboard” of how many messages they
spammedsentPer-user timeout: 1m
Arguments:
- chatter: a user login, defaults to you
Util
-
ping~ cost: -1⚡︎
Respond with “pong!”.
I heard that scarcity creates value, so getting a pong is very cool and pog, because only one person can get it in an hour.
There is also some magical property to this command..
Global timeout: 1h
-
echo:text~ >=Caster global macro exempt
A building block for basic static text commands.
This just makes the bot print the given text, but non-moderators can only call it through global macros.
So you can call a global macro
discord~
which will resolve toecho:"discord link etc"~
and print it.Per-user timeout: 5s
Arguments:
- text: string
-
last-error:chatter?~
Respond with the last error message for user.
Last error message is set when you run invalid commands, or if the command execution managed to crash somehow. In the latter case, you’ll be given the message id - please send it to me to look at logs and fix the issue.
Per-user timeout: 3s
Arguments:
- chatter: a user login, defaults to you
-
is-game-running~
Check if noita.exe process is present, aka not dead.
Per-user timeout: 1m
-
wait:duration?~ shortcode: w
Wait for a specified duration milliseconds.
Very useful for multi-command messages.
Arguments:
- duration: duration in milliseconds, will be limited to at most 300000, defaults to 500
-
break:chatter?~
Complete all current holds immediately.
“Holds” here are referring to all “non-instantaneous” commands that are currently being executed right now - so movement,
hold~
andwait~
.This is kind of a niche thing, most likely you need
interrupt~
.Arguments:
- chatter: a user login, optional
-
interrupt:chatter?~
Stop running all current commands.
This is similar to
break~
, except the commands following the holds that get completed do not run.Arguments:
- chatter: a user login, optional
-
discard~
Interrupt all commands originating from the current message.
On it’s own this does nothing, but it can be used to limit the duration of the current message or similar.
-
flag:flag~ >=Moderator
Set a bot flag.
Flags that currently do things are:
full-stop
: disables processing any commands from non-modsno-restarts
: disables the game restarting on player deathnightmare
: enables the nightmare mode
If the flag argument is prefixed with
-
it is removed if it was set previously.Arguments:
- flag: string
-
set-nightmare:value~ >=Vip
Makes the bot start the game in nightmare/ng.
Arguments:
- value: a boolean value (true/t/yes/y/1 or false/f/no/n/0)
-
fix-seed:seed~ >=Vip
Makes the bot start the game with a specific seed.
Arguments:
- seed: a non-negative number
-
unfix-seed~ >=Vip
Undoes the effect of
fix_seed~
, so the game will start with a random seed again. -
what-is:name:to?~
Get information about a macro or command.
If you see someone running some weird command, run
what-is:command:their-name~
to figure out what it was.Per-user timeout: 3s
Arguments:
- name: string
- to: a user login, defaults to you
Voting
-
yes~
Vote yes in an ongoing vote.
-
no~
Vote no in an ongoing vote.
-
is-vote~
Check if there is an ongoing vote and what it is about.
Global timeout: 10s
-
votekick:chatter~
If several(!) people run this command with the same Twitch login (this is important, use their login, not display name) as an argument - this will start a vote to send that user to the shadow realm.
The vote has to have >=70% yes votes for them to get yeeted.
By “shadow realm” I mean that their commands will be ignored - initially for an hour, but if they get voted for the second time then forever, unless a moderator clears them.
This is obviously to deal with trolls and other problematic users. Channel moderators and above are immune.
Be aware that there can be only one vote at a time and this command has a large per-user cooldown, so dont waste it.
Per-user timeout: 5m
Arguments:
- chatter: a user login