Introduction
DM is a game programming language for the BYOND platform.
DM By Example (DMBE) is a collection of examples that illustrate various DM concepts. To get even more out of these examples, don't forget to install BYOND locally and check out the official ref.
If you find an error or you want to contribute, you can go check our GitHub. You can also join the coderbus discord if you need help with code or this guide itself.
Installation
⚠⚠ WIP PAGE ⚠⚠
Downloading BYOND is easy to do!
When you get to the download page it's recommended to grab the BETA version of BYOND which includes everything you need to get started.
The reason for this being that the BETA version gets updated more often and usually includes more features and fixes than the stable version. It is also typically required if you are looking to contribute to an existing project in BYOND.
Actually Installing BYOND
You can grab either the Windows or Linux version of BYOND; however, this guide assumes you are using a Windows OS.
For an easy installation simply click on the Windows button under BETA. This will download an executible file that you can run after the download is complete. The defaults are just fine unless you want to change the location that BYOND is installed to.
Once that's is all finished, you are ready to get started with the DM language!
VSCode (Very Highly Recommended)
An installation to make your life much easier when coding in DM would be VSCode. It's an open-source multi-platform code editor that includes a lot of features and QOL improvements that make it substantially better than the editor that comes with your BYOND download.
You can get VSCode from here. For this download, we suggest using the stable build as the insider edition updates frequently and can mess with your projects if you aren't experienced enough. As for the actual installation process, it's the same deal with BYOND. You can change the options if you like but if you are unsure of what you are doing the defaults will work just fine.
Once you finish installing and open up the editior be sure to login with your Microsoft account so you can access the Extensions Marketplace. There are a few goodies there that you will want to grab in order to work with the DM language and BYOND itself.
The absolute bare minimum that you'll need is the BYOND Extension Pack. You can either click the link to install the pack remotely or you can click on the icon that looks like 4 squares with one popping off on the left of your screen. This pack includes the tools needed to properly write and run DM code. It also includes some assistive tools to help you out when you write such as auto-completion and syntax highlighting.
If you want to grab even more tools to assist you there is always the Goonstation Extension Pack. Contrary to the name, you don't have to only use this pack for Goonstation. It includes some extra tools that aren't 100% needed but just provides a bit more assistance and some extra functionality that will be very useful to you later on if you decide to join the BYOND or SS13 community.
Hello World
This is the source code of the traditional Hello World program.
// This is a comment, and is ignored by the compiler
// This is a proc `Login()` defined on a base mob. We'll learn more about those later.
/mob/Login()
// Statements here are executed when a mob logs into the game
// Print text to world
world << "Hello World!"
world << "x"
is the most basic way to output text to the default window, using the << operator.
To run this, simply open up a blank BYOND project and compile it.
When you log into the game, you'll see the text in the side chat bar!
Activity
Try adding a new line with a second world << "x"
so that the output shows:
Hello World!
I'm a Developer!
Comments
Any program requires comments, and DM supports a few different varieties:
- Regular comments which are ignored by the compiler:
// Line comments which go to the end of the line.
/* Block comments which go to the closing delimiter. */
- Doc comments which are parsed for SS13 codebase documentation:
/// Generate docs for the following item.
//! Generate docs for the enclosing item.
/** Generate docs until the closing delimiter. */
/proc/test()
// This is an example of a line comment
// There are two slashes at the beginning of the line
// And nothing written inside these will be read by the compiler
// world.log << "Hello, world!"
/*
* This is another type of comment, a block comment. In general,
* line comments are the recommended comment style. But
* block comments are extremely useful for temporarily disabling
* chunks of code. /* Block comments can be /* nested, */ */
* so it takes only a few keystrokes to comment out everything
* in this test() proc.
*/
/// This is an example of a doc comment documenting the x variable.
var/x = "Hi!"
/**
* Now I'm documenting the other() proc.
*
* I can type on multiple lines.
* `I can embed markdown as well!`
*/
/proc/other()
world.log << "foo"
Basic Types
Even though variables do not need to have their types defined for primitive data, different types still exist, and variables need their type defined if you are going to assign them to an object and if you plan to use the variable to access some of the object's properties.
Primitive types
Primitive types are types that do not need variables to be typecasted anyhow. This includes:
- Numeric values:
1, 24.3, 3.14
- Strings:
"a", "abc"
- Null:
null
You can simply assign them to your variable, and access the variable's contents.
In particular, notice how lists (/list
) aren't considered a primitive type. Even though assigning a list to a var won't cause an error normally, it's bad practice to not typecast it since a list is essentially an object. You also won't be able to access list methods and properties on a non-typecasted list.
Null (null
) is considered a primitive type because it can be assigned to any var, and since null basically means no value you aren't going to access any of its properties since it has none.
Forgetting that vars can be uninitialised or deleted can cause a lot of issues. A common thing to see in procs is the following check for null value:
if (isnull(variable)
return
which prevents null values from interacting with later code and throwing a bunch of errors.
Object types
Object types are the core of any object oriented programming language like DM. These will be covered further later.
Variables
Variables are data containers which, as the name implies, can get their data changed through assignment operations. If you want to store some data, you'll use a variable. The basic syntax is:
var/myVariable = initialValue
where var
is the keyword telling the compiler we're defining a new variable.
myVariable
is the variable's name, which cannot be a reserved word like var
as we said previously.
= initialValue
is used to assign an initial value to the variable. This is optional, and if omitted, the variable's value will be null
, a special value that means the variable holds nothing.
A key difference in other languages, like C, is that primitive variables don't need the user to define their type, which means you can do
var/myVar = "Hello World"
myVar = 1
without any sort of error. Even though this will work with any sort of var type, we will see futher on that it can cause issues when our variables hold objects and we try to access its methods or such, but we'll expand on this later.
Activity
Try using a combination of a variable and a
world << "x"
statement to output the text"DMBE"
to the chat window.
Strings
Like most other languags, DM has text constants. In DM, we use double quotes "
to denote them:
var/x = "Hello World!"
To place a quote inside a string, escape it with a backslash \
character. You'll also need to escape a backslash if you want to use one on purpose.
world << "The cow says, \"Hi.\"" // The cow says, "Hi."
Backslashes are also used for special macros and other symbols that are otherwise hard to include. A backslash at the end of a line will ignore the line break and continue the string on the next line after ignoring any leading spaces:
var/str = "Multi \
Line \
String"
We also have the ability to interpolate variables within strings as such:
var/num = 5
world << "Bob has [num] cows." // Bob has 5 cows.
Instead of escaping every line, there is another format for multi-line strings ({""}
):
var/an = "an"
var/text = {"
This is how we have
multi
line
text. Also, [an] embedded string.
"}
DM also has a format for raw strings, which do not allow escape characters or embedded expressions. There are two main ways to specify a raw string, all of which begin with @
.
Simple raw strings generally follow @
with a single-character delimiter, usually "
. Line breaks are not allowed in simple raw strings.
world << @"I can say \ or [] without escaping anything!"
Complex raw strings use more complicated delimiters, but they let you include line breaks. The main way to do this starts with @{"
and ends with "}
, like the familiar multi-line format.
world << @{"
Now I have absolute freedom to use "quotes"
or [brackets] or line breaks.
"}
Lists
Lists are used to represent groups of objects. Like objects, in order to properly use their methods and vars, they must be declared of type /list
.
var/list/L // list reference
L = world.contents // assign to existing list
L = list() // make a new list
L += "futz" // L = {"futz"}
By default, lists start with a length of 0. However, we can create a list with a predetermined size via these methods:
var/tenlist[10] // empty list of size 10 (c-style)
var/fivelist = new/list(5) // empty list of size 5
We can also initalize lists with content included:
var/list/L = list("foo", "bar") // L = {"foo", "bar"}
Important: List indices range from
1
tolen
.
To access an item in a list:
var/list/L = list("foo", "bar")
world << L[1] // "foo"
To resize a list at runtime, we use the len
variable.
If the length of the list is changed, existing elements in the list will be preserved if they are less than the new length. New elements in the list will be given the initial value of null
:
var/list/L[5]
for (var/i in 1 to length(L))
L[i] = i // L = {1, 2, 3, 4, 5}
L.len = 7 // L = {1, 2, 3, 4, 5, null, null}
To get the length of a list (the first way is faster):
var/list/L = list(1, 2, 3, 4)
world << length(L) // 4
world << L.len // 4
For multi-dimensional lists, both of these produce the same list ({{}, {1, 2}}
):
var/grid[1][2]
grid[1][1] = 1
grid[1][2] = 2
var/list/same = list()
same += list(1, 2)
Associated lists
Associated lists, or list associations, add a unique functionality to regular lists by allowing you to associate values within the list with another value often referred to in other languages as a key-value pair
, map
, or dictionary
. This can be done as such:
var/list/L = list()
L["fizz"] = "buzz" // "fizz" is the key, "buzz" is the value
L["money"] = 100 // "money" is the key, 100 is the value
//L = {"fizz" = "buzz", "money" = 100}
The above list L
now contains the keys "fizz"
and "money"
which are associated with the values "buzz"
and 100
respectively.
Now the question becomes, "What does this actually do for us that a regular list can't do?" The biggest answer to that is now you can retrieve items from the list by the name of the key rather than a generic index.
world << L["fizz"] // "buzz"
world << L["money"] // 100
This is especially helpful for lists that have values which move in and out constantly since there is no guarantee that something at index [1]
will be the same value you call for at the same index later on. However, as long as you don't delete a key or its value from the list, it doesn't matter how the items are shifted as you'll get the value associated with the proper key.
As with regular lists, you can initalize an associated list with nearly the same syntax.
var/list/L = list("fizz" = "buzz", "money" = 100)
This can also be done in shorthand if your key is a text string that meets the requirements for a variable name.
var/list/L = list(fizz = "buzz", money = 100)
Notice: See how the keys don't have double quotes around them?
Associated List Loops
Looping through associated lists is fairly straightforward. You can either loop through the list items,
var/list/exlist = list(fizz = "buzz", money = 100)
var/i
for(i in exlist)
world << "[i] = [exlist[i]]"
which will print out the key while using it to grab value associated with it, or you can loop through the array indicies.
var/j
var/k
for(j = 1, j <= length(exlist), j++)
k = exlist[j]
world << "[k] = [exlist[k]]"
The second option is useful if you need to only grab certain items from the list while the first is best if you need to grab everything.
Note: Using a key that doesn't have an associated value or a key that doesn't exist will return
null
.
Determining list type
It might sometimes come up that you need to check whether the list you're working with is associative or not. Unfortunately DM does not provide a convenient and reliable way of doing this.
The two common ways that this is approached are:
- Use the DM proc
json_encode()
- if the JSON representation of the list starts with an open curly bracket{
that means it's a JSON object, so it has to be an associative list. - Iterate over the entire list and make sure all values are null. This is usually faster, but can yield a false positive - associative lists with all values set to
null
are unlikely to come up in practice, but legal.
Conversion to Associative
Conversion to an associative list is hassle-free, though serves limited purpose. Probably best avoided if you're trying to write clean code.
If you assign a value to any existing or new key using the list[key] = value
syntax the list will turn into an associative one, where all existing items will become keys without set values.
var/list/mylist = list("jim", "angela")
mylist["stacey"] = "apple"
//mylist = {"jim" = null, "angela" = null, "stacey" = "apple"}
Conversion From Associative and Numerical Keys
DM makes certain relevant operations here possible, but it's really not a good idea to use this outside of very unique scenarios. You can safely skip this section.
The way DM differentiates associative lists from non-associative ones is by checking if every key has an associated value. This is not the same as checking if the value is null
. From the point of view of the programmer there is no way to differentiate between a non-existent value and a value equal to null
.
There is, however, a way to remove a value from a list - using a numerical key during assignment.
This will assign the provided value as a key of that index, not value, while removing the existing value altogether.
var/list/mylist = list()
mylist["foo"] = "bar"
mylist["soup"] = "barszcz"
mylist[1] = "sandwich"
↓ this null is not a null, but a non-existent value
//mylist = {"sandwich" = null, "soup" = "barszcz"}
mylist[2] = "barszcz"
//mylist = ["sandwich", "barszcz"]
You can use this method to supply numerical keys, however they will be interpreted as empty string keys until the list stops being associative. You can't actually access any item by those numerical keys, since accessing by number refers to indexes and not keys, so their use is limited.
var/list/mylist = list()
mylist["foo"] = "bar"
mylist["test"] = "test"
mylist[1] = 5
//mylist = {"":null,"test":"test"}
mylist[2] = 6
//mylist = [5, 6]
Casting
DM provides implicit type conversion (coercion) between primitive types.
var/x = "string"
world << x // "string"
x = 5
world << x // 5
Say you have an /obj/honk
object, you're fully able to assign it to a type-less var as we did before with our string and number; however, you won't be able to actually access any of the object's data without typecasting it first.
The syntax to cast a variable varname
to the type your/type
is as such:
var/your/type/varname = initialValue
Constants
⚠⚠ WIP PAGE ⚠⚠
Constant is a modifier for vars that define a constant value. This is done by using the const
keyword.
var/const/max_cash = 1000
They are useful for vars that don't need to be changed and are used multiple times. It also helps reduce the amount of "magic numbers', (e.g. numbers that you don't know what they do/what they are for by looking at them).
For example, you may want something to have a max speed. Instead of writing code to check different objects against an arbitrary number like 100
, you can declare a const var like var/const/max_speed = 100
that you can use in place of a number. This also provides the ease of only having to change one var instead of potentially hundreds if you decide to change your mind later on.
TODO: explain how different from defines, scoping
Operators
The operators available in DM are very similar to other C-like languages. Their precedence is also similar.
Here's the base operators:
Addition = "+"
Subtraction = "-"
Multiplication = "*"
Division = "/"
Powower = "**"
Modulo = "%"
Conditional operators:
Equal = "=="
Not Equal = "!="
And = "&&"
Or = "||"
Less Than = "<"
Greater Than = ">"
Less Than or Equal = "<="
Greater Than or Equal = ">="
Unary operators:
Not = "!"
Binary Not = "~"
Negate = "-"
Increment = "++"
Decrement = "--"
Assignment operators:
Assign = "="
Addition Assign = "+="
Subtract Assign = "-="
Multiply Assign = "*="
Divide Assign = "/="
Modulo Assign = "%="
Assign Into = ":=" // walrus operator
And Assign = "&&="
Or Assign = "||="
Binary operators:
Binary And = "&"
Binary Or = "|"
Binary Xor = "^"
Left Shift = "<<"
Right Shift = ">>"
Binary assignment operators:
Binary And Assign = "&="
Binary Xor Assign = "^="
Binary Or Assign = "|="
Left Shift Assign = "<<="
Right Shift Assign = ">>="
Equivalence operators:
Equivalent = "~="
Not Equivalent = "~!"
Special BYOND operators
In = "in" // Used for ranges ex. `for(var/x in 1 to 5)`
To = "to" // Only appears in the RHS of `In`, above
Step = "step" // Only in for loops, ex. `for(var/x in 10 to 1 step -1)`
There is also the C-style ternary operator expression: condition ? if_true : if_false
.
Activity
Go over the following snippet. What will the value of N be at each line?
var/N
N = 0
N += 1+1*2
if(N - 1 == 2) N = 2
if(N==2 && 1/2==0.5) N = 0.5
Bitflags
Bitflags, or bit fields, are handy ways to compactly store data in a variable.
Since DM only supports floating point numbers, we are restricted to 24 bits (23 explicitly stored) per variable.
#define DISABILITY_EYE (1<<1) // 1
#define DISABILITY_ARM (1<<2) // 2
#define DISABILITY_LEG (1<<3) // 4
/mob/var/disabilities = 0
/mob/proc/add_disability(dis)
disabilities |= dis
/mob/proc/check_disability(dis)
if( disabilities & dis )
return TRUE
else
return FALSE
/mob/proc/remove_disability(dis)
disabilities &= ~dis
For a closer look at the binary math behind this, check out tgstation's article.
Activity
Using the above code snippet as a base, try implementing the ability to toggle a flag easily.
Hint: What other binary operators are there?
Operator Overloading
## ⚠⚠ WIP PAGE ⚠⚠
Operator overloading is a special way to define a different process for operators to use when working with datums/objects. It's similar to object overriding which you'll learn about later. This is yet another way to save you some time and writing if you have to perform an operation multiple times.
An example would be if you have two objs that you wanted to have the vars within added up. Without operator overloading it would look something like this:
/obj/foo
var/a = 1
var/b = 2
proc/showvars()
return "a = [a], b = [b]"
/obj/bar
var/a = 4
var/b = 5
/proc/main()
var/obj/foo/newfoo = new
var/obj/bar/newbar = new
newfoo.a += newbar.a
newfoo.b += newbar.b
world << newfoo.showvars()
Operator overloading would allow you to define how the +=
should act differently when we want to add our vars together since normally it wouldn't do what we want it to. This is done through defining a proc inside of a datum/obj using the keyword operator
almost always immediately followed by the operator we want to override with new functionality.
/obj/foo
var/a = 1
var/b = 2
proc/showvars()
return "a = [a], b = [b]"
proc/operator+=(obj/temp)
a += temp.a
b += temp.b
/obj/bar
var/a = 4
var/b = 5
/proc/main()
var/obj/foo/newfoo = new
var/obj/bar/newbar = new
newfoo += newbar
world << newfoo.showvars()
While this didn't save us much work for this small example, with more complex datums/objs this can help eliminate a lot of headache as you can take care of a lot of different operations with a single overload.
Statements
A DM program is (mostly) made up of a series of statements:
/mob/Login()
// statement
// statement
There are a few kinds of statements in DM. Statements can stand alone, but an expression cannot.
An expression:
x + 5
A statement:
x += 5
Statements can be a part of other statements, and expressions must be part of a statement.
// variable binding
var/x = 5
// this line is a statement, and there is a (expression) in it
x = (x + 5)
var/lessThan3 = x < 3
world.log << lessThan3
Activity
Try making a statement of your own from a total of three expressions. It should be true (1) if a variable y
is greater than 3 but less than 10.
Hint: Think of nested expressions.
Control Flow
An essential part of any programming languages are ways to modify control flow: if/else
, for
, and others. Let's talk about them in DM.
Note: While not strictly required, you can use {curly braces} instead of/in addition with indentation to indicate scope.
if/else
Branching with if-else
is similar to other languages. The boolean condition needs to be surrounded by parentheses and each condition is followed by a block. if-else
conditionals are expressions.
/mob/Login()
var/n = 5
if (n < 0)
world << "[n] is negative"
else if (n > 0)
world << "[n] is positive"
else
world << "[n] is zero"
You can also construct if statements on a single line, as such:
if (n == 7) world.log << "Special number!"
Activity
Using the code in the first block, try adding a condition to check if the number is divisible by two.
Loops
Loops are the fundamental way to do repeat actions in a programming language.
One thing in common across all types of loops in DM are the break
and continue
statements.
The break
statement can be used to exit a loop at anytime, whereas the continue
statement can be used to skip the rest of the iteration and start a new one from the beginning of the loop.
/mob/Login()
var/count = 0
world << "Let's count until infinity!"
// Infinite loop
while (TRUE)
count += 1
if (count == 3)
world << "three"
// Skip the rest of this iteration
continue
world << "#[count]"
if (count == 5)
world << "OK, that's enough"
// Exit this loop
break
while
The while
keyword can be used to run a loop while a condition is true.
Let's write the infamous FizzBuzz problem using a while loop.
/mob/Login()
// A counter variable
var/n = 1
// Loop while `n` is less than 101
while (n < 101)
if (n % 15 == 0)
world << "fizzbuzz"
else if (n % 3 == 0)
world << "fizz"
else if (n % 5 == 0)
world << "buzz"
else
world << "[n]"
// Increment counter
n += 1
for
For loops are the other core type of loop in DM.
There's two main syntaxes for iteration, one traditional and one cleaner:
for (var/x = 0; x < 10; x++)
...
for (var/x in 0 to 10)
...
You can use also variables instead of constant iteration numbers:
var/number = rand(5,10) // gives a random number from 5-10
for (var/i in 1 to number)
...
However, keep in mind that for the for-in-to
syntax you cannot modify the iterator within the loop as you can with a traditional-style one:
for (var/x = 0; x < 10; x++)
x++ // valid
for (var/y in 0 to 10)
y++ // invalid
Let's rewrite our FizzBuzz example from while as a for loop this time!
/mob/Login()
// `n` will take the values: 1, 2, ..., 100 in each iteration
for (var/n in 1 to 101) {
if (n % 15 == 0)
world << "fizzbuzz"
else if (n % 3 == 0)
world << "fizz"
else if (n % 5 == 0)
world << "buzz"
else
world << "[n]"
Nesting
In addition to having stand-alone loops, you can also nest them as such (iterator variables cannot be the same):
for (var/x in 1 to 10)
for (var/y in 1 to 10)
...
This forms the backbone of many things, such as working with multi-dimensional lists.
var/list/my_list[3][3]
for (var/i in 1 to length(my_list))
for(var/j in 1 to length(my_list))
my_list[i][j] = "[i],[j]"
world << json_encode(my_list)
Activity
Try fixing the code above so it works with non-square multi-dimensional lists.
Procedures
Procedures, or procs, are declared using the proc
keyword. These are also known by other names in other languages, such as functions or methods. Its arguments come after in parentheses. DM does not have a return type annotation like other languages.
If you are not declaring (therefore, you would be overriding) a new proc, you omit the proc
keyword. See: /mob/Login()
below.
The return
statement can be used to return a value from within the proc, even from inside loops or if statements.
Let's rewrite FizzBuzz using a proc!
// Unlike C/C++, there's no restriction on the order of function definitions
/mob/Login()
fizzbuzz_to(100)
// Returns a boolean value
/proc/is_divisible_by(lhs, rhs)
// Edge case, early return
if (rhs == 0)
return FALSE
return (lhs % rhs == 0)
/proc/fizzbuzz(n)
if (is_divisible_by(n, 15))
world << "fizzbuzz"
else if (is_divisible_by(n, 3))
world << "fizz"
else if (is_divisible_by(n, 5))
world << "buzz"
else
world << "[n]"
/proc/fizzbuzz_to(n)
for (var/p in 1 to n+1)
fizzbuzz(p)
Arguments
⚠⚠ WIP PAGE ⚠⚠
The parameters that are found within the paraentheses of a proc are known as arguments
. This allows you to make more diverse procs and helps to prevent rewriting similar code as different values can be used in the same proc with arguments.
For instance, let's bring over part of that FizzBuzz proc that was written on the last page.
/proc/is_divisible_by(lhs, rhs)
if (rhs == 0)
return FALSE
return (lhs % rhs == 0)
As you can see, you can call this proc with two arguments. This can be with any set of two numbers which the proc will use in place of lhs
and rhs
. For example, if you called is_divisible_by(10, 5)
then lhs
will be replaced with the value 10
and rhs
with 5
. The proc can be reused later on with a different set of arguments so you don't have to manually check/code divisions with other numbers.
Default Arguments
You can also set a default value for your arguments as well. This means that when that proc is called the caller doesn't have to specify a value for any arguments with a default. Let's take the proc from earlier and add a default value to one of the parameters.
/proc/is_divisible_by(lhs, rhs = 5)
if (rhs == 0)
return FALSE
return (lhs % rhs == 0)
Now when we call this proc we can simply do is_divisible_by(10)
to get the same answer as before as 5
will be used in the place of rhs
due to being a default parameter.
NOTE: We can override the default value of an argument simply by defining a value for that parameter. If we wrote
is_divisible_by(10, 7)
then the default5
will be overwrote by our7
.
Argument Specification
There is also the notion of specifing the type of variable that you want to use for your arguments. This helps to keep your statements short and to make sure that the right type of variable is being used within the proc itself.
proc/set_card_desc(obj/car/C, desc = "Red hatchback")
C.desc = desc
world << "Your [C] looks like a [desc]."
Specifing works like declaring a var but
var/
is implied and not needed.
TODO: ...
Named Arguments
⚠⚠ WIP PAGE ⚠⚠
Another way to pass arguments to a func is to name them when calling. Normally when you call a func, the arguments used will be placed in the func's arguments in the order they are set when you call that func.
proc/SomeProc(a, b, c)
world << "[a] is first, [b] is second, [c] is third."
proc/Main()
SomeProc(1, 2, 3) // 1 goes to a since it's first in the list and so on...
However, you can specify the order using named arugments.
proc/Main()
SomeProc(c = 3, b = 2, a = 1) // will produce the same result as above...
This is mainly useful for ensuring that the right variables go to the right arguments since order doesn't matter when using names.
Note: Named arguments that don't match any of the arguments in the proc you call will produce a
runtime error
. You'll learn more about these later on.
TODO: Stuff...
Return Values
Every proc has a return value associated with it. This defaults to a value of null
.
To call a proc and store its return value in a variable, you can do this:
var/value = foo()
To return specific values from a proc, you use the return
keyword along with an optional argument value, (defaulting to null
without one).
/proc/foo(arg)
switch (arg)
if (1)
return "good!"
else
return "bad!"
There also exists a special variable for each proc called the 'dot variable', accessed via the .
symbol.
What’s special about the dot variable is that it's automatically return
-ed at the end of a proc, provided that the proc does not already manually return, (e.g. return x
).
To re-code the above more tersely:
/proc/foo(arg)
. = "bad!"
if (arg == 1)
return "good!"
With .
being present in every proc, we use it as a temporary variable. However, the .
variable cannot replace a typecasted variable - it can hold data as any other var in DM can, but it just can’t be accessed as one. Although, the .
variable is compatible with a few operators that look weird but work perfectly fine, such as: .++
for incrementing .
's value.
Objects
⚠⚠ WIP PAGE ⚠⚠
To check if an object is of a certain type, use istype(x, /type)
which returns a TRUE
or FALSE
value:
/obj/foo
var/x = 1
/proc/foobar(obj/passed)
if (istype(passed, /obj/foo))
var/obj/bar = passed
world << bar.x
You can also cast, (done here in the argument), then implicitly typecheck:
...
/proc/foobar(obj/foo/bar)
if (istype(bar))
world << bar.x
Disposal
Once an object is created in DM, you'll want to eventually get rid of it to free up resources. There's two ways about this:
- Explicit deletion (
Del
) - Garbage collection
The second is much preferable, as how the explicit deletion mechanic works is that it has to scan the entire game for references to that object to clear which is quite resource-intensive. SS13 does not use explicit deletion for this reason. However, you would do it as such:
var/obj/foo = new
Del(foo)
A primer on DM garbage collection:
The garbage collector works by using a reference counting system. Once an object is no longer referenced, it gets deleted.
Important to note: Circular references will never be deleted by the garbage collector. This is defined as a pair of objects with variables that point to each other, or even an object with a variable that points to itself. Also, an object with running or sleeping procs will not be deleted.
So, let's do a GC-ing version of the above snippet:
var/obj/foo = new
foo = null
Since we've gotten rid of the only reference to the object we created, it'll get garbage collected on the next pass.
A note for SS13:
SS13 uses a pair of procs named
qdel()
anddisposing()
for ease of reference removal.
To delete an object, you will call qdel()
on it. To implement custom reference removal for an object, you would override disposing()
. For example:
/obj/baz
var/datum/my_new_ref
/obj/baz/New()
. = ..()
my_new_ref = new
/obj/baz/disposing()
. = ..()
my_new_ref = null
/mob/Login()
..()
var/obj/baz/bork = new
sleep(100) // do whatever with the object
qdel(bork)
By doing the above, the datum reference created by the object will be automatically removed when we queue the object for deletion.
Inheritance
A key part of DM, being an Object-Oriented language, is the idea of inheritance.
Let's say you have an type with the path /datum/foo
. If you then define a subtype /datum/foo/bar
, it will inherit properties from the first.
/datum/foo
var/foo_var = 5
/proc/main()
var/datum/foo/bar/my_subtype = new
world << my_subtype.foo_var // 5
You can also override properties of the parent:
/datum/foo
var/foo_var = 5
/datum/foo/bar
foo_var = 10
/proc/main()
var/datum/foo/bar/my_subtype = new
world << my_subtype.foo_var // 10
You can do this with procs as well:
/datum/foo/proc/xyzzy()
world << "parent"
/datum/foo/bar/xyzzy()
world << "child"
/proc/main()
var/datum/foo/bar/my_subtype = new
my_subtype.xyzzy() // "child"
Sometimes, you'll want to have some custom functionality, in addition to, the parent's functionality. This is expressed in other languages sometimes as super()
. Instead, we use ..()
in DM. This can be called at any point in the proc.
/datum/foo/proc/dream()
return "yond"
/datum/foo/bar/dream()
world << "Be" + ..()
/proc/main()
var/datum/foo/bar/my_subtype = new
my_subtype.dream() // "Beyond"
Primitive Types
In Object Oriented programming, objects inherit behaviour from parents. DM has some built in ancestors which share certain behaviours. These will be gone over individually and in more detail in their own pages, but here is the general overview.
Datums
The first is the Datum (/datum/
). It's the ancestor of all the other types (except for some types like /world, /client, /list, and more) that we will see. It's essentially pure data, hence the name, and can have pretty much any vars you like.
Note: When you define a new "top level" object, if you do not specify a parent_type, it defaults to /datum.
MyType
var/myvar = "test"
// this "mytype object" is a datum. Normally, though, you'd put it as datum/MyType or /datum/MyType
Atoms
Atoms (/atom/
) are the direct child of datums, and they represent objects that can go on a map and therefore in the world. It gives important information to its children, mainly areas, turfs, objs, and mobs.
Similarly, /atom/movable
defines behaviour related to moving things, and is the parent of /obj
and /mob
.
/atom/movable/car
//don't do this
Areas
An area (/area/
) is how the game controls rooms and certain zones. Every tile on a map should have exactly one area.
/area/Entered(O)
.=..() // makes sure that the parent stuff is called when the function returns, for instance, from /atom.
if (desc)
O << desc // makes it so that entering any area will return the description, if there is one.
/area/outside
desc = "What a lovely day."
/area/inside
desc = "Wonder what the weather's like."
Turfs
A turf (/turf/
) is essentially the tile itself. They can't be moved, only replaced with new ones (which removes the old one). Depending on what game or what server you're running, these are generally used as floors, walls and windows (although, of course, it varies).
Note: the return value of .loc on an atom is usually the turf that the atom is on. Unless its location is null, of course.
/turf/wall
desc = "A steel wall."
density = 1 // it can't be walked through.
/turf/floor
desc = "A steel tiled floor"
/turf/floor/specialfloor
desc = "Something seems strange about this floor."
Entered() // when the tile is walked over, do a special thing
doSpecialThing()
proc/doSpecialThing()
// the thing that the tile would do when walked on would go here.
Mobs
Living things that can move around, deriving from the word "mobile". These are what the player controls, and are slightly more complicated than objs since they can have a /client attached to them (i.e. a player).
/mob/human
desc = "A regular old human."
Objs
Objs (NOT the same as objects) are general purpose items and things that you can find in a map. Everything that's not a turf or a mob on a map is almost certainly an obj.
Objects vs Objs: A programming object is a type of thing that can hold multiple kinds of data (i.e. a datum). An obj on the other hand is a "physical" object that you'd find within the world. Not the same concept.
/obj/egg
desc = "A chicken egg."
Preprocessor
⚠⚠ WIP PAGE ⚠⚠
#define / #undef
Sometimes, you'll want to have shared values across your project instead of copy-pasting them everywhere. This is where #define
comes in.
The #define Name Value
statement substitues the Name
for Value
as long for as the define is not #undef
-ined. Substitution only applies to whole words. Text inside of double or single quotes is not processed for substitution, so "This is BIG."
would not be modified even if a define named BIG
were to be defined. That's different from "This is [BIG]."
, whereas BIG
is an embedded expression, which does get processed for macro substitution.
Example:
#define DAY 0
#define NIGHT 1
var/daytime = NIGHT // daytime = 1
Macros
There also exists a subset of defines called Macros. These are in the format: #define Name(Parameters) Value
. These are basically like procs, but without the ability to have variables stored within. These are great for common operations, such as checking conditions.
/// Returns true if given is a client
#define isclient(x) istype(x, /client)
/proc/hello(client/bar)
if (isclient(bar))
world << "Hello there!"
#if / #elif / #else / #ifdef / #endif
These conditional defines work very similarly to the regular if()
and else
logic that we saw earlier, except that you have to close the code block with #endif
. The code within the true statements gets compiled, and untrue statements have their code blocks simply not included in the code. Like defines, they get sort of 'swapped out' before the code is run, so can't be used with variables, but only with other defines.
#elif
is equivalent to else if
, and #if
and #else
are self explanatory.
#ifdef DEFINE
is a special case which substitutes in its code block when the define in the argument is defined, and is false otherwise. It's almost functionally identical to #if defined(DEFINE)
, but comes with a caveat: there is no #elsedef
for you to connect it to, they have to be closed off with #endif
. #ifndef DEFINE
meanwhile, is similar to #if !defined(DEFINE)
, with the same.
// some random defines that get determined before the code compiles.
#define LOGIC_A 1
#define LOGIC_B 2
#define LOGIC_C 3
#ifdef LOGIC_C // LOGIC_C is defined, so this code will run.
world << "Logic C is in place"
#endif
#ifdef LOGIC_D // this code will not run, since LOGIC_D isn't defined
world << "Logic D is in place"
#endif
#if LOGIC_A > LOGIC_B
world << "Logic A is larger than B" // this part isn't compiled
#elif LOGIC_A < LOGIC
world << "Logic A is smaller than B" // this part is compiled
#endif
You can use these to help define large groups of defines at once.
// in this example, you define the one that you want by uncommenting it in the code, before the code compiles. This is a fairly common usage of so called 'build' defines.
//#define STARTUP_SETTING_A
#define STARTUP_SETTING_B // we're defining setting b
//#define STARTUP_SETTING_C
#ifdef STARTUP_SETTING_A
#define FULL_BOOT
#define TRACKING_SETTINGS
#define OTHER_NONSENSE
#elif defined(STARTUP_SETTING_B)
#define FULL_BOOT
#define OTHER_NONSENSE
#elif defined(STARTUP_SETTING_C)
#define FULL_BOOT
#endif
// the defines FULL_BOOT and etc would then be used by later parts of code, with further conditionals.
#include
TODO
#error / #warn
Both #error
and #warn
automatically display messages when compiling if they are reached. Like normal, errors prevent projects from compiling whereas warnings do not. Example usage:
#if DM_VERSION < 513
#error This compiler is too far out of date!
#endif
...
#ifdef USE_LIGHTING
#warn The lighting feature is experimental.
#endif
Advanced Usage
Advanced Macros
The last parameter of a macro can end in ...
which means that it, and all other arguments following it, count as a single argument. This is called a variadic macro because it lets you use a variable number of arguments. The last parameter will also become optional.
#define LAZY_LIST(n, items...) if(!n) n = list(items)
In a macro's body, if you precede a parameter by #
, the replacement value will be turned into a string. For instance, 2
would become "2"
.
#define DEBUG_VAR(v) world.log << #v + " = [v]"
DEBUG_VAR(x) // world.log << "x" + " = [x]"
A parameter preceded by ##
in the macro body is substituted directly, without any spaces. If you use this with the last argument in a variadic macro, any preceding spaces, and a comma (if found), will be removed if the replacement is empty.
#define MACROVAR(k) var/macro_state_##k
MACROVAR(right) // becomes `var/macro_state_right`
Using ###
in the macro body, preceded by a number, will repeat the replacement a certain number of times.
#define SAYTWICE(t) 2###t
#define TOTEXT(t) #t
world << "[TOTEXT(SAYTWICE(hi))]" // world << "hihi"
Meta
⚠⚠ WIP PAGE ⚠⚠
The SS13 community has built up a lot of toolings around the base DM language.
Language Server
⚠⚠ WIP PAGE ⚠⚠
A language server (protocol) is something that is used to help a tool/client and a server, that provides the actual "smartness", so that the client can use features like auto complete and the like. In even more layman terms, it's something that gives you the nice features that makes your coding life easier.
Unless you skipped the large recommendation on the second page, you should already have the language server for the DM language. This is what gave you, and VSCode, the ability to have searchable definitions and auto complete.
Extra Readings
VSCode DM Language Client Extension
DMDoc
Used by many SS13 codebases, DMDoc automatically generates code documentation from comments.
Types, macros, vars, and procs can be documented using any of the four different doc comment styles which both block and line comments are supported. Documentation blocks may target their enclosing item or the following item.
/// This comment applies to the following macro.
#define BLUE rgb(0, 255, 0)
/** Block comments work too. */
/obj/foo
var/affinity = BLUE //! Enclosing comments follow their item.
/proc/chemical_reaction()
/*! Block comments work too. */
Crosslinks
You can also link inside any doc comment or markdown file to another documented piece of code.
Valid forms of crosslinks:
[DEFINE_NAME]
[/path_to_object]
[/path_to_object/proc/foo]
[/path_to_object/var/bar]
You can customize the link text that appears. This is done by prepending the custom link text in brackets, such as: [some define][DEFINE_NAME]
.
Titles
The title of a documentation entry is determined by whichever is set first:
- A # Title set at the top of a doc block, if present.
- The type's name var if present and not disabled in config.
- The last component of the typepath.
Example:
/**
* # Fubar
*/
/obj/foo
Source code for DMDoc can be found here.
StrongDMM
A useful alternative to BYOND's built-in map editor, StrongDMM is a downloadable map editor for Windows/MacOS/Linux built to assist you with map creation and editing.
If you want to get started using it, you may consult a quick-start Guide to Mapping. It's been tooled to explain several concepts to absolute begineers, but there is still always more to learn. Although some of the advice was built on one specific flavor of Space Station 13 Code (/tg/), there should be enough to guide you through using it in the specified sections.
If you wish to skip the guide and want to play it out on your own, simply go to the page and scroll down until you find the download link pertaining to your Operating System. Run it and point it to your project's .DME (Environment File) for the tool to understand your project's parameters and generate the content needed to map. Then, you can create edit map files (.DMM) to your heart's content.