PAISLey (Plasma Automation / Instruction Scripting Language) is a scripting language designed to allow for easy command-based behavior scripting. The purpose of this language is NOT to be fast. Instead, Paisley is designed to be a simple, light-weight way to chain together more complex behavior (think NPC logic, device automation, etc.). This language is meant to make complex device logic very easy to implement, while also being as easy to learn as possible.
Q: Why not use Lua instead?
A: The biggest advantage Paisley has over Lua nodes is that the script does not require any "pausing" logic that one would have to implement in order to get the same functionality in Lua; the Paisley runtime will automatically pause execution periodically to avoid Lua timeouts or performance drops, and will always wait on results from a command before continuing execution.
Q: Why not use sketch nodes instead?
A: Paisley was designed as a language for scripting NPC behavior, and the thing about NPCs is that their behavior needs to be able to change in response to various events. First off, when using just sketch nodes, dynamically changing an NPC's programming is downright impossible. Second, connecting all the nodes necessary for every possible event is difficult and time consuming (for example NPC chatter, a sequence of movements, events that only happen ONCE, etc). TL;DR: NPC logic is difficult to implement using just sketch nodes. Trust me, I've done it.
Q: How do I connect the Paisley engine to my device?
A: Get the Paisley Engine device from the Steam workshop (ID 3087775427), and attach it to your device. Then in a different controller, set the inputs (Paisley code, a list of valid commands for this device, and optional file name), and make sure the "Run Command" output will eventually flow back into the "Command Return" input. Keep in mind this MUST have a slight delay (0.02s at least) or Plasma will detect it as an infinite loop!
Q: Why???
A: haha
Q: Isn't writing an entire compiler in Plasma a bit overkill?
A: Your face is overkill. Compilers are fun, fight me.
As a general rule, white space and line endings do not matter in Paisley.
The only use of line endings is to separate commands, which can also be done with a semicolon ;
character.
A Paisley script may consist of a series of comments, statements, and/or commands.
#
character and continue to the end of the line. There are no multi-line comments.Before continuing, note that commands do not have to be hard-coded. You can put expressions in them, such as
let r = 500
print "r = {r}, d = {3.14 * r * r}"
See how in the above, expressions are contained inside curly braces, {}
. More on that later.
"If" statements have the following structure:
if {expression is truey} then
...
elif {expression is truey} then
...
else
...
end
You can also leave out the "then" clause if all that's needed is the "else" clause, e.g.:
if {expression is truey} else
... do this if expression is falsey ...
end
Note that, unlike Lua's elseif
keyword, the appropriate "else if" keyword in Paisley is elif
. Also keep in mind that if statements convert the expression to a boolean, and so use a few rules to test an expression's trueness: false, null, zero, and empty strings, arrays and objects are falsey, everything else is truey.
There is also the match
structure, which is similar to c-like languages' switch/case
structure (or Rust's match
). This structure is included to allow for more readable logic with less repeated code.
match {expression} do
if {case 1} then ... end
if {case 2} then ... end
...
else
... default action if no cases match ...
end
For example:
match {random_int(1,5)} do
if 1 then print one end
if 2 then print two end
else
print "some other number"
end
Of course, like if
statements, the else
branch is optional and can be excluded.
While and For loops have a similar syntax to Lua:
while {expression is truey} do
...
end
for value in {expression} do
...
end
for key value in {pairs(object or array)} do
...
end
These are the only loop structures possible. Note that the middle loop type will iterate over all values in an array, and all keys in an object!
If you want syntax similar to Lua's integer for loops (for i = 1, 10 do ... end
), you can use something like for i in {1:10} do ... end
.
If you want an infinite loop, just use something like while 1 do ... end
or while {true} do ... end
.
Variable assignment always starts with let
, e.g.
let pi = 3.14
let circumference = {2 * pi * r}
Note that the let
keyword is required even when reassigning variables.
For example, consider the following:
let var = 13
var = 99
The second line will not set var's value to 13. Instead, that would attempt to run a command called "var" with the parameters ["=", "99"]
.
Of course, sometimes a variable will contain an array that you don't want to overwrite, instead you just want to update a single element or append to the array.
The following will result in var containing the array (1, 2, 123, 4, 99)
. Note that giving negative values as the index will start counting from the end, so index of -1 will update the last element.
let var = 1 2 3 4 5
let var{3} = 123
let var{-1} = 99
Appending is just as simple. The following will result in var containing the array (1, 2, 3, 4, 5, 6)
.
let var = 1 2 3 4 5
let var{} = 6
You can also assign multiple variables at the same time.
let a b c = 1 2 3
#Alternatively, you can assign variables from values in an array
let list = {1:9}
let a b c = {list}
To simply define a variable as null, you can just leave off the expression. The following all initialize variables as null.
let var
let a b c
let foo = {null}
REMEMBER: All variables are global, so any "re-definition" of a variable just sets it to the new value.
There is a special keyword for setting a variable's value if it hasn't been assigned already.
initial variable = {value}
Unlike the let
keyword, initial
can only define one variable, and it cannot insert or update sub-elements in arrays. The use of initial
is instead a concise way to set a default value for un-initialized variables. Logically, it is identical to the following:
if {not (variable exists)} then
let variable = {value}
end
Proper use of subroutines can let you easily reuse common code.
Subroutines are basically user-defined functions that are called as if they were commands. Like commands, they can take parameters and optionally return a value, but they don't have to. Unlike commands, they can modify global variables, which may or may not be desired. Just keep it in mind when writing subroutines.
An example subroutine usage might look like the following:
subroutine print_numbers
for i in {0 : @[1]} do
if {i > 30} then
print "whoa, too big!"
return
end
print {i}
end
end
gosub print_numbers 10
gosub print_numbers 50
subroutine power
return {@[1] ^ @[2]}
end
print ${gosub power 2 10}
See how in the above, the @
variable stores any parameters passed to a subroutine as an array, so the first parameter is @[1]
, the second is @[2]
and so on. For constant indexes, the square brackets are optional, e.g. @1
and @2
will also work, but not @ 2
.
Also see that subroutines return values the same way that commands do, using the inline command evaluation syntax, ${...}
.
Note that it is also possible to jump to subroutines with an arbitrary label ID. See how in the following example, the program will randomly call one of 5 possible subroutines, and then print "Subroutine exists".
if gosub "{random_int(1,5)}" then
print "Subroutine exists"
end
subroutine 1 end
subroutine 2 end
subroutine 3 end
subroutine 4 end
subroutine 5 end
Some subroutines may take a very long time to compute values, when we only really need them to be computed once for any given input.
For these kinds of subroutines, the cache
keyword can be used to memoize the subroutine and only compute the results once.
See the following recursive fibonacci example:
cache subroutine fib
if {@1 < 2} then return {@1} end
return {
${gosub fib {@1 - 1}} +
${gosub fib {@1 - 2}}
}
end
Subsequent calls to fib
will be very fast, because each fibonacci number only has to be computed once.
If it turns out that you need to invalidate a specific subroutine's cache, you can manually do so:
break cache fib
If the subroutine is not memoized, this of course does nothing.
Lambdas are another good way to reuse code, however unlike subroutines, these are specifically for reusing parts of expressions.
Lambdas are defined with the syntax ![expression]
, and are referred to with that same !
identifier, just without the brackets. Note that the !
can be any number of exclamation marks, optionally followed by an alphanumeric identifier. So for example, !!
, !2
, and !!lambda_1
are all valid lambda identifiers, all referring to different lambdas. Note that unlike lambdas in other languages, lambdas in Paisley are not true functions; they don't take any parameters. However, they do have access to the same global scope as the rest of the program.
Below is an example of lambda usage. Both the top and bottom commands will print 5 random numbers in the range 0-100.
print {![random_int(0, 100)], !, !, !, !}
#do the same thing, but using the define keyword
define {!rnd[random_int(0, 100)]}
print {!rnd, !rnd, !rnd, !rnd, !rnd}
Note that either of the above commands are equivalent to the following:
print {random_int(0, 100), random_int(0, 100), random_int(0, 100), random_int(0, 100), random_int(0, 100)}
Unlike variables, lambdas are restricted to their scope. Thus, for example, if you define a lambda in a subroutine, you cannot use it outside the subroutine, unless that outside scope also has a lambda definition with the same identifier.
break
or break 1
or break 2
etc, will exit as many while/for loops as are specified (defaults to 1 if not specified)continue
or continue 1
or continue 2
etc, will skip an iteration of as many while/for loops as are specified (defaults to 1 if not specified)delete
will delete the variables listed, e.g. delete x y z
stop
will immediately halt program execution.return
returns from a subroutine back to the caller.define
will parse the following expression(s) but will ignore them at run time. This is most useful for defining lambdas outside of where they're used.First and foremost, expressions will only be evaluated inside curly braces, {}
. If you place an expression outside of braces, it will be treated as plain text. For example print {1+2}
will print "3" but print 1+2
will print the actual string "1+2".
Expressions can be placed anywhere inside a command or statement operand. In addition, they can also be placed inside double-quoted strings (e.g. "a = {1+2}"
gives a = 3
) to perform easy string interpolation. Note that single-quoted strings do not interpolate expressions, so for example 'a = {1+2}'
would give exactly a = {1+2}
without parsing any expression.
If you would like to avoid interpolation in double-quoted strings, simply escape the opening curly brace with a backslash, e.g.
print "the expression \{1+2} evaluates to {1+2}"
print "you can also put \"quotes\" and line breaks (\n) inside strings!"
There are a few special escape sequences:
\n
outputs a line ending.\t
outputs a tab.\r
outputs a carriage return.\
(backslash + space) outputs a non-breaking space.\"
outputs a double quote.\'
outputs a single quote.\{
outputs a left curly brace.\}
outputs a right curly brace.
There are also a bunch of escape sequences that correspond to emoticons, included for convenience:\^-^
outputs <sprite=0>
\:relaxed:
outputs <sprite=0>
\:P
outputs <sprite=1>
\:yum:
outputs <sprite=1>
\<3
outputs <sprite=2>
\:heart_eyes:
outputs <sprite=2>
\B)
outputs <sprite=3>
\:sunglasses:
outputs <sprite=3>
\:D
outputs <sprite=4>
\:grinning:
outputs <sprite=4>
\^o^
outputs <sprite=5>
\:smile:
outputs <sprite=5>
\XD
outputs <sprite=6>
\:laughing:
outputs <sprite=6>
\:lol:
outputs <sprite=6>
\=D
outputs <sprite=7>
\:smiley:
outputs <sprite=7>
\:sweat_smile:
outputs <sprite=9>
\DX
outputs <sprite=10>
\:tired_face:
outputs <sprite=10>
\;P
outputs <sprite=11>
\:stuck_out_tongue_winking_eye:
outputs <sprite=11>
\:-*
outputs <sprite=12>
\;-*
outputs <sprite=12>
\:kissing_heart:
outputs <sprite=12>
\:kissing:
outputs <sprite=12>
\:rofl:
outputs <sprite=13>
\:)
outputs <sprite=14>
\:slight_smile:
outputs <sprite=14>
\:(
outputs <sprite=15>
\:frown:
outputs <sprite=15>
\:frowning:
outputs <sprite=15>
Expressions also give access to a full suite of operators and functions, listed below:
+
-
*
/
//
(divide then round down)%
^
, e.g. a^3
raises a
to the 3rd power.and
, or
, xor
, not
. Note that these operators do NOT short-cut, i.e. given an expression a and b
, b
is evaluated even if a
is false.>
, >=
, <
, <=
=
or ==
(both are the same)!=
or ~=
(both are the same)exists
(e.g. x exists
)&
(e.g. &variable
):
. Note that slices are inclusive of both their upper and lower bounds (e.g. 0:5
gives (0,1,2,3,4,5)
),
(e.g. 1,2,3
is an array with 3 elements, (1,)
is an array with 1 element, (,)
has 0 elements, etc). can combine this with slicing, e.g. 1,3:5,9
gives (1,3,4,5,9)
.like
, checks whether a string matches a given pattern (e.g. "123" like "%d+"
gives true
).in
(e.g. 3 in (1,2,4,5,6)
gives false
)val1 if expression else val2
. Like Python's ternary syntax, this will result in val1
if expression
evaluates to true, otherwise it will result in val2
.[]
. Like most languages, this lets you get an element from a string, array, or object (e.g. "string"[2]
gives "t", ('a','b','c')[3]
gives "c", and ('a'=>'v1', 'b'=>'v2')['a']
gives "v1"), however Paisley also lets you select multiple items at once in a single index expression. E.g. "abcde"[2,5,5] gives "bee", "abcde"[1:3] gives "abc", (6,7,8,9,0)[3,1,5]
gives (8,6,0)
.An extra note on slices: when slicing an array or a string, it's possible to replace the second number with a colon, to indicate that the slice should go from the start index all the way to the end of the string or array.
So for example, "abcdef"[4::]
would result in "def"
, (5,4,3,2,1)[2::]
would result in (4,3,2,1)
, etc.
0xFFFF
0b1111
1.2345
or 12345
or 1_000_000
. Note that underscores are ignored by the compiler, you can use them for readability purposes.true
or false
null
, equivalent to Lua's "nil""some text"
'some text'
var_name
, x
, etc.@
$
_VARS
${}
(1,2,3,4,5)
("a" => 1, "b" => 2)
random_int(min_value, max_value) -> number
random_float(min_value, max_value) -> number
random_element(array) -> any
word_diff(str1, str2) -> number
(levenshtein distance)dist(point1, point2) -> number
sin(x), cos(x), tan(x), asin(x), acos(x), atan(x), atan2(x, y), sinh(x), cosh(x), tanh(x) -> number
sqrt(x) -> number
sign(number) -> number
. Returns -1 if a number is negative, 0 if zero, or 1 if positive.bytes(number, count) -> array
frombytes(array) -> number
sum(a,b,c,...) or sum(array) -> number
mult(a,b,c,...) or mult(array) -> number
min(a,b,c,...) or min(array) -> number
max(a,b,c,...) or max(array) -> number
clamp(number, min_value, max_value) -> number
smoothstep(number, min_value, max_value) -> number
lerp(ratio, start, stop) -> number
split(value, delimiter) -> array
. Note that unlike in Lua, the delimiter is just a string, not a pattern.join(values, delimiter) -> string
count(array/string, value) -> number
find(array/string, value, n) -> number
. Returns 0 if not found.index(array/string, value) -> number
. Returns 0 if not found.type(value) -> string
. Output will be one of "null", "boolean", "number", "string", "array", or "object"bool(value) -> boolean
num(value) -> number
int(value) -> number
. This is functionally equivalent to floor(num(value))
str(value) -> string
numeric_string(value, base, pad_width) -> string
floor(value) -> number
ceil(value) -> number
round(value) -> number
abs(value) -> number
ascii(string) -> number
. Only the first character is considered, all others are ignored.char(number) -> string
. If outside of the range 0-255, an empty string is returned. Non-integers are rounded down.append(array, value) -> array
lower(text) -> string
upper(text) -> string
camel(text) -> string
replace(text, search, replace) -> string
beginswith(search, substring) -> boolean
endswith(search, substring) -> boolean
json_encode(data) -> string
json_decode(text) -> any
json_valid(text) -> boolean
b64_encode(text) -> string
b64_decode(text) -> string
lpad(string, character, to_width) -> string
rpad(string, character, to_width) -> string
hex(value) -> string
filter(text, pattern) -> string
matches(text, pattern) -> array[string]
clocktime(value) -> array
time(timestamp) -> string
date(date_array) -> string
reduce(array, operator) -> any
, e.g. reduce(1:9, +)
sums the numbers from 1 to 9, resulting in 45. This works for any boolean or arithmetic operator!reverse(array) -> array
or reverse(string) -> string
sort(array) -> array
merge(array, array) -> array
update(array, index, value) -> array
insert(array, index, value) -> array
delete(array, index) -> array
hash(string) -> string
object(array) -> object
, i.e. the array (key1, val1, key2, val2)
will result in the object (key1 => val1, key2 => val2)
array(object) -> array
, i.e. the object (key1 => val1, key2 => val2)
will result in the array (key1, val1, key2, val2)
keys(object) -> array
values(object) -> array
pairs(object/array) -> array
, i.e. the object (key1 => val1, key2 => val2)
will result in ((key1, value1), (key2, value2))
; and the array (val1, val2)
will result in ((1, val1), (2, val2))
interleave(array) -> array
, i.e. the arrays (1,2,3)
and (4,5,6)
will result in (1,4,2,5,3,6)
unique(array) -> array
union(array1, array2) -> array
intersection(array1, array2) -> array
difference(array1, array2) -> array
. Note that changing the order of the parameters can change the result!symmetric_difference(array1, array2) -> array
is_disjoint(array1, array2) -> boolean
is_subset(array1, array2) -> boolean
is_superset(array1, array2) -> boolean
flatten(array) -> array
Note that functions can be called in one of two ways:
split(var, delim)
var.split(delim)
Both are exactly equivalent, the latter syntax is included simply for convenience.
The comma ,
and slice :
operators always indicate an array.
For example, (1,2,3)
is an array, as are (1,2,3,)
, (1,)
and (,)
. Note that expressions are allowed to have a trailing comma, which simply indicates that the expression is an array with a single element. Likewise, a single comma by itself indicates an empty array.
Note that an expression must contain a comma ,
or slice :
operator to be considered an array, just parentheses is not enough.
So (1,)
is an array and (1:1)
is an equivalent array, but (1)
is a number, not an array.
Basically, if there's a comma, you have an array.
To access an array's elements, use the usual square-brackets syntax, e.g.
let array = {'a', 'b', 'c'}
print {array[1]} #prints "a"
And to change an array's elements,
let array{2} = 'd' #array is now ('a', 'd', 'c')
let array{-1} = 'e' #array is now ('a', 'd', 'e')
You can also append to an array,
let array{} = 'f' #array is now ('a', 'd', 'e', 'f')
Note that array indexes start at 1, and can be negative to start from the end of the array instead of the beginning (e.g. -1 is the last element, -2 is the second to last, etc).
Objects function very much like JavaScript objects. The keys are always strings, and the values can be anything.
To define an object, just construct a list of key-value pairs, which are any two expressions separated by an arrow, e.g. "key" => "value"
.
Note that unlike in Lua, you cannot mix array and object syntax; it's either one or the other.
Like with arrays, key-value pairs are allowed to have an optional trailing comma.
let object = {
'name' => 'Jerry',
'age' => 30,
'friend' => (
'name' => 'Susan',
'age' => 35,
),
}
In some cases, it can be useful to create an empty object. To do so, just use the arrow operator by itself.
let object = {=>}
print {'my object is "' (=>) '"'}
Object values can of course be accessed the same way array values can, with the regular indexing []
syntax. However, attributes can also be accessed with dot notation.
The following lines do the exact same thing.
print {object['name']}
print {object.name}
Like with arrays, object values can be added or changed with the following syntax,
let object{'name'} = 'Jekyll'
let object{'friend', 'name'} = 'Hyde'
However, you cannot use the append syntax on an object, as it does not make sense in that context. So the following will not work:
let object{} = 'some value'
Often you need to take an array and mutate every element in some way. While you could very well use a for loop for this, this operation comes up often enough that there is a convenient shorthand for it. See how in the following script, we're taking the array x
and multiplying every element by 2
, then assigning the result to y
.
let x = {1,2,3}
let y = {,}
for i in {x} do
let y = {append(y, x * 2)}
end
The above could be written much more succinctly as the following:
let x = {1,2,3}
let y = {i * 2 for i in x}
Those of you familiar with Python will realize where the syntax comes from, and like in Python, you can filter out array elements based on a condition. See how in the following script, x
is all numbers from 1 to 100, and we're selecting only those numbers divisible by 5
, and storing the result in y
.
let x = {1:100}
let y = {,}
for i in {x} do
if {i % 5 = 0} then
let y = {append(y, x)}
end
end
The above could instead be written as the following:
let x = {1:100}
let y = {i for i in x if i % 5 = 0}
Since commands can return values to Paisley after execution, you can also use those values in further calculations. For example:
#Get an integer value representing in-game time, and convert it to a human-readable format
let t = {floor(${time})}
let hour = {t // 3600}
let minute = {(t // 60) % 60}
let second = {t % 60}
print {hour ":" minute ":" second}
Of course, there is also a simpler version that does the same thing:
print {${time}.clocktime()[1:3].join(":")}
For ease of use and consistency, there are 6 built-in commands that will always be the same regardless of device.
time
: Returns a number representing the in-game time. Arguments are ignored.systime
: Returns a number representing the system time (seconds since midnight). Arguments are ignored.sysdate
: Returns a numeric array containing the system day, month, and year (in that order). Arguments are ignored.print
: Send all arguments to the "print" output, as well as to the internal log.error
: Send all arguments (plus line number and file, if applicable) to the "error" output, as well as to the internal warning log.sleep
: Pause script execution for the given amount of seconds. If the first argument is not a positive number, delay defaults to minimum value (0.02s).Note that all commands take a little bit of time to run (at least 0.02s), whether they're built-in or not. This is to prevent "infinite loop" errors or performance drops.
Lastly, to give an idea of the syntax, here is an example program that will create a clock that stays in sync with the client system.
#Repeat forever. "while 1 do" would also work.
while {true} do
#Format date as YYYY-MM-DD
let date = {${sysdate}.reverse().join('-')}
#Format time as HH:MM:SS
#Note the lpad() use makes sure that hours/minutes/seconds are always 2 digits
let time = {(i.lpad('0', 2) for i in ${systime}.clocktime()[1:3]).join(':')}
print {date ' @ ' time}
#Only update once per second
sleep 1
end