Skip to content

Scripting

Adam Szerszenowicz edited this page Jun 3, 2025 · 6 revisions

Scripting basics

✏️ Editing scripts

You can edit your scripts in any text editor. Pick whatever you're most comfortable with. Script changes can be tested immediately after saving, no reloading is necessary.

πŸ”€ Language syntax overview

Tip

If helper commands are enabled, you can use slcshelper syntax command to view simplified syntax rules.

πŸ’­ Expressions

Every script is a collection of expressions. Just like commands every expression can succeed or fail. Your script will parse and execute expressions in order until an expression fails (parsing errors also count as expression failure) or there's no more expressions left. There are only two types of expressions: commands and directives.

Command expressions are easy to use since they follow the same syntax rules as standard in-game console commands.

bc 5 This is an example command

In their simplest form scripts look and behave just like a collection of commands consumed by console line-by-line.

Note

Command names are case insensitive. For example bc gives the same results as bC.

bc 5 Hello World
cassie hello
forcestart

Of course certain commands require more space than others. Every line can be extended by putting a backslash \ before a new line.

bc 4 this is \
a very long \
booooiiiiiiiiiiiiiiiiiiiiiiii

If you find it not easy to read you can alternatively put your command inside a directive expression. Directives use square brackets to determine their length and are automatically extended until they're closed. Putting backslashes before new lines in directives also works but is redundant.

[bc 4 this is
a very long
booooiiiiiiiiiiiiiiiiiiiiiiii]

The only difference between these two approaches is that inside directives language keywords must be prefixed with a backslash if you want to use them as command arguments.

[bc 5 \if backslash is missing then a parsing error occurs ]

Note

Since square brackets are used to define directive expressions they always must be prefixed by a backslash when used as command arguments.

#️⃣ Comments

Comments can be added to your script for documentation purposes or for fun. They are ignored by the language. To start a comment place a # symbol after a whitespace or at the beggining of a line.

# This is a comment
bc 3 This is a command
bc 3 Example t#xt #This is not visible

If you want to include # at the start of your command arguments you need to prefix them with a backslash.

bc 3 This #creates a comment
bc 3 Th#is is visible
bc 3 This is \#also visible

Comment lines can also be extended with backslashes.

bc 3 test \
# This is a multi
# line comment

bc 3 test \
# This is also a multi \
line comment

↕️ Borrowed syntax

One of the coolest feautures of SLCS is that it can seamlessly borrow syntax extensions from other installed plugins. If your server has plugins extending command syntax in consoles then those syntax extensions are also available for use in your scripts. The following examples were created with Axwabo.CommandSystem plugin installed.

# @a is a custom selector provided by Axwabo's plugin
forceclass @a Scp173
# or
[forceclass @a Scp173]

# Square brackets must be prefixed with backslashes
forceclass @a\[alive\] Scp173
# or
[forceclass @a\[alive\] Scp173]

βš™οΈ Guards

Guards are specialized comments which control interpreter behavior for current script. They're constructed out of special symbol at the begining of a comment and a collection of setting values. Changes made by guards are applied until the end of the script or new guard of the same type is encountered. Guards without any values restore default behavior.

Note

Guard setting values are case insensitive. NOClip gives the same results as noClip.

πŸ”’ Permission guards

Permission guards are started with !. They control what permissions command sender must have to execute the following expressions. By default the script does not perform permissions validation since most of in-game commands have proper verification but plugin commands may provide varying levels of security. Permission guards allow you to properly secure certain parts of your script and prevent privilege escalation attacks.

Note

Depending on plugin configuration permission guards can use different permission systems for verification. Some of them may actually be case sensitive!

# Permission guards
# Expressions won't be executed if sender doesn't have ALL permissions listed in permission guard
bc 2 Visible to anyone #! Noclip
bc 2 Visible to Noclip users #! ServerConsoleCommands
bc 2 Visible to Command users
#! Noclip ServerConsoleCommands
bc 2 Visible to Noclip & Commands users
#!
bc 2 Visible to anyone

πŸ”Ž Scope guards

Scope guards are started with ?. They control what elements of console hierarchy should be included in command searches. By default the script uses full console hierarchy when looking up commands to use. When a command expression is encountered the interpreter checks if a command with matching name or alias exists on next console available in scope. If nothing is found the console is ignored. This step is repeated until the end of consoles hierarchy is reached (parsing error) or until an existing command is found (command is executed). Full console hierarchy is made out of 3 consoles. RemoteAdmin always takes the priority, LocalAdmin is checked right after (if it exists) and the client console is used as a last resort.

graph TD
RemoteAdmin --> Console
Console --> Client
Loading

Important

Before version 2.0.0 the Client scope was named GameConsole!

# Let's assume we have a script named `bc` located in `server` folder
# By default Broadcast command from RemoteAdmin will override all `bc` calls
bc 2 This makes broadcast
# With scope guard we can change console hierarchy scope to execute our script
# Scope guard only limits console hierarchy scope to listed consoles, lookout order is not changed
# When we're done we can return to default commands scope
#? Console
bc Run my script #?
bc 2 We're done

πŸ”’ Argument guards

introduced in version 2.0.0

Argument guards are started with $. They control how many arguments command sender must provide to execute the following expressions. By default the script does not check arguments until they're needed.

Note

Argument guards cannot process more than 1 value.

# Argument guards
# Expressions won't be executed if sender doesn't provide enough arguments
bc 2 Visible always #$ 5
bc 2 Visible when 5 or more arguments provided #$ 3
bc 2 Visible when 3 or more arguments provided
#$
bc 2 Visible always

❗ Directives

Directives allow you to construct more advanced expressions with language keywords.

Note

Keywords are case insensitive. foreach gives the same results as FOREACH.

➑️ Sequences

Sequence directive is a collection of expressions separated with |. The directive executes all inner expressions in order, until directive end is reached (success) or a failure occurs (fail). Its main purpose is to easily inject more complex expressions into other directives.

# Traditional approach with multiple expressions
bc 3 Hello world
cassie hello
forcestart

# Same functionality in a single expressions
[bc 3 Hello world | cassie hello | forcestart]

❔ Conditionals

Conditional directive works similarly to if/else constructions in other languages. The condition expression is executed first and depending on its output, other inner expression is executed. if keyword is used to define an expression executed when condition succeeds and else defines expression executed when condition fails. Condition expression failure does not cause directive failure, but fails of other inner expressions do!

Warning

Parsing errors, even in condition expression will always fail the directive.

# help command never fails so it can be used as a condition that always succeeds

# Condition is always on the right side of if keyword
[bc 3 Condition succeeded! IF help ]

# But also always on the left side of else keyword
[help ELSE bc 3 Condition failed!]

# Handling both scenarios in single expression is also possible
[bc 3 Condition succeeded! IF help ELSE bc 3 Condition failed!]

πŸ• Delays

To create a delay directive, add delayby keyword after your expression and type a number which will represent expression delay in milliseconds. Optionally you can also add a name for your expression to easily identify errors in log files. The directive starts an asynchronous operation which will wait for specified duration of time and then execute its inner expression. The directive succeeds immediately, so any errors which occured during wait time or inner expression execution are saved to server logs instead of standard output. If delay directive is named, its name will be added to prefix in error logs. If the specified time is below 1, the directive is executed as a standard directive without keywords instead.

# Works the same way as [bc 3 test]
[bc 3 test DELAYBY 0]

# Executes after 5 seconds
[bc 3 Executed after 5 seconds DELAYBY 5000]

# Named version
[bc 3 Executed after 5 seconds DELAYBY 5000 DelayExample]

πŸ” Foreach loops

Foreach loop can be used to iterate on entire iterable object. Follow your expression with foreach keyword and iterable object provider name. Inner expression can use variables to access iterated object's properties.

[forceclass $(id) Scp173 FOREACH player]

🎲 Forrandom loops

Forrandom loop is similar to foreach loop but the order of iterated elements is randomized. By default only a single element is used but that behavior can be adjusted with limit number or percentage (loop will still end when the last element is reached). Additional expression can be added with else keyword to define alternative behavior for remaining elements.

# Forceclasses a random player
[forceclass $(id) Scp173 FORRANDOM player]

# Forceclasses 10 random players to Scp173 (will affect all players if there are only <=10 players on the server)
[forceclass $(id) Scp173 FORRANDOM player 10]

# Forceclasses randomly selected 10% of currently connected players
[forceclass $(id) Scp173 FORRANDOM player 10%]

# Forceclasses a random player to Scp173 and all other players to ClassD.
[forceclass $(id) Scp173 FORRANDOM player ELSE forceclass $(id) ClassD]

# Forceclasses 10 random players to Scp173 and all other players to ClassD (no class d will be made if there are only 10 players or less).
[forceclass $(id) Scp173 FORRANDOM player 10 ELSE forceclass $(id) ClassD]

# Forceclasses randomly selected 10% of currently connected players to Scp173 and the remaining 90% to ClassD.
[forceclass $(id) Scp173 FORRANDOM player 10% ELSE forceclass $(id) ClassD]

πŸͺΊ Nested scopes

You can access variable values from higher scopes in nested loops by adding a ^ prefix to variable name for every scope level.

# To access id variable from outer loop, add ^ prefix for each scope level
[[bc 3 $(id) $(^id) $(^name) FOREACH itemid] FORRANDOM player]

[[[bc 3 $(id) $(^id) $(^^id) FORRANDOM roleid] FOREACH itemid] FORRANDOM player]

πŸ’» Scripts as commands

One of core principles of SLCS is that every script is a command. Every script can be used as a command in console or in other script. Scripts can also take arguments just like normal commands. To use an argument value in your script type $(X) where X is the index number of argument you want to use. Trying to use an argument that doesn't exist or isn't provided will result in parsing error.

bc $(1) In$(2)fix

Yes, argument indexes start from 1. The index 0 is reserved for script name which can be used for recursive script calls that won't break after script file renaming.

# This script will call itself and fail on second call due to a missing argument
bc 3 $(1)
$(0)

Note

Event handlers cannot be called from any console or script but they still receive arguments. Recursion using argument 0 is not possible.

Another funny trick with arguments is possible due to consoles using only spaces as command arguments separators when SLCS language also suppports tab separators. Yup, entire expressions can be injected with arguments when tabs are used for separation in console. To avoid making arguments too overpowered and for security reasons the following language elements cannot be injected with arguments:

  • line extensions (they're not needed anyway since the entire argument is processed as a single line)
  • comments and guards
  • script arguments (variables still can be injected)

🚫 Errors

All script errors produce a stacktrace for easier identification of a root cause. Stacktraces display names of executed scripts and line numbers where the issue occured in bottom-top order.

Note

Stacktraces might look different depending on used scripts loader implementation.

In the following example we have two scripts called call.slcs and return.slcs:

# Call a return script with 2 arguments
return $(1) $(2)
# Call original script with only 1 argument
# This call will fail
call $(1)

Executing call command will result in the following stacktrace.

Missing argument $(2), sender provided only 1 arguments
at call.slcs:2
at return.slcs:3
at call.slcs:2

Stacktraces can also show which inner expression is a problem, when a failure occurs inside a directive.

[[tf2 IF help] IF help]

Executing this script results in:

Command 'tf2' was not found
in if branch expression
at bad.slcs:1