There are nearly infinite ways to program the same thing, but some ways will get your PR rejected.
In this page you'll learn all about the coding conventions we have chosen for the codebase, which you'll need to follow if you want to get your PR merged.
See Codebase Organization for guidelines on how files and folders are organized in the SS14 codebase.
Keep in mind that some older areas of the codebase might not follow these conventions.
These should be refactored in the future to follow them.
All new code should try to follow these conventions as closely as possible.
These conventions are C# guidelines that are not specific to SS14's systems. If I could use a time machine to force these onto every C# developer ever, I would.
Always put all fields and auto-properties at the top of a type definition. No putting auto properties in between a bunch of methods. It should be easy to see all the data a type contains.
Bad:
class Foo
{
public int Bar { get; set; }
public void Honk()
{
}
public int Baz { get; set; }
}
Good:
class Foo
{
public int Bar { get; set; }
public int Baz { get; set; }
public void Honk()
{
}
}
If you're defining a function and the parameter declarations are so long they don't fit on a single line, break them apart so you have one parameter per line. Some leeway is granted for closely tied parameter pairs like X/Y coordinates and pointer/length in C APIs.
Bad:
public void CopyTo(ISerializationManager serializationManager, SortedDictionary<TKey, TValue> source, ref SortedDictionary<TKey, TValue> target,
SerializationHookContext hookCtx, ISerializationContext? context = null)
Good:
public void CopyTo(
ISerializationManager serializationManager,
SortedDictionary<TKey, TValue> source,
ref SortedDictionary<TKey, TValue> target,
SerializationHookContext hookCtx,
ISerializationContext? context = null)
Human-readable text should never be used as an identifier or vice versa. That means no putting human-readable text (result of localization functions) in a dictionary key, comparing with ==
, etc...
This avoids spaghetti when these inevitably have to be decoupled for various reasons, and avoids inefficiency and bugs from comparing human-readable strings.
In a property setter, the value of the property should always literally become the value
given. None of this:
public string Name
{
get => _name;
private set => _name = Loc.GetString(value);
}
If you're doing something like a filter/search dialog, use CurrentCulture
comparisons over human-readable strings. Do not use invariant cultures.
Comment as much as you can, within limits.
You don't need to comment every line of what an obvious piece of code does, but leaving comments around roughly explaining what your code should be doing is highly encouraged.
You should always make use of XML docs for documenting classes, structs, methods, properties/fields, class members, etc. For explaining certain lines of code, leaving a TODO, etc, you can just use regular comments.
If you have a specific value such as an integer you should generally make it either:
This is so it is clear to others what it is. This is especially true if the same value is used in multiple places to make the code more maintainable.
Read the Pull Request guidelines to learn how to make your code more reviewable by maintainers.
Don't cache prototypes, use prototypeManager to index them when they are needed. You can store them by their ID. When using data-fields that involve prototype ID strings, use custom type serializers. For example, a data-field for a list of prototype IDs should use something like:
[DataField("exampleTypes", customTypeSerializer: typeof(PrototypeIdListSerializer<ExamplePrototype>))]
public List<string> ExampleTypes = new();
The usage of enums for in-game types is heavily discouraged.
You should always use prototypes over enums.
Example: In-game tool "kinds" or "types" should use prototypes instead of enums.
When specifying sound data fields, use SoundSpecifier
.
[DataField("sound", required: true)]
public SoundSpecifier Sound { get; } = default!;
# You can specify a specific sound file like this
- type: MyComponent
sound:
path: /Audio/path/to/my/sound.ogg
# But this works, too!
- type: MyOtherComponent
sound: /Audio/path/to/my/sound.ogg
# You can only specify a sound collection like this
- type: AnotherComponent
sound:
collection: MySoundCollection
When specifying sprite or texture data fields, use SpriteSpecifier
.
[DataField("icon")]
public SpriteSpecifier Icon { get; } = SpriteSpecifier.Invalid;
# You can specify a specific texture file like this
- type: MyComponent
icon: /Textures/path/to/my/texture.png
# You can specify an rsi sprite like this
- type: MyOtherComponent
icon:
sprite: /Textures/path/to/my/sprite.rsi
state: MySpriteState
version -> license -> copyright -> size -> states
.Example:
{
"version": 1,
"license": "CC0-1.0",
"copyright": "GitHub @PJB3005",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "hello",
"flags": {},
"directions": 4,
"delays": [
[1, 1, 1],
[2, 3, 4],
[3, 4, 5],
[4, 5, 6]
]
}
]
}
Please ensure you structure entities with components as follows for easier YAML readability:
type: entity
parent: Base<nameofparent>
id:
name:
abstract:
components:
<rest of file>
When using EntityUid
in admin logs, use the IEntityManager.ToPrettyString(EntityUid)
method.
// If you're in an entity system...
_adminLogs.Add(LogType.MyLog, LogImpact.Medium, $"{ToPrettyString(uid)} did something!");
// If you're not in an entity system...
_adminLogs.Add(LogType.MyLog, LogImpact.Medium, $"{entityManager.ToPrettyString(uid)} did something!");
If you need to pass "optional" entities around, you should use a nullable EntityUid
for this.
Never use EntityUid.Invalid
to denote the abscence of EntityUid
, always use null
and nullability so we have compile-time checks.
e.g. EntityUid? uid
All data in components should be public.
You may not have setters with any logic whatsoever in properties. Instead, you should create a setter method in your entity system, and apply the [Friend(...)]
attribute to the component so only that system can modify it.
Your component may use properties with setter logic for ViewVariables integration (until we have a better system for that)
The [Access(...)]
attribute allows you to specify which types can read or modify data in your class, while prohibiting every other type from modifying it.
Components should specify their access restrictions whenever possible, usually only allowing the entity systems that wrap them to modify their data.
If a shared component is inherited by server and client-side counterparts, it should be marked as abstract.
Game logic should always go in entity systems, not components.
Components should only hold data.
When possible, try using the EntitySystem
proxy methods instead of using the EntityManager
property.
// Without proxy methods...
EntityManager.GetComponent<MetaDataComponent>(uid).EntityName;
// With proxy methods
Name(uid);
// Without proxy methods...
EntityManager.GetComponent<TransformComponent>(uid).Coordinates;
// With proxy methods
Transform(uid).Coordinates;
All public Entity System API Methods that deal with entities and game logic should always follow a very specific structure.
All relevant EntityUid
should come first.
Next, any arguments you want should come afterwards.
And finally, all the components you need from the entity or entities should come last.
The component arguments shall be nullable, and default to null
.
The first thing you should do in your method's body should then be calling Resolve
for the entity UID and components.
public void SetCount(EntityUid uid, int count, StackComponent? stack = null)
{
// This call below will set "stack" to the correct instance if it's null.
// If all components were resolved to an instance or were non-null, it returns true.
if(!Resolve(uid, ref stack))
return; // If the component wasn't found, this will log an error by default.
// Logic here!
}
The Resolve
helper performs a few useful checks for you. In DEBUG
, it checks whether the component reference passed (if not null) is actually owned by the entity specified.
This helper will also log an error by default if the entity is missing any of the components that you attempted to resolve.
This error logging can be disabled by passing false
to the helper's logMissing
argument. You may want to disable the error logging for resolving optional components, TryX
pattern methods, etc.
Please note that the Resolve
helper also has overloads for resolving 2, 3 or even 4 components at once.
If you want to resolve components for multiple entities, or you want to resolve more than 4 components at once for a given entity, you'll need to perform multiple Resolve
calls.
See the ESM resolves proposal document for a more comprehensive and exhaustive explanation.
Extension methods (those with an explicit this
for the first argument) should never be used on any classes directly related to simulation--that means EntityUid
, components, or entity systems. Extension methods on EntityUid
are used throughout the codebase, however this is bad practice and should be replaced with entity system public methods instead.
Method Events are events that you raise when you want to perform a certain action. Example:
// This would change the damage on the entity by 10.
RaiseLocalEvent(uid, new ChangeDamageEvent(10));
On the other hand, Entity System Methods are methods you call on systems to perform an action.
// This would change the damage on the entity by 10.
EntitySystem.Get<DamageableSystem>().ChangeDamage(uid, 10);
Method Events are prohibited, always use Entity System Methods instead.
There's an exception to this, however.
You may use Method Events as long as they're wrapped by an Entity System Method.
In the example above, this would mean that DamageableSystem.ChangeDamage()
would internally raise the ChangeDamageEvent
, which would then by handled by any subscriptors...
Always suffix your events with Event
.
Example: DamagedEvent
, AnchorAttemptEvent
...
Always name your event handler like this: OnXEvent
Example: OnDamagedEvent
, OnAnchorAttemptEvent
...
Events should always be structs, not classes, and should always be raised by ref. If possible it should also be readonly if applicable.
They should also have the [ByRefEvent] attribute.
In practice this will look like the following:
var ev = new MyEvent();
RaiseLocalEvent(ref ev);
The EventBus should generally be used over C# events where possible. C# events can leak, especially when used with components which can be created or removed at any time.
C# events should be used for out-of-simulation events, such as UI events.
Remember to always unsubscribe from them, however!
For things such as DoAfter, always use events instead of async.
Async for any game simulation code should be avoided at all costs, as it's generally virulent, cannot be serialized (in the case of DoAfter, for example), and usually causes icky code.
Events, on the other hand, tie in nicely with the rest of the game's architecture, and although they aren't as convenient to code, they are definitely way more lightweight.
- type
should be together without any empty newlines separating themname:
and description:
fields should never have quotations unless punctuation in the name/description requires the use of them, then you will use ''. For example: name: 'Spessman's Smokes packet'
description: 'A label on the packaging reads, 'Wouldn't a slow death make a change?''
type
> abstract
> parent
> id
> name
> description
> components.
components:
section.components:
- type: Sprite
state:
Not thiscomponents:
- type: Sprite
state:
components:
- type: Sprite # Engine-specific
- type: Physics
- type: Anchorable # Content, but generalized
- type: Emitter # A component for a specific type of item
``
You should always use XAML over UIs defined entirely in C# code.
Extending existing C#-defined UIs is fine, but they should be converted eventually.
Always use iterator methods over creating a new collection and returning it in your method.
Keep in mind, however, that iterator methods allocate a lot of memory.
If you need to reduce allocations as much as possible, use struct iterators.
Your class must be marked as either abstract
, static
, sealed
or [Virtual]
. This is to avoid accidentally making classes inheritable when the shouldn't be and can improve performance slightly when accessing or invoking virtual members.
Use sealed
if the class shouldn't be inherited, [Virtual]
for the normal C# behavior (it mutes the compiler warning), static
for classes that don't need to be instantiated, or abstract
if it's meant for being inherited but not meant to be instantiated by itself.
Where possible you should always have your system run code in response to an event rather than updating every tick. Your code may only take up 0.5% of CPU time but when 100 systems do this it's unnecessary.
When using lambdas or local functions be sure to avoid variable captures.
If you're adding a method that takes in a Func delegate, be sure to have an overload that allows the caller to pass in custom data to it.
void DoSomething(EntityUid otherEntity)
{
// This is BAD. It will allocate on the heap a lot.
var predicate = (EntityUid uid)
=> uid == otherEntity;
// This method doesn't allow us to pass custom data,
// so we're forced to do a costly variable capture.
MethodWithPredicate(predicate);
}
void MethodWithPredicate(Func<EntityUid, bool> predicate)
{
// We do something with the predicate here...
}
void DoSomething(EntityUid otherEntity)
{
// This is good and much more performant than the example before.
var predicate = (EntityUid uid, EntityUid otherUid)
=> uid == otherUid;
// Pass our custom data to this method.
MethodWithPredicate<EntityUid>(predicate, otherEntity);
}
// This method allows you to pass custom data into the predicate.
void MethodWithPredicate<TState>(Func<EntityUid, TState, bool> predicate, TState state)
{
// We do something with the predicate here, making sure to pass "state" to it...
}
Shared types should only be prefixed with Shared
if and only if there are server and/or client inherited types with the same name.
Example:
FooComponent
only exists in shared, it doesn't need a prefix.BarComponent
exists in shared, server and client, the shared type should be prefixed with shared: SharedBarComponent
.PascalCase
is used for IDs and component names.
Everything else, even prototype type names, uses camelCase
.
prefix.Something
should NEVER be used for IDs.
kebab-case
and should never contain capital letters.antag-traitor-user-was-traitor-message = ...
Not thistraitor-message = ...
Use suffix
in prototypes, this it's a spawn-menu-only suffix that allows you to distinguish what prototypes are, without modifying the actual prototype name. You can use it like this:
And results in this:
Always use TransformComponent
anchoring.
You may use PhysicsComponent
static body anchoring but only if you know what you're doing and you can defend your choice over transform anchoring.
Every player-facing string ever needs to be localized.