The code for CM-SS13 is licensed under the GNU Affero General Public License v3, which can be found in full in /LICENSE-AGPL3.
Assets including icons and sound are under the Creative Commons 3.0 BY-SA license unless otherwise indicated. Authorship for assets including art and sound under the CC BY-SA license is defined as the active development team of CM-SS13 unless stated otherwise (by author of the commit).
All code is assumed to be licensed under AGPL v3 unless stated otherwise by file header. Commits before 9a001bf520f889b434acd295253a1052420860af are assumed to be licensed under GPLv3 and can be used in closed source repo.
CM-SS13 doesn't have a list of goals and features to add; we instead allow freedom for developers to suggest and create their ideas for the game. That doesn't mean we aren't determined to squash bugs, which unfortunately pop up a lot due to the deep complexity of the game. Here are some useful starting guides, if you want to contribute or if you want to know what challenges you can tackle with zero knowledge about the game's code structure.
While developers have the freedom to work on whatever they want, it falls to the team as a whole to decide if changes to the repository should be merged or not. This means that a merge request or change may be denied at the end, so understand that creative freedom does not grant you full reigns to commit anything to the repository without review, and you may have to deal with change requests or potential denial of your change.
To start contributing:
-
Download and install your git client. GitKraken is recommended.
-
Register and log into Gitlab. Wait for the Repository Owner to grant you access.
-
Clone the repository with your git client.
Host
The Host is responsible for controlling, adding, and removing maintainers from the project.
Maintainer
Maintainers refers to everyone in the development team. As a part of the development team, you are required to meet the guidelines and standards set by the Leads and the team when contributing to the repository. As well as contributing to the repository, Maintainers are in charge of handling the review of open MRs, as well as the management of suggestions and issues.
Contributors
Contributors refer to anyone outside of the development team contributing to the repository in the form of MRs. Much like Developers, you are required to follow this document with regards to code quality and standards. As a contributor, you may only open MRs with changes related to bugs, runtimes, or Accepted Suggestions on the Gitlab issue tracker, which are handled by the development team.
All Merge Requests (except Dev to Master MRs) must either target a work-in-progress branch, or the Dev branch.
All features added to Dev must come through a Merge Request. No one may push to Dev without a Merge Request.
All Merge Requests should be appropriately tagged. Any MR that is not complete and ready for review should be marked as a draft. Add any tags that apply to your MR, without being excessive.
Merge Requests should include a copy of the changelog entries for that branch in its description
Merge Requests targeting Dev should be set to squash commits and remove the branch when the merge is complete to keep the repository clean.
All Merge Requests must wait for the CI pipeline to complete before merging unless there is a confirmed issue with the unit tests themselves.
All Merge Requests require two valid Approvals to merge into Dev immediately, or one valid Approval by a Maintainer or higher and at least 24 hours of awaiting further approval. See Approvals below to determine who/when to approve an MR. MRs should only be approved once all discussions opened during review of the MR are resolved.
Merge requests should not include multiple sweeping changes unrelated to each other. Contributors and maintainers must split up and create multiple MRs for each of their changes where necessary so as to keep them organized as their own standalone feature or change. The only time multiple changes in an MR is acceptible is when submitting bugfixes to the repository.
Open issues are categorized into two tags, bugs & suggestions.
Bugs must be submitted using the bug template and should be reproduced by a member of the development team where possible. They should be tagged as bugs, as well as any relevant sub-categories that apply to them. If the bug could not be reproduced, the "Could not reproduce" tag should be applied.
If the bug in question is related to any change that may have been implemented by an active member of the development team, the Issue should be assigned to the person in question.
When attempting to fix a bug, if unassigned, Developers must assign themselves to the issue.
Feature requests and suggestions should not be made on the issue tracker.
As mentioned before, you are expected to follow these specifications in order to make everyone's lives easier. It'll save both your time and ours, by making sure you don't have to make any changes and we don't have to ask you to.
As BYOND's Dream Maker (henceforth "DM") is an object-oriented language, code must be object-oriented when possible in order to be more flexible when adding content to it.
While procs for getting variables are not required within DM, procs for setting should be preferred where possible. Code that modifies another datum's variables should be encapsulated into calling the other datum's setter procs if that variable is broadly used. Ensure that both the variable and the parameters are valid before using them.
For example:
for(var/obj/item/food in mre_packet)
food.add_poison(2)
/obj/item/food/proc/add_poison(var/amount)
poison += amount
Front-end usage of a datum and its procs should be easy to use and understand, and when called by other datums it should hide internal implementation details. Datums should only reveal operations relevant for the other datums.
Variables, procs, flags, and code should be inherited where possible to cut down on copy-pasted code. If two separate datums require the same code to function, they should inherit said code from their common parent. Additional differences in procs can then be defined on the child-level after using ..()
to call the parent's verison of the proc.
Subtypes of datums should be polymorphic where possible, with their procs and functionality inheriting from parent defines and adjusting them to meet requirements.
For example:
/obj/item/proc/add_poison(var/amount)
poison += amount
/obj/item/food/add_poison(var/amount)
..()
//Make the food turn green
if(poison >= poison_overflow_threshold)
color = "#ff0000"
/obj/item/weapon/add_poison(var/amount)
..()
//Make the weapon deal poison damage
if(poison)
flags_atom |= WEAPON_POISONOUS
(i.e. absolute pathing)
DM will allow you nest almost any type keyword into a block, such as:
datum
datum1
var
varname1 = 1
varname2
static
varname3
varname4
proc
proc1()
code
proc2()
code
datum2
varname1 = 0
proc
proc3()
code
proc2()
..()
code
The use of this is not allowed in this project as it makes finding definitions via full text searching next to impossible. The only exception is the variables of an object may be nested to the object, but must not nest further.
The previous code made compliant:
/datum/datum1
var/varname1
var/varname2
var/static/varname3
var/static/varname4
/datum/datum1/proc/proc1()
code
/datum/datum1/proc/proc2()
code
/datum/datum1/datum2
varname1 = 0
/datum/datum1/datum2/proc/proc3()
code
/datum/datum1/datum2/proc2()
..()
code
The use of the : operator and ?. proc call operator to override type safety checks is not allowed. You must cast the variable to the proper type. Do not use ?. over checking for a variable's existence (e.g. client && client.prefs instead of client?.prefs).
eg: /datum/thing
, not datum/thing
eg: /datum/thing/blue
, not datum/thing/BLUE
or datum/thing/Blue
In DM, this is optional, but omitting it makes finding definitions harder.
Do not put type paths in a text format, as there are no compile errors if the type path no longer exists. Here is an example:
//Good
var/path_type = /obj/item/baseball_bat
//Bad
var/path_type = "/obj/item/baseball_bat"
While DM allows other ways of declaring variables, this one should be used for consistency. Defining lists can be abstract when only a single datum type is known (var/list/datum_list
).
You must use tabs to indent your code, NOT SPACES.
(You may use spaces to align something, but you should tab to the block level first, then add the remaining spaces)
All variables, procs, and files should use snake case.
var/snake_case_variable
/mob/living/simple_mob/snake/proc/attack_prey(var/mob/target_prey)
...
Hacky code, such as adding specific checks, is highly discouraged and only allowed when there is no other option. ('I couldn't immediately think of a proper way so thus there must be no other option' is not gonna cut it here. If you can't think of anything else, say that outright and admit that you need help with it. The team exists for a reason.)
You can avoid hacky code by using object-oriented methodologies, such as overriding a proc or sectioning code into functions and then overriding them as required.
Copying code from one place to another may be suitable for small, short-time projects, but CM is a long-term project and highly discourages this.
Instead you can use object orientation, or simply placing repeated code in a function, to obey this specification easily.
Repeated code sometimes may be necessary, but it should only be considered when all other options are unavailable.
First, read the comments in this BYOND thread, starting where the link takes you.
Lazy instantiation as a whole is about not instantiating objects before you actually use them. In terms of lists this means only declaring (but not defining) your list in the definition or defining it as null.
Before you use the list in your code (anywhere), you use LAZYINITLIST(L)
, which just checks if the list has been defined and creates it for you if not.
When adding or removing elements to/from the list, use LAZYREMOVE(L, I)
/LAZYADD(L, I)
. It's just a wrapper for checking if the list exists first, but for additions it will create the list if it doesn't exist, and for removals it also nulls the list if it's empty after the removal. Therefore it's important to be consistent about your use of lazy instantiation. LAZYINSERT(L, I, X)
does the obvious, but LAZYDISTINCTADD(L, I)
only inserts into the list if I
does not already exist in the list.
All the above are for indexed lists. If you use associative lists, use LAZYSET(L, A, I)
, which performs L[A] = I
.
For list access, use LAZYACCESS(L, I)
, which gets you L[I]
.
Other defines are LAZYLEN(L)
which gives you the length of the list, LAZYISIN(L, I)
which is basically an in
keyword replacement, and LAZYCLEARLIST(L)
which clears the full list for you.
One important thing to note is that all these defines have null safety. I.e. it'll check if the list exists first for you and create it if it doesn't when needed.
Our game controller is pretty good at handling long operations and lag, but it can't control what happens when the map is loaded, which calls New
for all atoms on the map. If you're creating a new atom, use the Initialize
proc to do what you would normally do in New
. This cuts down on the number of proc calls needed when the world is loaded.
You should only ever use New
when it is absolutely necessary.
While we require bringing out of date code up to date when you make unrelated changes near the out of date code, that is not the case for New
-> Initialize
conversions. These systems are generally more dependant on parent and children procs so unrelated random conversions of existing things can cause bugs that take months to figure out.
To tie in with using Initialize, atoms must use the garbage collector's qdel(atom)
function in order to queue deletions instead of hard-calling del()
. The priority of an atom's deletion can be marked with qdel hints (such as GC_HINT_DELETE_NOW).
All datums must correctly nullify and delete all stored references to other datums within their Dispose()
proc. The Dispose()
proc should also never be directly called outside of the garbage collector.
This means stuff like having a "mode" variable for an object set to "1" or "2" with no clear indicator of what that means. Make these #defines with a name that more clearly states what it's for. For instance:
/datum/proc/do_the_thing(thing_to_do)
switch(thing_to_do)
if(1)
(...)
if(2)
(...)
There's no indication of what "1" and "2" mean! Instead, you'd do something like this:
#define DO_THE_THING_REALLY_HARD 1
#define DO_THE_THING_EFFICIENTLY 2
/datum/proc/do_the_thing(thing_to_do)
switch(thing_to_do)
if(DO_THE_THING_REALLY_HARD)
(...)
if(DO_THE_THING_EFFICIENTLY)
(...)
This is clearer and enhances readability of your code! Get used to doing it!
Use TRUE and FALSE for boolean logic instead of 1 and 0.
/datum/proc/do_the_thing(thing_to_do)
switch(thing_to_do)
if(DO_THE_THING_REALLY_HARD)
return TRUE
if(DO_THE_THING_EFFICIENTLY)
return FALSE
(if, while, for, etc)
- All control statements must not contain code on the same line as the statement (
if (blah) return
) - All control statements comparing a variable to a number should use the formula of
thing
operator
number
, not the reverse (eg:if (count <= 10)
notif (10 >= count)
)
Do not enclose a proc in an if-block when returning on a condition is more feasible This is bad:
/datum/datum1/proc/proc1()
if (thing1)
if (!thing2)
if (thing3 == 30)
do stuff
This is good:
/datum/datum1/proc/proc1()
if (!thing1)
return
if (thing2)
return
if (thing3 != 30)
return
do stuff
This prevents nesting levels from getting deeper then they need to be.
If you need to use a typecheck multiple times for a specific type path, create an istype macro within the #define/type_check folder.
#define isbrain(A) (istype(A, /mob/living/brain))
-
Player input must always be escaped safely, we recommend you use stripped_input in all cases where you would use input. Essentially, just always treat input from players as inherently malicious and design with that use case in mind
-
Calls to the database must be escaped properly - use sanitizeSQL to escape text based database entries from players or admins, and isnum() for number based database entries from players or admins.
-
All calls to topics must be checked for correctness. Topic href calls can be easily faked by clients, so you should ensure that the call is valid for the state the item is in. Do not rely on the UI code to provide only valid topic calls, because it won't.
-
Information that players could use to metagame (that is, to identify round information and/or antagonist type via information that would not be available to them in character) should be kept as administrator only.
-
It is recommended as well you do not expose information about the players - even something as simple as the number of people who have readied up at the start of the round can and has been used to try to identify the round type.
-
Where you have code that can cause large-scale modification and FUN, make sure you start it out locked behind one of the default admin roles - use common sense to determine which role fits the level of damage a function could do.
-
Because runtime errors do not give the full path, try to avoid having files with the same name across folders.
-
File names should not be mixed case, or contain spaces or any character that would require escaping in a uri.
-
Files and path accessed and referenced by code simply being #included should be strictly lowercase to avoid issues on filesystems where case matters.
-
TGM Format & Map Merge
- All new maps submitted to the repo through a merge request must be in TGM format (unless there is a valid reason present to have it in the default BYOND format.) This is done using the Map Merge utility included in the repo to convert the file to TGM format.
- Likewise, you MUST run Map Merge prior to opening your MR when updating existing maps to minimize the change differences (even when using third party mapping programs such as FastDMM.)
- Failure to run Map Merge on a map after using third party mapping programs (such as FastDMM) greatly increases the risk of the map's key dictionary becoming corrupted by future edits after running map merge. Resolving the corruption issue involves rebuilding the map's key dictionary; id est rewriting all the keys contained within the map by reconverting it from BYOND to TGM format - which creates very large differences that ultimately delay the MR process and is extremely likely to cause merge conflicts with other merge requests.
-
Variable Editing (Var-edits)
- While var-editing an item within the editor is perfectly fine, it is STRONGLY PREFERRED that when you are changing the base behavior of an item (how it functions) that you make a new subtype of that item within the code, especially if you plan to use the item in multiple locations on the same map, or across multiple maps. This makes it easier to make corrections as needed to all instances of the item at one time as opposed to having to find each instance of it and change them all individually.
- Please attempt to clean out any dirty variables that may be contained within items you alter through var-editing. For example, due to how DM functions, changing the
pixel_x
variable from 23 to 0 will leave a dirty record in the map's code ofpixel_x = 0
. Likewise this can happen when changing an item's icon to something else and then back. This can lead to some issues where an item's icon has changed within the code, but becomes broken on the map due to it still attempting to use the old entry. - Areas should not be var-edited on a map to change it's name or attributes. All areas of a single type and it's altered instances are considered the same area within the code, and editing their variables on a map can lead to issues with powernets and event subsystems which are difficult to debug.
- All new player-facing user interfaces must use nanoui.
- Raw HTML is permitted for admin and debug UIs.
- Operators that should be separated by spaces
- Boolean and logic operators like &&, || <, >, ==, etc (but not !)
- Bitwise AND &
- Argument separator operators like , (and ; when used in a forloop)
- Assignment operators like = or += or the like
- Operators that should not be separated by spaces
- Bitwise OR |
- Access operators like . and :
- Parentheses ()
- logical not !
Math operators like +, -, /, *, etc are up in the air, just choose which version looks more readable.
- Bitwise AND - '&'
- Should be written as
bitfield & bitflag
NEVERbitflag & bitfield
, both are valid, but the latter is confusing and nonstandard.
- Should be written as
- Associated lists declarations must have their key value quoted if it's a string
- WRONG: list(a = "b")
- RIGHT: list("a" = "b")
Like all languages, Dream Maker has its quirks, some of them are beneficial to us, like these
for(var/i = 1, i <= some_value, i++)
is a fairly standard way to write an incremental for loop in most languages (especially those in the C family), but DM's for(var/i in 1 to some_value)
syntax is oddly faster than its implementation of the former syntax; where possible, it's advised to use DM's syntax. (Note, the to
keyword is inclusive, so it automatically defaults to replacing <=
; if you want <
then you should write it as 1 to some_value-1
). You can use the step keyword to iterate at different steps (e.g. i-- or i -= 2)
. It would go like for (var/i in 100 to 0 step -1)
.
HOWEVER, if either some_value
or i
changes within the body of the for (underneath the for(...)
header) or if you are looping over a list AND changing the length of the list then you can NOT use this type of for-loop!
Placement of operators and bracketing can affect the outcome of certain logic within DM code, and it's important to make sure you are writing the correct condition checking when making situationals.
A common mistake that's easy to fall for is that bitwise AND has a lower operator precedence than equality checks, so a & b == c
evaluates to a & (b == c)
instead of (a & b) == c
as most sane people would probably expect
The former is faster than the latter, as shown by the following profile results: https://file.house/zy7H.png Code used for the test in a readable format: https://pastebin.com/w50uERkG
A name for a differing syntax for writing for-each style loops in DM. It's NOT DM's standard syntax, hence why this is considered a quirk. Take a look at this:
var/list/bag_of_items = list(sword, apple, coinpouch, sword, sword)
var/obj/item/sword/best_sword
for(var/obj/item/sword/S in bag_of_items)
if(!best_sword || S.damage > best_sword.damage)
best_sword = S
The above is a simple proc for checking all swords in a container and returning the one with the highest damage, and it uses DM's standard syntax for a for-loop by specifying a type in the variable of the for's header that DM interprets as a type to filter by. It performs this filter using istype()
(or some internal-magic similar to istype()
- this is BYOND, after all). This is fine in its current state for bag_of_items
, but if bag_of_items
contained ONLY swords, or only SUBTYPES of swords, then the above is inefficient. For example:
var/list/bag_of_swords = list(sword, sword, sword, sword)
var/obj/item/sword/best_sword
for(var/obj/item/sword/S in bag_of_swords)
if(!best_sword || S.damage > best_sword.damage)
best_sword = S
specifies a type for DM to filter by.
With the previous example that's perfectly fine, we only want swords, but here the bag only contains swords? Is DM still going to try to filter because we gave it a type to filter by? YES, and here comes the inefficiency. Wherever a list (or other container, such as an atom (in which case you're technically accessing their special contents list, but that's irrelevant)) contains datums of the same datatype or subtypes of the datatype you require for your loop's body,
you can circumvent DM's filtering and automatic istype()
checks by writing the loop as such:
var/list/bag_of_swords = list(sword, sword, sword, sword)
var/obj/item/sword/best_sword
for(var/s in bag_of_swords)
var/obj/item/sword/S = s
if(!best_sword || S.damage > best_sword.damage)
best_sword = S
Of course, if the list contains data of a mixed type then the above optimisation is DANGEROUS, as it will blindly typecast all data in the list as the specified type, even if it isn't really that type, causing runtime errors.
Like other languages in the C family, DM has a .
or "Dot" operator, used for accessing variables/members/functions of an object instance.
eg:
var/mob/living/carbon/human/H = YOU_THE_READER
H.gib()
However, DM also has a dot variable, accessed just as .
on its own, defaulting to a value of null. Now, what's special about the dot operator is that it is automatically returned (as in the return
statement) at the end of a proc, provided the proc does not already manually return (return count
for example.) Why is this special?
With .
being everpresent in every proc, can we use it as a temporary variable? Of course we can! However, the .
operator cannot replace a typecasted variable - it can hold data any other var in DM can, it just can't be accessed as one, although the .
operator is compatible with a few operators that look weird but work perfectly fine, such as: .++
for incrementing .'s
value, or .[1]
for accessing the first element of .
, provided that it's a list.
DM has a var keyword, called global. This var keyword is for vars inside of types. For instance:
mob
var
global
thing = TRUE
This does NOT mean that you can access it everywhere like a global var. Instead, it means that that var will only exist once for all instances of its type, in this case that var will only exist once for all mobs - it's shared across everything in its type. (Much more like the keyword static
in other languages like PHP/C++/C#/Java)
Isn't that confusing?
There is also an undocumented keyword called static
that has the same behaviour as global but more correctly describes BYOND's behaviour. Therefore, we always use static instead of global where we need it, as it reduces suprise when reading BYOND code.
Bitflags are a method of defining properties of an atom in order to pass or fail checks in other sections of the code. Bitflags should be used as a replacement for any single true/false boolean variable that may be tied to a type, so that atom defines do not end up with hundreds of unique booleans to track.
For instance:
#define ITEM_FLAG_UNACIDABLE 1
#define ITEM_FLAG_INDESTRUCTIBLE 2
#define ITEM_FLAG_BOUNCY 4
#define ITEM_FLAG_SQUEAKY 8
/obj/attackby()
if(flags_atom & ITEM_FLAG_BOUNCY)
...
/obj/item/rubber_ball
flags_atom = ITEM_FLAG_UNACIDABLE|ITEM_FLAG_BOUNCY
In the case where span classes are required, for strings and text to be displayed to a user, span macros should be used to encapsulate and style where necessary.
For instance:
to_chat(SPAN_WARNING("You are on fire!"))
-
Code should be modular where possible; if you are working on a new addition, then strongly consider putting it in its own file unless it makes sense to put it with similar ones (i.e. a new tool would go in the "tools.dm" file)
-
Bloated code may be necessary to add a certain feature, which means there has to be a judgement over whether the feature is worth having or not. You can help make this decision easier by making sure your code is modular.
-
You are expected to help maintain the code that you add, meaning that if there is a problem then you are likely to be approached in order to fix any issues, runtimes, or bugs.
-
Do not divide when you can easily convert it to multiplication. (ie
4/2
should be done as4*0.5
, because it's slightly faster) -
If you used regex to replace code during development of your code, post the regex in your MR for the benefit of future developers.
-
Changes to the
/config
tree must be made in a way that allows for updating server deployments while preserving previous behaviour. This is due to the fact that the config tree is to be considered owned by the server and not necessarily updated alongside the remainder of the code. The code to preserve previous behaviour may be removed at some point in the future given the OK by developers. -
English/British spelling on var/proc names
- Color/Colour - both are fine, but keep in mind that BYOND uses
color
as a base variable
- Color/Colour - both are fine, but keep in mind that BYOND uses
-
Space usage in control statements
if()
andif ()
- Spaces should be used between conditions, but never between brackets:if(istype(src, /obj/item) && (unacidable || indestructible))
-
Space usage in lists
- Lists should only have spaces after commas:
var/list/new_list = list(foo, bar, x, y)
- Lists should only have spaces after commas:
-
Make sure your merge request complies.
-
You are going to be expected to document all your changes in the merge request. Failing to do so will mean delaying it as we will have to question why you made the change. On the other hand, you can speed up the process by making the merge request readable and easy to understand, with diagrams or before/after data.
-
Use the changelog system to document your change, which prevents our players from being caught unaware by changes.
-
If you are proposing multiple changes, which change many different aspects of the code, you are expected to section them off into different merge requests in order to make it easier to review them and to deny/accept the changes that are deemed acceptable.
-
If your merge request is accepted, the code you add no longer belongs exclusively to you but to everyone; everyone is free to work on it, but you are also free to support or object to any changes being made, which will likely hold more weight, as you're the one who added the feature. It is a shame this has to be explicitly said, but there have been cases where this would've saved some trouble.
-
Explain why you are submitting the merge request, and how you think your change will be beneficial to the game. Failure to do so will be grounds for rejecting the MR.
-
If your merge request is not finished make sure it is at least testable in a live environment. merge requests that do not at least meet this requirement will be closed. You may reopen the merge request when you're ready, or make a new one.
Valid Approvals are required for an MR to be merged to Dev. Approving an MR implies that certain actions have been taken by the approving party and that certain conditions are met:
- For MRs that include coding changes, at least one of the approving parties must be a Coder or a Developer+.
- For MRs that include spriting changes, at least one of the approving parties must be a Spriter+. The Lead Spriter may waive this requirement on a case-by-case basis.
- For MRs that include mapping changes, at least one of the approving parties must be a Mapper or a Senior Developer+.
- For MRs with a mixture of changes, all of the above requirements must be met; a Developer+ counts as a Coder and Mapper for this purpose and only one Developer is required in such a case. Additionally, Spriters who contribute to the MR may permit an approving Developer to review the MR in lieu of another Spriter doing so; the Lead Spriter may veto this to review it themselves at their discretion.
- The approving party must observe all changes in the MR relevant to their rank and conclude that they fit our standards of quality.
- Approving parties must ensure that necessary testing has been conducted in a satisfactory manner (see Testing below).
All Merge Requests must wait for the CI pipeline to complete before merging unless there is a confirmed issue with the unit tests themselves.
Merge Requests require two valid Approvals to merge into Dev immediately, or one valid Approval by a Developer or higher and at least 24 hours of awaiting further approval. MRs should only be approved once all discussions opened during review of the MR are resolved.
Once a Merge Request is approved and merged into dev, ensure that all changelog files are forwarded to the Wiki Maintainer, as well as any art assets or additional information they request.
The master branch, in place to act as the stable build of the game, should remain separate from upcoming changes in the dev branch.
In order to update master with new changes, a "dev to master" must be overseen by the Head Developer. This is performed by submitting a Merge Request from dev into master. Before merge, all changelogs must be compiled via the changelog generation tool.
Dev to masters must be preceded by a period of live testing on the dev branch, and any new runtimes and bugs must be handled before a merge is considered.
Testing of new features is absolutely vital to ensuring smooth integration of new and changed content. It’s also deceptively necessary even for small bugfixes and features or tiny tweaks; it’s all too easy to change just a line or two, assume that it works, and push it because it compiles.
Unit Testing allows you to run large scale tests quickly by writing out a set of instructions for the server to run by itself. They can be defined under /test
and can be triggered in the debug tools, as well as during any MR build test. When the code can be tested quickly by the server, consider writing a unit test for it (meaning if your code has to wait for an event or sleep, don't write a test).
The following are mandatory before merging an MR into Dev, to be overridden only by those with Dev to Master merge permissions, and only in emergency or immediately-rectifiable cases (i.e. a single forgotten escape character or quotation mark to close a string).
- Compile locally. This is to save you time waiting for the pipeline to fail.
- Run a local instance and directly test that the intended change has occurred. Enlist additional help in #dev-test if you need extra hands. Some high-level changes are too complex to fully test without the server’s population stressing them, but it’s best to at least test that the new content doesn’t produce runtime errors or etc.
- Test for unintended consequences where possible. Consider what other elements of the game may interact with the change and simulate what might occur. For example, test what happens when a machine is blown up while it’s performing some process, ensure that a new action can’t be taken while stunned or knocked down, and so on.
- Observe that the CI pipeline for your MR has completed with no issues.
Dream Daemon can be unintuitive to set up. This document will cover the basic steps for setting up a local instance for testing with. It won’t go over port forwarding; Google handles that nicely.
- Compile, if you haven’t already.
- Start Dream Daemon from the gear icon in the BYOND hub.
- Locate and select the Colonial Marines repository .dmb file for running.
- Set the trust level to Trusted.
- Set a fixed port, and forward it if you haven’t already.
- Press Go, wait for the server to spin up, and enter with the small yellow Join button.
Be sure Dream Daemon is inactive when compiling, as it locks up assets and prevents recompiling in some cases.
Unit tests can be run locally with DreamDaemon ColonialMarinesALPHA.dme someport -trusted -params "run_tests=1&verbose_tests=1"
or by hosting a server and using the debug verbs for running test cases. If you've only made changes relevant to one test set you can use the world parameter test_set
to run only the specified test set, i.e.
-params "run_tests=1&verbose_tests=1&test_set=\"Maps\""
The Host, System Administrator, Head Developer, and Senior Developers can change which branch is active on the game server. This allows the Team to test a feature without spoiling other upcoming features in Dev, to test Dev features for a short time to gather feedback before permanent release, and so on.
Senior Developers+ may authorize a test. The Head Dev must be informed of what is being tested and when, prior to conducting the test; it is not necessary to request specific permission to run a test so long as it is authorized by a Senior Developer+, only to ensure that the Head Dev is aware of it.
Public tests must be supervised by at least one Developer+, with the change owner either being present or authorising another Developer+ to oversee their test. Any stand-in Developers for the author must be fully aware of the test's changes and functionality.
Upon completing the public test, follow up with the Head Dev and describe the results of the test and what actions will be taken in response to the collected data. It is among the Head Dev’s responsibilities to compare the Team’s perspective on experimental features to feedback from the community - informing them is essential.