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 procedure/function named `main()` defined globally.
// We'll learn more about these later.
/proc/main()
// Print text to the console
world.log << "Hello World!"
world.log << "xyz" is the most basic way to output text to the console, using the << operator.
Activity
Try adding a new line with a second world.log << 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
1tolen.
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
nullare 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
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.
const vs #define
const and #define both help you avoid magic numbers, but they are not the same thing.
A const is a normal DM variable declaration whose value is meant to stay fixed. Because it is still a real variable, it follows the usual scoping rules.
A #define is a preprocessor directive. Before the compiler parses your code, the preprocessor replaces the defined name with the text you gave it. It is not a variable, does not belong to an object, and does not obey normal variable scope.
Scope
Because const is still a variable, you can declare it in different places depending on how widely it should be used.
- A global
constcan be read anywhere. - An object
constbelongs to that type. - A proc-local
constonly exists inside that proc.
#define SALES_TAX 0.07
var/const/MAX_CARRY_WEIGHT = 50
/obj/stack
var/const/stack_limit = 20
/proc/main()
var/const/local_discount = 0.10
var/price = 100
world.log << "From #define: [price * SALES_TAX]"
world.log << "Local const: [price * local_discount]"
world.log << "Global const: [MAX_CARRY_WEIGHT]"
world.log << "Object const: [/obj/stack::stack_limit]"
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
Operator overloading lets a datum define what an operator should do when that datum is used on the left-hand side of an operation.
This is useful when your type has a natural meaning for operations such as +, +=, ==, or [].
The overload is written as a proc named operator immediately followed by the operator itself.
For example:
proc/operator+()overloads+proc/operator+=()overloads+=proc/operator==()overloads==
Without operator overloading, combining two custom values means manually touching each field yourself:
/datum/stats
var/strength = 0
var/agility = 0
proc/as_text()
return "STR [strength], AGI [agility]"
/proc/main()
var/datum/stats/base_stats = new
var/datum/stats/boots_bonus = new
base_stats.strength = 3
base_stats.agility = 4
boots_bonus.strength = 1
boots_bonus.agility = 2
base_stats.strength += boots_bonus.strength
base_stats.agility += boots_bonus.agility
world.log << base_stats.as_text()
That works, but it gets repetitive quickly.
With overloads, we can teach the datum what + and += mean instead:
/datum/stats
var/strength = 0
var/agility = 0
proc/as_text()
return "STR [strength], AGI [agility]"
proc/operator+(datum/stats/other)
var/datum/stats/result = new
result.strength = strength + other.strength
result.agility = agility + other.agility
return result
proc/operator+=(datum/stats/other)
strength += other.strength
agility += other.agility
/proc/main()
var/datum/stats/base_stats = new
var/datum/stats/ring_bonus = new
base_stats.strength = 3
base_stats.agility = 4
ring_bonus.strength = 1
ring_bonus.agility = 2
var/datum/stats/combined = base_stats + ring_bonus
world.log << "base + ring = [combined.as_text()]"
base_stats += ring_bonus
world.log << "base stats are now [base_stats.as_text()]"
As a rule of thumb, overload operators only when the meaning is obvious.
NOTE: If you overload
+but not+=, DM can still fall back toa = a + bfor datums. Writing a dedicatedoperator+=()is clearer.
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.
Advanced Loops
Basic while and numeric for loops cover a lot of ground, but DM also has some very useful loop forms for working with lists and other collections.
Looping Over Lists
If you already have a list, you can loop over its contents directly:
/proc/main()
var/list/items = list("crowbar", "wrench", "cable")
for (var/item in items)
world.log << item
This is usually cleaner than manually working with indexes.
NOTE: When you loop through a list, DM loops over a copy of that list. If the original list changes during the loop, the current loop will not suddenly start using the new contents.
Typed Loop Variables
Loop variables can have types too.
That means DM will filter out values that do not match the type you asked for:
/datum/coin
/proc/main()
var/list/mixed = list(
new /datum/coin,
"hello",
5,
new /datum/coin
)
for (var/datum/coin/coin in mixed)
world.log << "Ooh! A coin!"
Even though mixed contains text and a number, only the /datum/coin values are handled inside the loop.
Looping Over Contents
You can also loop over objects exist in the world and contain things:
for (var/obj/O in turf.contents)
world << "There's a [O] here."
This is a common ways to find mobs, objs, turfs, and other map objects.
Key/Value Loops
Associative lists can be looped through as key/value pairs:
/proc/main()
var/list/prices = list(
"apple" = 2,
"banana" = 3,
"pear" = 5
)
for (var/name, price in prices)
world.log << "[name] costs [price]"
This is nicer than writing prices[name] each time when you need both the key and the value.
Labeled break and continue
When loops get nested, sometimes you want to break out of an outer loop rather than just the innermost one.
DM supports labels for that:
finding_key:
for (var/mob/M in world)
for (var/obj/O in M.contents)
if (O.name == "gold key")
world << "Found the key on [M]."
break finding_key
Likewise, continue can jump to the next iteration of a labeled loop:
finding_enemies:
for (var/mob/M in world)
for (var/mob/G in world)
if (M in G.friends)
continue finding_enemies
world.log << "[M] has no enemies"
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
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 default5will be overwrote by our7.
Argument Specification
You can also specify 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_make(obj/car/C, make = "Red sedan")
Specifying arguments works like declaring a local var, except
var/is implied and does not need to be written.
This means that obj/car/C should be read as “a variable named C that should be treated as an /obj/car”.
That gives you two main benefits:
- It documents what the proc expects.
- It lets the compiler treat
Cas a car inside the proc, so accessing car vars and procs is much more natural.
If C were left untyped, you could still pass a car in, but the compiler would only know that C is a generic variable. With obj/car/C, it knows the proc is written for cars specifically.
/obj/car
var/make = "Plain car"
/proc/paint_car(obj/car/C, make = "Red sedan")
C.make = make
world.log << "Painted car: [C.make]"
/proc/main()
var/obj/car/my_car = new
paint_car(my_car)
paint_car(my_car, "Blue sedan")
As a rule of thumb, use the most specific argument type that matches what your proc wants.
Named Arguments
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.
Examples
Named arguments are most useful when a proc has several optional arguments with defaults. Instead of counting commas until you reach the setting you want to change, you can name only the one you care about.
/proc/show_popup(txt, c = "blue", b = FALSE, w = 2, t = 3)
world.log << "[txt] | c=[c] | b=[b] | w=[w] | t=[t]"
/proc/main()
show_popup("Testy")
show_popup("Oh No", c = "teal", b = TRUE)
show_popup("Quick", t = 10)
show_popup("Mixed", "blue", t = 5)
That last call mixes both styles: "Mixed" and "blue" are positional, while t = 5 is named.
This makes longer calls easier to read, easier to change later, and much harder to mix up.
Mixing Positional and Named Arguments
You do not have to use names for every argument in a call. DM lets you mix positional arguments with named ones.
proc/Main()
SomeProc(1, b = 2, c = 3)
Here, 1 is still passed into a because it is first in the argument list, while b and c are matched by name.
However, this can be confusing.
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
Most values you have seen so far have been primitive values such as numbers, text, or null.
Objects are different: they can have their own vars and procs attached to them.
In DM, most user-defined objects ultimately descend from /datum, and more specific kinds of objects are described with type paths.
Type Paths
Type paths are how DM names object types.
/obj
/obj/weapon
/obj/weapon/sword
Each extra step in the path makes the type more specific. A /obj/weapon/sword is a kind of /obj/weapon, which is itself a kind of /obj. Just like a file system.
Creating Objects
Use new to create an object.
var/obj/weapon/W = new /obj/weapon
If the variable already has a type, DM can infer the type for new:
var/obj/weapon/W = new
Typed Variables
Typing an object variable is important because it tells the compiler what vars and procs you expect that object to have.
/obj/weapon
var/title = "Weapon"
var/damage = 0
/proc/main()
var/obj/weapon/W = new
W.title = "Sword"
W.damage = 15
world.log << "[W.title] deals [W.damage] damage."
If W were not typed as obj/weapon, the compiler would not know whether title and damage were valid to access.
Checking Types
Sometimes you are given a more general object and need to see whether it is really the kind you want.
For that, use istype():
/obj/weapon
var/damage = 10
/proc/show_damage(obj/thing)
if (istype(thing, /obj/weapon))
var/obj/weapon/W = thing
world.log << W.damage
Casting
If you want to convert a general object reference into a more specific typed one, astype() can help:
/proc/show_damage(obj/thing)
var/obj/weapon/W = astype(thing, /obj/weapon)
world.log << W?.damage
If thing is not actually a /obj/weapon, then W becomes null.
Putting It Together
/obj/weapon
var/title = "Weapon"
var/damage = 0
proc/describe()
return "[title] deals [damage] damage."
/obj/weapon/sword
title = "Longsword"
damage = 15
/proc/print_weapon(obj/thing)
var/obj/weapon/W = astype(thing, /obj/weapon)
world.log << W?.describe()
/proc/main()
var/obj/weapon/sword/my_sword = new
print_weapon(my_sword)
Objects become much more powerful once you start inheriting from existing types and overriding their behaviour, which is what the next few pages cover.
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
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
#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 substitutes Name with Value wherever it appears as a whole word, before the code is compiled.
Text inside double or single quotes is not processed for substitution, so "This is BIG." would not be modified even if a define named BIG were defined.
That’s different from "This is [BIG].", where BIG is an embedded expression, which does get processed for substitution.
Defines can be removed with #undef Name, after which substitution no longer applies.
#define DAY 0
#define NIGHT 1
var/daytime = NIGHT // daytime = 1
Macros
Macros are defines with parameters, in the format #define Name(Parameters) Value.
They are purely textual substitution - the preprocessor replaces every call site with the expanded text before compilation.
There is no function call at runtime, no scope, no type checking, and no local variables.
#define isred(x) ((x).color == "#ff0000")
/proc/main()
var/obj/dog = new /obj{color = "#ff0000"}
if (isred(dog))
world.log << "Hello there!"
Because arguments are substituted as raw text, always wrap parameters in parentheses to avoid operator precedence issues:
#define double(x) x * 2 // BAD: double(1+1) expands to 1+1 * 2 = 3
#define double(x) (x) * 2 // GOOD: double(1+1) expands to (1+1) * 2 = 4
#if / #elif / #else / #ifdef / #endif
These conditional preprocessor directives work similarly to the regular if() and else() logic, except that you close the block with #endif.
The code within the true branch gets compiled; untrue branches are excluded entirely before the compiler ever sees them. They can only be used with other defines and constants, not runtime variables.
#elif is equivalent to else if().
#define LOGIC_A 1
#define LOGIC_B 2
#define LOGIC_C 3
/proc/main()
#ifdef LOGIC_C // LOGIC_C is defined, so this code will run.
world.log << "Logic C is in place"
#endif
#ifdef LOGIC_D // this will not run, since LOGIC_D isn't defined
world.log << "Logic D is in place"
#endif
#if LOGIC_A > LOGIC_B
world.log << "Logic A is larger than B" // this isn't compiled
#elif LOGIC_A < LOGIC_B
world.log << "Logic A is smaller than B" // this is compiled
#endif
#ifdef DEFINE is true when the named define exists, regardless of its value.
It is functionally similar to #if defined(DEFINE), but there is no #elsedef - chains must use #elif defined(...) instead.
#ifndef DEFINE is the inverse, similar to #if !defined(DEFINE).
A common use is grouping defines under a single “build setting” flag:
// Uncomment the one setting you want before compiling.
// This is a common pattern for '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
#include
#include "file.dm" includes another file into the current compilation unit.
The included file’s contents are processed as if they were written inline at that point.
This is how DM projects split code across multiple files.
#include "defines.dm"
#include "objects/furniture.dm"
#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.
#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"
Try / Catch
You can prevent exceptions (“runtimes”) from terminating a proc utilizing a try/catch block. If an exception occurs in any of the code within the “try” block, or any functions called inside of it, it will “catch” the exception in the “catch” block.
If an exception occurs within a try/catch block, it will unwind the entire callstack back to the try block.
try
var/foo = ""
foo += 1
catch(exception/e)
world.log << "Caught an exception! [e.name]"
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.