Repository navigation
Contributing code style
How upstream's C# looks, from the file down to the line. Each rule names its source: written on upstream's code style page, editor from Source/NexusForever.sln.DotSettings, review when reviewers ask for it, or habit when nearly all of the code does it without anything saying so. Which source wins when they disagree is on Contributing.
| Rule | Source | Note |
|---|---|---|
| CRLF line endings | written |
.gitattributes enforces it for *.cs
|
| A newline at the end | written, editor | |
| British English in identifiers, comments and commit messages | written, review |
Initialise, Customisation, Neighbour. Names taken from the client's tables or from .NET keep their own spelling: DyeColorRamp, Serialize
|
| UTF-8, usually with a byte-order mark | habit | |
| Four spaces, no tabs | habit | |
| One top-level type per file, named after it | habit | Nested types are the exception, below |
| About 120 characters a line | habit | A soft limit; nothing enforces it |
| Rule | Source | Note |
|---|---|---|
| Block-scoped namespaces | habit | A handful of files in the newer services are file-scoped |
| The namespace is the project plus the folder path | habit, review | Folder names are singular |
using directives outside the namespace, in one block with no blank lines |
habit | Then one blank line, then namespace
|
System first, then alphabetical |
habit | Aliases (using X = ...;) go last. No global using, no using static
|
| Implicit usings aren't repeated | habit | Most projects switch ImplicitUsings on and leave out using System; and friends. The projects that don't write them out |
| No unused usings | review |
| Rule | Source | Note |
|---|---|---|
Explicit access modifiers, private included |
habit | |
sealed only on Singleton<T> managers |
habit | Other classes are left open |
| Member order: constants and static fields, instance fields (readonly first), properties, constructors, interface members, other members, nested types | editor | The usual drift is a backing field placed straight after its property |
No this. qualifier |
editor | Except where a parameter shares the field's name, as in a constructor: this.log = log;
|
| A constructor initializer on its own line | editor |
: base(movementManager) under the parameter list |
| Attributes on their own line, on parameters too in practice | written, editor | The written rule allows a parameter attribute inline, but every [Parameter] on a command sits on its own line |
Three kinds of type are nested, and little else:
| Nested type | Where it goes |
|---|---|
| The save mask | An enum at the top of the class whose changes it tracks |
| Message sub-structures | Inside the message model that carries them |
| Command subcategories | Inside the command category |
A class with injected services wraps its fields and constructor in a region. The parameters go one per line, even when there's only one, and the assignments are aligned. From ClientChatHandler:
#region Dependency Injection
private readonly ILogger<ClientChatHandler> log;
private readonly IGlobalChatManager globalChatManager;
private readonly ICommandManager commandManager;
public ClientChatHandler(
ILogger<ClientChatHandler> log,
IGlobalChatManager globalChatManager,
ICommandManager commandManager)
{
this.log = log;
this.globalChatManager = globalChatManager;
this.commandManager = commandManager;
}
#endregionA constructor that takes an owner object instead of services, such as a manager built with (IPlayer owner, CharacterModel model), has no region. Regions aren't used for anything else.
| Rule | Source | Note |
|---|---|---|
Explicit types, with var only for new |
habit | The editor setting asks for explicit types outside built-ins. The code goes slightly further: var x = new Thing() is common, and everything else is typed, foreach and out included |
| Block-bodied methods, one-liners included | habit | Expression bodies (=>) are for properties and accessors |
== null and != null |
habit |
is null is rare upstream |
$"..." interpolation |
habit | Never string.Format
|
| Collection expressions for new collections | habit, review | List<uint> Ids { get; } = []; |
switch statements more than switch expressions |
habit | |
| LINQ at load time, loops on the tick | habit | Index building at start-up uses LINQ; Update bodies almost never do |
The Async suffix on new asynchronous methods |
habit |
| Rule | Source |
|---|---|
| Allman braces | written |
No braces around a single-line body of if, else, for or foreach
|
written, editor |
In an if/else, only a branch spanning several lines takes braces |
written |
| An empty method keeps its braces, on separate lines | written |
An auto-property keeps { get; set; } on one line; an accessor with a body puts its braces on separate lines |
written |
| Rule | Source | Note |
|---|---|---|
| A blank line between members | habit | Consecutive auto-properties sit together with none |
A blank line after a one-line if (...) return; |
habit | |
= aligned in a run of assignments |
habit | Object initializers, constructor assignments, enum members. Message Read bodies are the exception |
A wrapped call breaks after (, one argument per line |
habit | |
A fluent chain breaks before the . |
habit |
| Thing | Style | Source | Example |
|---|---|---|---|
| Types, namespaces, methods, properties, public fields | PascalCase | written | |
| Interfaces |
I and PascalCase |
written | IGuildManager |
| Locals and parameters | lowerCamel | written | |
| Private and protected fields, world side | lowerCamel, no prefix | written, editor |
log, entityFactory
|
| Private and protected fields, broker services |
_ and lowerCamel |
habit | The Server.* and API.* projects, and the chat, group, friendship and query database projects |
| The logger field | log |
habit |
_log in the projects above |
The IServiceCollection parameter |
sc |
habit | |
| Ids | unsigned | habit |
uint for most, ulong for character ids and guids, ushort for realm ids |
Where the client has a name for something, in its tables or its UI scripts, reviewers ask for that name over an invented one. Message and handler names put the area first and stay consistent across a family: ServerPetClearFlair, not ServerClearPetFlair. Names for the rest of the server's moving parts are on Contributing code patterns.
| Rule | Source | Note |
|---|---|---|
A /// <summary> on every public method |
written | The code drifts most on message Read and Write, handlers' HandleMessage and constructors. Write one anyway |
| The three-line form, a sentence with a full stop | habit | |
| A verb first | habit | "Returns ...", "Initialise ...", and "Invoked when ..." for On hooks |
<see cref="..."/> for the types a summary names |
habit | |
| The same summary on the interface and the class | habit | The text is copied; <inheritdoc/> isn't used |
<param>, <returns>, <exception> |
written | Optional |
Update methods carry a stock summary, copied from IUpdate: "Invoked each world tick with the delta since the previous tick occurred."
| Rule | Source | Note |
|---|---|---|
// with a space, no closing full stop |
habit | |
// TODO: for a known gap |
habit |
FIXME and HACK are almost never used |
| Evidence goes beside what it explains | habit | In message models: what a capture showed, or which table a value comes from |
| Rule | Source | Example |
|---|---|---|
| Lower-case suffixes | habit |
0u, 1ul, 0.5f
|
| Upper-case hex digits | habit |
0x1A, never 0x1a
|
| Bit widths as unsigned literals | habit | writer.Write(Result, 8u) |
| Rule | Source | Note |
|---|---|---|
| PascalCase names and values | written | |
Values ordered, = aligned |
written | A list kept in another order says so in a comment |
| No comma after the last value | written | |
| Explicit values, in hex for flags | habit | Flags are usually padded: 0x0001
|
The default int base |
habit |
: uint only when a value needs bit 31 |
In Game.Static, one per file |
habit | Under a folder for its area |
| Rule | Source | Note |
|---|---|---|
ILogger<T> injected in new code |
habit | Older classes hold NLog's LogManager.GetCurrentClassLogger() in a static field |
| Interpolated messages on the world side | habit | Structured templates ({Name}) in the broker services and scripts |
| Trace for per-entity events, Info for start-up and load counts, Warn for unexpected data, Error for exceptions | habit | Debug is barely used |
| Situation | What's thrown |
|---|---|
| A client sent a value it couldn't have |
InvalidPacketValueException, with no message. The session logs it and disconnects the client |
| A saved row doesn't match the game tables | DatabaseDataException |
| A caller broke a contract |
InvalidOperationException, ArgumentException, ArgumentOutOfRangeException
|
A message, when there is one, is a sentence with a full stop. catch is rare, and catches Exception by name.
Places where the code doesn't follow its own written rules. Don't copy them:
- Many files in
Game.Abstracthave no newline at the end. - Some enums still carry a comma after the last value.
- About a third of the files under
Network.World/Message/Model/AccountInventoryleave.Modelout of their namespace. - A few files use tabs.
| Piece | Path |
|---|---|
| Editor settings | Source/NexusForever.sln.DotSettings |
| Line endings | .gitattributes |
| The dependency injection region, by example | Source/NexusForever.WorldServer/Network/Message/Handler/Chat/ClientChatHandler.cs |
The stock Update summary |
Source/NexusForever.Shared/IUpdate.cs |
| Upstream's written rules | code style |
Guides
- Running it locally
- Using the web console
- Adding a player feature
- Verifying a feature
- Taking a change upstream
- Syncing with upstream
Explanation
- Server architecture
- Login handshake
- Game tables
- Branches and workflow
- Upstream branches
- Contributing
- Galactic Archive
- Account entitlements
Reference
- Processes and projects
- File formats
- Databases
- Web console
- Galactic Archive messages and storage
- Code style
- Code patterns
- Pull requests
Design
About this wiki