[MIRROR] Migrate some docs from the GitHub wiki into the repository (#2027)

* Merge pull request #55268 from AutomaticFrenzy/patch/docs-from-github-wiki

Migrate some docs from the GitHub wiki into the repository

* Migrate some docs from the GitHub wiki into the repository

Co-authored-by: Jordan Brown <Cyberboss@users.noreply.github.com>
This commit is contained in:
SkyratBot
2020-12-03 18:16:02 +01:00
committed by GitHub
parent 8c20ba83be
commit 2d58d386ef
14 changed files with 213 additions and 56 deletions
+83 -35
View File
@@ -1,5 +1,18 @@
# CONTRIBUTING
1. [Reporting Issues](#reporting-issues)
2. [Introduction](#introduction)
3. [Getting Started](#getting-started)
4. [Meet the Team](#meet-the-team)
1. [Headcoder](#headcoder)
2. [Maintainers](#maintainers)
3. [Issue Managers](#issue-managers)
5. [Specifications](#specifications)
6. [Pull Request Process](#pull-request-process)
7. [Porting features/sprites/sounds/tools from other codebases](#porting-featuresspritessoundstools-from-other-codebases)
8. [Banned content](#banned-content)
9. [A word on Git](#a-word-on-git)
## Reporting Issues
If you ever encounter a bug in-game, the best way to let a coder know about it is with our GitHub Issue Tracker. Please make sure you use the supplied issue template, and include the round ID for the server.
@@ -18,26 +31,63 @@ First things first, we want to make it clear how you can contribute (if you've n
/tg/station doesn't have a list of goals and features to add; we instead allow freedom for contributors 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.
If you want to contribute the first thing you'll need to do is [set up Git](http://tgstation13.org/wiki/Setting_up_git) so you can download the source code.
After setting it up, optionally navigate your git commandline to the project folder and run the command: 'git config blame.ignoreRevsFile .git-blame-ignore-revs'
After setting it up, optionally navigate your git commandline to the project folder and run the command: `git config blame.ignoreRevsFile .git-blame-ignore-revs`.
We have a [list of guides on the wiki](http://www.tgstation13.org/wiki/index.php/Guides#Development_and_Contribution_Guides) that will help you get started contributing to /tg/station with Git and Dream Maker. For beginners, it is recommended you work on small projects like bugfixes at first. If you need help learning to program in BYOND, check out this [repository of resources](http://www.byond.com/developer/articles/resources).
We have a [list of guides on the wiki](http://www.tgstation13.org/wiki/Guides#Development_and_Contribution_Guides) that will help you get started contributing to /tg/station with Git and Dream Maker. For beginners, it is recommended you work on small projects like bugfixes at first. If you need help learning to program in BYOND, check out this [repository of resources](http://www.byond.com/developer/articles/resources).
There is an open list of approachable issues for [your inspiration here](https://github.com/tgstation/tgstation/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22).
You can of course, as always, ask for help on the discord channels, or the forums, We're just here to have fun and help out, so please don't expect professional support.
You can of course, as always, ask for help on the Discord channels or the forums. We're just here to have fun and help out, so please don't expect professional support.
## Meet the Team
**Headcoder**
### Headcoder
The Headcoder is responsible for controlling, adding, and removing maintainers from the project. In addition to filling the role of a normal maintainer, they have sole authority on who becomes a maintainer, as well as who remains a maintainer and who does not.
**Maintainers**
### Maintainers
Maintainers are quality control. If a proposed pull request doesn't meet the following specifications, they can request you to change it, or simply just close the pull request. Maintainers are required to give a reason for closing the pull request.
Maintainers can revert your changes if they feel they are not worth maintaining or if they did not live up to the quality specifications.
<details>
<summary>Maintainer Guidelines</summary>
These are the few directives we have for project maintainers.
- Do not merge PRs you create.
- Do not merge PRs until 24 hours have passed since it was opened. Exceptions include:
- Emergency fixes.
- Try to get secondary maintainer approval before merging if you are able to.
- PRs with empty commits intended to generate a changelog.
- Do not merge PRs that contain content from the [banned content list](./CONTRIBUTING.md#banned-content).
These are not steadfast rules as maintainers are expected to use their best judgement when operating.
Our team is entirely voluntary, as such we extend our thanks to maintainers, issue managers, and contributors alike for helping keep the project alive.
</details>
### Issue Managers
Issue Managers help out the project by labelling bug reports and PRs and closing bug reports which are duplicates or are no longer applicable.
<details>
<summary>What You Can and Can't Do as an Issue Manager</summary>
This should help you understand what you can and can't do with your newfound github permissions.
Things you **CAN** do:
* Label issues appropriately
* Close issues when appropriate
* Label PRs when appropriate
Things you **CAN'T** do:
* [Close PRs](https://imgur.com/w2RqpX8.png): Only maintainers are allowed to close PRs. Do not hit that button.
</details>
## Specifications
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. Thank you for reading this section!
@@ -51,6 +101,7 @@ As BYOND's Dream Maker (henceforth "DM") is an object-oriented language, code mu
DM will allow you nest almost any type keyword into a block, such as:
```DM
// Not our style!
datum
datum1
var
@@ -80,6 +131,7 @@ The use of this is not allowed in this project as it makes finding definitions v
The previous code made compliant:
```DM
// Matches /tg/station style.
/datum/datum1
var/varname1
var/varname2
@@ -99,9 +151,9 @@ The previous code made compliant:
code
```
### All process procs need to make use of delta-time and be frame independent
### All `process` procs need to make use of delta-time and be frame independent
In a lot of our older code, process() is frame dependent. As an example, here's some example mob code.
In a lot of our older code, `process()` is frame dependent. Here's some example mob code:
```DM
/mob/testmob
@@ -110,14 +162,13 @@ In a lot of our older code, process() is frame dependent. As an example, here's
/mob/testmob/process(delta_time) //SSmobs runs once every 2 seconds
health -= health_loss
```
As the mobs subsystem runs once every 2 seconds, the mob now loses 4 health every process, or 2 health per second. This is called frame dependent programming.
Why is this an issue? If someone decides to make it so the mobs subsystem processes once every second (2 times as fast), your effects in process() will also be two times as fast. Resulting in 4 health loss per second rather than 2.
How do we solve this? By using delta-time. Delta-time is the amount of seconds you would theoretically have between 2 process() calls. In the case of the mobs subsystem, this would be 2 (As there is 2 seconds between every call in process()). Here is a new example using delta-time
How do we solve this? By using delta-time. Delta-time is the amount of seconds you would theoretically have between 2 process() calls. In the case of the mobs subsystem, this would be 2 (As there is 2 seconds between every call in `process()`). Here is a new example using delta-time:
```DM
/mob/testmob
@@ -126,8 +177,8 @@ How do we solve this? By using delta-time. Delta-time is the amount of seconds y
/mob/testmob/process(delta_time) //SSmobs runs once every 2 seconds
health -= health_loss * delta_time
```
In the above example, we made our health_loss variable a per second value rather than per process. In the actual process() proc we then make use of deltatime. Because SSmobs runs once every 2 seconds. Delta_time would have a value of 2. This means that by doing health_loss * delta_time, you end up with the correct amount of health_loss per process, but if for some reason the SSmobs subsystem gets changed to be faster or slower in a PR, your health_loss variable will work the same.
For example, if SSmobs is set to run once every 4 seconds, it would call process once every 4 seconds and multiply your health_loss var by 4 before subtracting it. Ensuring that your code is frame independent.
@@ -135,11 +186,11 @@ For example, if SSmobs is set to run once every 4 seconds, it would call process
### No overriding type safety checks
The use of the : operator to override type safety checks is not allowed. You must cast the variable to the proper type.
### Type paths must begin with a /
### Type paths must begin with a `/`
eg: `/datum/thing`, not `datum/thing`
### Type paths must be snake case
eg: `/datum/thing/blue_bird`, not `datum/thing/BLUEBIRD` or `datum/thing/BlueBird` or `datum/thing/Bluebird` or `datum/thing/blueBird`
eg: `/datum/blue_bird`, not `/datum/BLUEBIRD` or `/datum/BlueBird` or `/datum/Bluebird` or `/datum/blueBird`
### Datum type paths must began with "datum"
In DM, this is optional, but omitting it makes finding definitions harder.
@@ -155,16 +206,16 @@ var/path_type = /obj/item/baseball_bat
var/path_type = "/obj/item/baseball_bat"
```
### Use var/name format when declaring variables
### Use `var/name` format when declaring variables
While DM allows other ways of declaring variables, this one should be used for consistency.
### Tabs, not spaces
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)
You may use spaces to align something, but you should tab to the block level first, then add the remaining spaces.
### No hacky code
Hacky code, such as adding specific checks, is highly discouraged and only allowed when there is ***no*** other option. (Protip: '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. Maintainers exist for exactly that reason.)
Hacky code, such as adding specific checks, is highly discouraged and only allowed when there is ***no*** other option. (Protip: "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. Maintainers exist for exactly that reason.)
You can avoid hacky code by using object-oriented methodologies, such as overriding a function (called "procs" in DM) or sectioning code into functions and then overriding them as required.
@@ -304,7 +355,6 @@ This is good:
getter_turned_into_variable = condition ? VALUE_C : VALUE_D
```
### Develop Secure Code
* 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
@@ -345,7 +395,7 @@ This is good:
### Mapping Standards
* TGM Format & Map Merge
* All new maps submitted to the repo through a pull 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](https://github.com/tgstation/tgstation/wiki/Map-Merger) utility included in the repo to convert the file to TGM format.
* All new maps submitted to the repo through a pull 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](https://tgstation13.org/wiki/Map_Merger) utility included in the repo to convert the file to TGM format.
* Likewise, you MUST run Map Merge prior to opening your PR 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 PR process and is extremely likely to cause merge conflicts with other pull requests.
@@ -371,7 +421,7 @@ All procs that are registered to listen for signals using `RegisterSignal()` mus
```
This is to ensure that it is clear the proc handles signals and turns on a lint to ensure it does not sleep.
There exists `SIGNAL_HANDLER_DOES_SLEEP`, but this is only for legacy signal handlers that still sleep, new/changed code may not use this.
There exists `SIGNAL_HANDLER_DOES_SLEEP`, but this is only for legacy signal handlers that still sleep, new/changed code should not use this.
### Enforcing parent calling
When adding new signals to root level procs, eg;
@@ -425,27 +475,27 @@ Math operators like +, -, /, *, etc are up in the air, just choose which version
#### Use
* Bitwise AND - '&'
* Should be written as ```bitfield & bitflag``` NEVER ```bitflag & bitfield```, both are valid, but the latter is confusing and nonstandard.
* Should be written as `variable & CONSTANT` NEVER `CONSTANT & variable`. Both are valid, but the latter is confusing and nonstandard.
* Associated lists declarations must have their key value quoted if it's a string
* WRONG: list(a = "b")
* RIGHT: list("a" = "b")
* WRONG: `list(a = "b")`
* RIGHT: `list("a" = "b")`
### Dream Maker Quirks/Tricks
Like all languages, Dream Maker has its quirks, some of them are beneficial to us, like these
#### In-To for-loops
```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```).
`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`).
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!
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!
### for(var/A in list) VS for(var/i in 1 to list.len)
#### `for(var/A in list)` versus `for(var/i in 1 to list.len)`
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
#### Istypeless for loops
#### For loops without `istype`
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:
```DM
var/list/bag_of_items = list(sword, apple, coinpouch, sword, sword)
@@ -454,7 +504,7 @@ 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:
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:
```DM
var/list/bag_of_swords = list(sword, sword, sword, sword)
var/obj/item/sword/best_sword
@@ -465,7 +515,7 @@ for(var/obj/item/sword/S in bag_of_swords)
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:
you can circumvent DM's filtering and automatic `istype()` checks by writing the loop as such:
```DM
var/list/bag_of_swords = list(sword, sword, sword, sword)
var/obj/item/sword/best_sword
@@ -477,25 +527,23 @@ for(var/s in bag_of_swords)
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.
#### Dot variable
Like other languages in the C family, DM has a ```.``` or "Dot" operator, used for accessing variables/members/functions of an object instance.
Like other languages in the C family, DM has a `.` or "Dot" operator, used for accessing variables/members/functions of an object instance.
eg:
```DM
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?
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.
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.
## Globals versus static
### Globals versus static
DM has a var keyword, called global. This var keyword is for vars inside of types. For instance:
```DM
mob
var
global
thing = TRUE
/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)
-14
View File
@@ -1,14 +0,0 @@
# Maintainer Guidelines
This file outlines the few directives we have for project maintainers.
- Do not merge PRs you create.
- Do not merge PRs until 24 hours have passed since it was opened. Exceptions include:
- Emergency fixes.
- Try to get secondary maintainer approval before merging if you are able to.
- PRs with empty commits intended to generate a changelog.
- Do not merge PRs that contain content from the [banned content list](./CONTRIBUTING.md#banned-content).
These are not steadfast rules as maintainers are expected to use their best judgement when operating.
Our team is entirely voluntary, as such we extend our thanks to maintainers, issue managers, and contributors alike for helping keep the project alive.
+11 -2
View File
@@ -189,10 +189,19 @@ GLOBAL_LIST_EMPTY(bloody_footprints_cache)
//subtypesof(), typesof() without the parent path
#define subtypesof(typepath) ( typesof(typepath) - typepath )
//Gets the turf this atom inhabits
/**
* Get the turf that `A` resides in, regardless of any containers.
*
* Use in favor of `A.loc` or `src.loc` so that things work correctly when
* stored inside an inventory, locker, or other container.
*/
#define get_turf(A) (get_step(A, 0))
//Same as above except gets the area instead
/**
* Get the ultimate area of `A`, similarly to [get_turf].
*
* Use instead of `A.loc.loc`.
*/
#define get_area(A) (isarea(A) ? A : get_step(A, 0)?.loc)
//Ghost orbit types:
+1 -1
View File
@@ -229,7 +229,7 @@
return null
//Pick a random element from the list and remove it from the list.
/// Pick a random element from the list and remove it from the list.
/proc/pick_n_take(list/L)
RETURN_TYPE(L[_].type)
if(L.len)
+6 -1
View File
@@ -247,7 +247,12 @@ GLOBAL_LIST_EMPTY(species_list)
return ..()
///Timed action involving one mob user. Target is optional.
/**
* Timed action involving one mob user. Target is optional.
*
* Checks that `user` does not move, change hands, get stunned, etc. for the
* given `delay`. Returns `TRUE` on success or `FALSE` on failure.
*/
/proc/do_after(mob/user, delay, atom/target, timed_action_flags = NONE, progress = TRUE, datum/callback/extra_checks)
if(!user)
return FALSE
+6 -1
View File
@@ -141,7 +141,12 @@ Turf and target are separate in case you want to teleport some distance from a t
return destination
/proc/getline(atom/M,atom/N)//Ultra-Fast Bresenham Line-Drawing Algorithm
/**
* Get a list of turfs in a line from `M` to `N`.
*
* Uses the ultra-fast [Bresenham Line-Drawing Algorithm](https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm).
*/
/proc/getline(atom/M,atom/N)
var/px=M.x //starting x
var/py=M.y
var/line[] = list(locate(px,py,M.z))
+23
View File
@@ -1,3 +1,26 @@
/*!
## Debugging GC issues
In order to debug `qdel()` failures, there are several tools available.
To enable these tools, define `TESTING` in [_compile_options.dm](https://github.com/tgstation/-tg-station/blob/master/code/_compile_options.dm).
First is a verb called "Find References", which lists **every** refererence to an object in the world. This allows you to track down any indirect or obfuscated references that you might have missed.
Complementing this is another verb, "qdel() then Find References".
This does exactly what you'd expect; it calls `qdel()` on the object and then it finds all references remaining.
This is great, because it means that `Destroy()` will have been called before it starts to find references,
so the only references you'll find will be the ones preventing the object from `qdel()`ing gracefully.
If you have a datum or something you are not destroying directly (say via the singulo),
the next tool is `QDEL_HINT_FINDREFERENCE`. You can return this in `Destroy()` (where you would normally `return ..()`),
to print a list of references once it enters the GC queue.
Finally is a verb, "Show qdel() Log", which shows the deletion log that the garbage subsystem keeps. This is helpful if you are having race conditions or need to review the order of deletions.
Note that for any of these tools to work `TESTING` must be defined.
By using these methods of finding references, you can make your life far, far easier when dealing with `qdel()` failures.
*/
SUBSYSTEM_DEF(garbage)
name = "Garbage"
priority = FIRE_PRIORITY_GARBAGE
+2
View File
@@ -1,4 +1,6 @@
/**
* Machines in the world, such as computers, pipes, and airlocks.
*
*Overview:
* Used to create objects that need a per step proc call. Default definition of 'Initialize()'
* stores a reference to src machine in global 'machines list'. Default definition
+6 -2
View File
@@ -7,6 +7,7 @@ GLOBAL_VAR_INIT(rpg_loot_items, FALSE)
GLOBAL_VAR_INIT(stickpocalypse, FALSE) // if true, all non-embeddable items will be able to harmlessly stick to people when thrown
GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to embed in people, takes precedence over stickpocalypse
/// Anything you can pick up and hold.
/obj/item
name = "item"
icon = 'icons/obj/items_and_weapons.dmi'
@@ -453,6 +454,7 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb
/obj/item/proc/talk_into(mob/M, input, channel, spans, datum/language/language, list/message_mods)
return ITALICS | REDUCE_RANGE
/// Called when a mob drops an item.
/obj/item/proc/dropped(mob/user, silent = FALSE)
SHOULD_CALL_PARENT(TRUE)
for(var/X in actions)
@@ -477,8 +479,10 @@ GLOBAL_VAR_INIT(embedpocalypse, FALSE) // if true, all items will be able to emb
return
/**
*called after an item is placed in an equipment slot
* Called after an item is placed in an equipment slot.
*
* Note that hands count as slots.
*
* Arguments:
* * user is mob that equipped it
* * slot uses the slot_X defines found in setup.dm for items that can be placed in multiple slots
+1
View File
@@ -1,3 +1,4 @@
/// Inert structures, such as girders, machine frames, and crates/lockers.
/obj/structure
icon = 'icons/obj/structures.dmi'
pressure_resistance = 8
+2
View File
@@ -1,4 +1,6 @@
GLOBAL_LIST_EMPTY(station_turfs)
/// Any floor or wall. What makes up the station and the rest of the map.
/turf
icon = 'icons/turf/floors.dmi'
flags_1 = CAN_BE_DIRTY_1
@@ -1,3 +1,4 @@
/// Any humanoid (non-Xeno) mob, such as humans, plasmamen, lizards.
/mob/living/carbon/human
name = "Unknown"
real_name = "Unknown"
@@ -1,3 +1,4 @@
/// Simple, mostly AI-controlled critters, such as pets, bots, and drones.
/mob/living/simple_animal
name = "animal"
icon = 'icons/mob/animal.dmi'
+70
View File
@@ -0,0 +1,70 @@
# Unit Tests
## What is unit testing?
Unit tests are automated code to verify that parts of the game work exactly as they should. For example, [a test to make sure that the amputation surgery actually amputates the limb](https://github.com/tgstation/tgstation/blob/e416283f162b86345a8623125ab866839b1ac40d/code/modules/unit_tests/surgeries.dm#L1-L13). These are ran every time a PR is made, and thus are very helpful for preventing bugs from cropping up in your code that would've otherwise gone unnoticed. For example, would you have thought to check [that beach boys would still work the same after editing pizza](https://github.com/tgstation/tgstation/pull/53641#issuecomment-691384934)? If you value your time, probably not.
On their most basic level, when `UNIT_TESTS` is defined, all subtypes of `/datum/unit_test` will have their `Run` proc executed. From here, if `Fail` is called at any point, then the tests will report as failed.
## How do I write one?
1. Find a relevant file.
All unit test related code is in `code/modules/unit_tests`. If you are adding a new test for a surgery, for example, then you'd open `surgeries.dm`. If a relevant file does not exist, simply create one in this folder, then `#include` it in `_unit_tests.dm`.
2. Create the unit test.
To make a new unit test, you simply need to define a `/datum/unit_test`.
For example, let's suppose that we are creating a test to make sure a proc `square` correctly raises inputs to the power of two. We'd start with first:
```
/datum/unit_test/square/Run()
```
This defines our new unit test, `/datum/unit_test/square`. Inside this function, we're then going to run through whatever we want to check. Tests provide a few assertion functions to make this easy. For now, we're going to use `TEST_ASSERT_EQUAL`.
```
/datum/unit_test/square/Run()
TEST_ASSERT_EQUAL(square(3), 9, "square(3) did not return 9")
TEST_ASSERT_EQUAL(square(4), 16, "square(4) did not return 16")
```
As you can hopefully tell, we're simply checking if the output of `square` matches the output we are expecting. If the test fails, it'll report the error message given as well as whatever the actual output was.
3. Run the unit test
Open `code/_compile_options.dm` and uncomment the following line.
```
//#define UNIT_TESTS //If this is uncommented, we do a single run though of the game setup and tear down process with unit tests in between
```
Then, run tgstation.dmb in Dream Daemon. Don't bother trying to connect, you won't need to. You'll be able to see the outputs of all the tests. You'll get to see which tests failed and for what reason. If they all pass, you're set!
## How to think about tests
Unit tests exist to prevent bugs that would happen in a real game. Thus, they should attempt to emulate the game world wherever possible. For example, the [quick swap sanity test](https://github.com/tgstation/tgstation/blob/e416283f162b86345a8623125ab866839b1ac40d/code/modules/unit_tests/quick_swap_sanity.dm) emulates a *real* scenario of the bug it fixed occurring by creating a character and giving it real items. The unrecommended alternative would be to create special test-only items. This isn't a hard rule, the [reagent method exposure tests](https://github.com/tgstation/tgstation/blob/e416283f162b86345a8623125ab866839b1ac40d/code/modules/unit_tests/reagent_mod_expose.dm) create a test-only reagent for example, but do keep it in mind.
Unit tests should also be just that--testing *units* of code. For example, instead of having one massive test for reagents, there are instead several smaller tests for testing exposure, metabolization, etc.
## The unit testing API
You can find more information about all of these from their respective doc comments, but for a brief overview:
`/datum/unit_test` - The base for all tests to be ran. Subtypes must override `Run()`. `New()` and `Destroy()` can be used for setup and teardown. To fail, use `Fail(reason)`.
`/datum/unit_test/proc/allocate(type, ...)` - Allocates an instance of the provided type with the given arguments. Is automatically destroyed when the test is over. Commonly seen in the form of `var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human)`.
`TEST_ASSERT(assertion, reason)` - Stops the unit test and fails if the assertion is not met. For example: `TEST_ASSERT(powered(), "Machine is not powered")`.
`TEST_ASSERT_EQUAL(a, b, message)` - Same as `TEST_ASSERT`, but checks if `a == b`. If not, gives a helpful message showing what both `a` and `b` were. For example: `TEST_ASSERT_EQUAL(2 + 2, 4, "The universe is falling apart before our eyes!")`.
`TEST_ASSERT_NOTEQUAL(a, b, message)` - Same as `TEST_ASSERT_EQUAL`, but reversed.
`TEST_FOCUS(test_path)` - *Only* run the test provided within the parameters. Useful for reducing noise. For example, if we only want to run our example square test, we can add `TEST_FOCUS(/datum/unit_test/square)`. Should *never* be pushed in a pull request--you will be laughed at.
## Final Notes
- Writing tests before you attempt to fix the bug can actually speed up development a lot! It means you don't have to go in game and folllow the same exact steps manually every time. This process is known as "TDD" (test driven development). Write the test first, make sure it fails, *then* start work on the fix/feature, and you'll know you're done when your tests pass. If you do try this, do make sure to confirm in a non-testing environment just to double check.
- Make sure that your tests don't accidentally call RNG functions like `prob`. Since RNG is seeded during tests, you may not realize you have until someone else makes a PR and the tests fail!
- Do your best not to change the behavior of non-testing code during tests. While it may sometimes be necessary in the case of situations such as the above, it is still a slippery slope that can lead to the code you're testing being too different from the production environment to be useful.