The Basics of sdsh
Socially Distant is heavily narrative-driven. Almost all of the game's main story is told through encounters with non-player characters, as well as through missions. These in-game encounters are written in a subset of Bash called sdsh
. This is also the same language used to parse commands in the in-game command shell.
Why would I want to write sdsh
?
Consider a mission where the player must hack into a network, download a file from a device inside the network, then delete the file from the hacked device.
There are three theoretical ways this mission could be programmed into the game. It could be written in C# directly, allowing it to have full access to the game's API. It could be written in a data markup language like YAML or JSON, and thus written in a data-driven way. Or, it could be written in an embedded scripting language like Lua, or, well, sdsh
.
Missions can be highly dynamic, meaning data-driven design can be a lot more challenging to deal with. Allowing a mission to be written in C# means it has to be compiled into the game, making it harder to iterate on the game's story telling. Using an embedded scripting language means the mission can be re-loaded on the fly after a change, while also allowing the mission to do basic programmer things within reason.
Furthermore, the game already needs sdsh
because a command shell is an integral part of gameplay.
Missions, as well as other in-game encounters, are also long-lasting tasks the game needs to execute over a long period of time. This means they either need to be asynchronous or run on a background thread. The sdsh
interpreter is designed to be ran asynchronously, while the language itself hides this away from you as a writer or programmer. This makes sdsh
perfect for scripting missions.
How to write sdsh
Use the in-game terminal to play around with these code examples.
Run a command
Commands are everything! They tell the game to do things.
For example, this command opens this website in the user's browser:
man
Run a command with parameters
Many commands accept or require you to specify parameters that control their behaviour. Each parameter is separated by whitespace.
echo Hello world!
String literals
Strings of text in sdsh
are surrounded in apostrophes (single-quotes).
echo 'This is a string of text.'
If you need to use an apostrophe within a string, you can escape it with a backslash.
echo 'I\'m Ritchie.'
Text expressions
Text expressions allow you to insert variables and other expressions inside a string of text. Text expressions are surrounded by double-quotes.
USER='ritchie'
echo "Hello, $USER!"
Variables
You can define a variable like this: NAME=expression
VAR1=ritchie
VAR2=is
VAR3=here
And you can access the value of a variable like this $NAME
echo $VAR1 $VAR2 $VAR3
You can also access a variable's value like this: ${NAME}
echo ${NAME}s
Writing to a file
You can write the output of a command to a file, like this:
echo Hello world! > ~/hello.txt
Or, you can write to the end of an existing file, like this:
echo Hello world! >> ~/hello.txt
Piping
You can use the output of one command as the input of another command. This is called piping. You can use piping to make a cow say some wise words.
fortune | cowsay
Reading a file and using it as input
You can also use the contents of a file as the input of a command. You can have the contents of a file read aloud to you, like this:
speak < ~/hello.txt
Command expansion
Command expansion allows you to treat the output of a command as if it were text. You can then store this text as a variable, or pass it as a parameter to another command.
COWFORTUNE=$(fortune | cowsay)
echo $COWFORTUNE
Environment variables
You can export any variable as an environment variable using the export
keyword. Use this to customize your prompt.
export PS1='My Cool Prompt >>> '
Functions
Use functions to group a list of commands together into a single command you can use later.
function cowfortune() {
fortune | cowsay
}
cowfortune
You don't need the function
keyword, adding it is a stylistic choice.
cowfortune() { fortune | cowsay }
Function Parameters
You can pass parameters to functions just like you do other commands. Function parameters are accessed by numbered variables, $0
is the name of the function itself.
function hello() {
echo "Hello, $1!";
}
hello ritchie
Run multiple things at once
You can run two commands at once with the &
operator. This command makes the game wait 5 seconds while reading some text from a file.
(speak < ~/hello.txt) & sleep 5000
Run one command after another, on the same line
You can use a semi-colon to separate two commands on the same line. This code saves a random fortune to a file, prints the file to the screen, then reads it aloud.
fortune > ~/hello.txt; cat ~/hello.txt; speak < ~/hello.txt
Run a command only if a previous command succeeds
You can use the &&
operator to run the right-hand command if the left-hand command completed without error.
fortune > ~/hello.txt && cat ~/hello.txt && speak < ~/hello.txt
Conditional expressions
Use the if
command to run a set of commands if a given command runs successfully.
if file ~/hello.txt
then
speak < ~/hello.txt
fi
If you prefer the then
keyword on the same line as the condition, don't forget a semi-colon before then
.
if file ~/hello.txt; then
speak < ~/hello.txt
fi
You can also check if the condition command didn't succeed:
if ! file ~/hello.txt
then
echo 'File not found!'
fi
Or, run one set of commands on success and another on failure:
if file ~/hello.txt
then
speak < ~/hello.txt
else
echo 'File not found!'
fi
You can use the elif
command in a similar way to else
, except it will only run its commands if this secondary condition succeeds.
if file ~/hello.txt
then
speak < ~/hello.txt
elif file ~/world.txt
speak < ~/world.txt
else
echo 'File not found!'
fi
Loops
You can repeat a set of commands while a condition is true, using the while
command.
while true;
do
echo "Infinite loop."
done
Text matching
You can use the case
command to match a string of text against a set of patterns, running a set of commands based on what pattern matches first.
VARIABLE=ritchie
case $VARIABLE
ritchie)
echo Ritchie is here
;;
hari)
echo A wild evil skeleton appears...
;;
*)
echo Someone unknown showed up. Their name is $VARIABLE
;;
esac
When defining a pattern, preceding whitespace is ignored and a closing parenthesis marks the end of the pattern. When defining the commands to run for a given pattern, use the double-semi-colon (;;
) to mark the end of the pattern's commands and the start of a new pattern. Use esac
after the end of the last pattern to mark the end of the case
command.
An asterisk (*
) acts as a wildcard inside patterns.