Contributing code patterns - rebaserHEAD/NexusForever GitHub Wiki

reference, committed verified 2026-10-14 game_rework f9bb26ef

The structural habits of upstream's world-side code: which project a thing goes in, how it's registered, and the shape each kind of class takes. The step-by-step path for one per-character feature is on Adding a player feature; this page is the rulebook behind it. Where an old and a new pattern sit side by side, the tables say which one new code follows. Line-level style is on Contributing code style.

Projects

Layers

Project references run one way, from pure data up to the host. Content scripts sit beside Game, seeing only Game.Abstract and Script.

flowchart TD
  S[Game.Static: enums] --> D[GameTable, Network.World, Database.*]
  D --> A[Game.Abstract: interfaces]
  A --> G[Game and Script]
  G --> W[WorldServer: host]
Loading

What goes where

Thing Goes in
Enums and constants Game.Static/<Area>/, one per file. Network.World/Message/Static is being emptied into it
Interfaces for game objects and managers Game.Abstract, at the same relative path as the class in Game
Message models Network.World/Message/Model/<Area>/
Message handlers WorldServer/Network/Message/Handler/<Area>/
Config classes <Project>/Configuration/Model/<Name>Config.cs
Content scripts Script.<Area>/<Zone>/, such as Script.Alizar or Script.Instance

Game.Abstract is a contract layer, but not a pure one: its interfaces extend the database save interfaces, and some of them return message models.

Package versions live in Source/Directory.Packages.props, and a project file names a package without a version. The maintainer updates them in batches.

Dependency injection

Registering

Every folder with services has its own ServiceCollectionExtensions.cs holding one method, Add<Project><Folder>(this IServiceCollection sc), called from its parent folder's. AddGameMatchingMatch lives in Game/Matching/Match/ and is called from AddGameMatching.

Helper Registers Use it for
AddSingleton, AddTransient The usual New services
AddSingletonLegacy<I, T> The class and its interface as one object A Singleton<T> class, so .Instance and injection share it
AddTransientFactory<I, T> The class and an IFactory<I> Objects made on demand: Resolve(), then Initialise(...)
IFactoryInterface<T> A factory that resolves a concrete type Choosing the type at run time: maps, matches, movement commands
AddRebusHandler A broker message handler Messages from the other services

Old and new

Old New
A global manager Derives Singleton<T>, read through .Instance A plain class registered with AddSingleton, injected where it's used and initialised from HostedService
A per-character manager new XManager(this, model) in Player.Initialise Registered with AddTransient, injected into Player's constructor, then .Initialise(this, model)
A logger NLog's LogManager.GetCurrentClassLogger() in a static field ILogger<T>, injected
Config SharedConfiguration.Instance.Get<T>() IOptions<T>, injected

MatchManager, ChatFormatManager and PublicEventTemplateManager are the new shape of a global manager. CurrencyManager and GuildManager are the new shape of a per-character one.

Important

Command categories are created with Activator.CreateInstance, so they can't take constructor parameters. They reach services through .Instance.

Start-up and shutdown

A global manager is wired into WorldServer/Service/HostedService.cs in up to three places: its Initialise in the start-up list, which is ordered and says so in comments where it matters; its Update in the tick; and its save on shutdown.

Messages

Models

Rule Note
[Message(GameMessageOpcode.X)] on the class No registration; models are found by reflection
Client* implements IReadable, with { get; private set; } Reviewers flag a public setter
Server* implements IWritable, with { get; set; }
Fields in wire order
Every width written out writer.Write(Id, 14u), reader.ReadEnum<Result>(5u)
Lists as List<T> Items { get; } = []; Write the count, then each item
Identity for a character The realm and character id together, not a bare ulong
No logic Beyond choosing which branch of a union to write
Unknown fields as Unknown0, Unknown1, ... UnusedN for a field known to be dead. Renamed when someone works out what it holds

Warning

The enum, ulong, long and double overloads of Write and Read default to 64 bits. Leave the width off an enum and it compiles, then writes 64 bits where the client reads a few, and every field after it lands in the wrong place.

Opcodes

GameMessageOpcode, in Network/Message/, holds every opcode for both the auth and the world server. Values are four hex digits, sorted, with the = aligned. An opcode nobody has identified yet is named after its value, such as Server0635.

Handlers

Rule Note
One class per message, <Message>Handler : IMessageHandler<IWorldSession, <Message>> Found by reflection
Services through the constructor In the dependency injection region
session for the session parameter
An impossible value throws InvalidPacketValueException The session logs it and disconnects the client. Any other exception is logged and swallowed

Upstream doesn't require a handler to hand its work to a manager, and some carry real logic. A thin handler that calls a manager on session.Player is still the easier review.

Broker handlers are a separate path: IHandleMessages<T>, registered with AddRebusHandler and subscribed in NetworkInternalHandlerHostedService. Leaving out either one still compiles.

The world thread

What runs where

Socket threads queue each packet, and the world update drains every session's queue in order, so message handlers run on the update. Maps update in parallel by default; MapConfig.SynchronousUpdate turns that off. Broker handlers run on the broker's threads.

Rules for game code

Rule Note
No async in game code
A database task comes back as an event session.Events.EnqueueEvent(new TaskEvent(task, callback)). The callback runs on the update
Other threads go through SynchroniseAsync On the entity
Timers are UpdateTimer Ticked from Update(double lastTick)
Broker publishes don't wait .FireAndForgetAsync()

Persistence

Save masks

Rule Note
One enum per saved class, nested in it <Type>SaveMask or SaveMask
None, Create, sometimes Delete, then one bit per saved column In hex
Reset to None after loading Otherwise every loaded row is written back

The save body

Title.Save is the model for the create and update paths, stock comments included:

  1. Nothing changed: return.
  2. Create: context.Add the whole model.
  3. Delete, where the class supports it: a model holding only the key, with its state set to EntityState.Deleted.
  4. Otherwise: context.Attach a key-only model, then set IsModified on each column whose bit is set.
  5. Reset the mask.

The interfaces are IDatabaseCharacter, with Save(CharacterContext), and IDatabaseAuth, with Save(AuthContext). IDatabaseState adds PendingCreate and PendingDelete. Saves on the world side run without an explicit transaction.

Databases and migrations

Which database owns what, and how tables and columns are named, is on Databases. Migrations are EF Core code-first, generated by the tools and named yyyyMMddHHmmss_PascalName. A new database's first migration is HelloWorld.

Game content

Game tables

Rule Note
[GameData] on every table the game reads Without it the table never loads
A new table goes on both GameTableManager and IGameTableManager
GetEntry returns null for an unknown id A handler throws InvalidPacketValueException, a command replies and returns, a load from the database throws DatabaseDataException
Text goes to the client as ids The server rarely reads a text table itself

How tables load is on Game tables.

Commands

Rule Note
[Command(Permission.X, "help", "name")] on the category and on each method Methods are Handle<Category><Action>(ICommandContext context, ...)
[Parameter("help")] on each argument, on its own line With ParameterFlags.Optional or a converter type where needed
Subcategories are nested classes
Replies through context.SendMessage SendError exists but is rarely used
Permissions explicitly numbered Permission.cs says so at the top, and lists retired ids to reuse first. Grouped by category, the parent first, then <Category><Action>

A new permission also needs its row in the auth database (Adding a player feature).

Scripts

Rule Note
One map script per zone, <Zone>MapScript.cs
Bound with a filter attribute [ScriptFilterOwnerId(...)] for a world, [ScriptFilterCreatureId(...)] for a creature
Dependencies through the constructor Game.Abstract types only
Ids become a local enum Not bare numbers

Known edges

Patterns in the code not to copy:

  • A few services are registered twice, and the second registration wins.
  • Three folders under Game/Entity spell their file ServiceCollectionExtentions.cs.
  • Some handlers kept their old name when their message was renamed.

Where the code lives

Piece Path
Dependency injection helpers Source/NexusForever.Shared/ServiceCollectionExtensions.cs
Start-up, tick and shutdown wiring Source/NexusForever.WorldServer/Service/HostedService.cs
Opcodes Source/NexusForever.Network/Message/GameMessageOpcode.cs
Packet reader and writer Source/NexusForever.Network/GamePacketReader.cs, GamePacketWriter.cs
The session's packet loop Source/NexusForever.Network/Session/GameSession.cs
Map updates Source/NexusForever.Game/Map/MapManager.cs
The model save Source/NexusForever.Game/Entity/Title.cs
Permissions Source/NexusForever.Game.Static/RBAC/Permission.cs
Broker subscriptions Source/NexusForever.WorldServer/Network/Internal/Handler/NetworkInternalHandlerHostedService.cs
Task events Source/NexusForever.Shared/Game/Events/TaskEvent.cs
⚠️ **GitHub.com Fallback** ⚠️