Every rule in this document is mandatory. No exceptions without documented justification in a code review.
- Explicit > Implicit — we write what we mean; the compiler never has to guess
- Readable > Short — longer but clear code is always preferred over a clever one-liner
- Airy > Compact — code breathes; logical blocks are visually separated
- Consistent > Personal — everyone writes the same way; no personal style
- Explicit Typing —
varIs Forbidden - Braces Are Mandatory Everywhere
- Allman Brace Style
- Spacing and Aeration
- Naming Conventions
- Member Ordering Within a Class
- Explicit Access Modifiers
this/baseQualification- One Public Type Per File
- Usings and Namespaces
- Expressions and Readability
- Documentation
- Expression-Bodied Members
- Null Handling
- Switch Statements
- Regions
- Magic Numbers
- Strings
- Records and Init
- Line and File Length
- Folder Organization
- Absolute Prohibitions Summary
var is forbidden everywhere, without exception. Always declare the full type.
// ❌ FORBIDDEN
var health = 100;
var player = GetPlayer();
var chunks = new Dictionary<ChunkPosition, ChunkData>();
var result = CalculateDamage(attacker, defender);
var items = inventory.GetItems();
// ✅ REQUIRED
int health = 100;
PlayerData player = GetPlayer();
Dictionary<ChunkPosition, ChunkData> chunks = new Dictionary<ChunkPosition, ChunkData>();
DamageResult result = CalculateDamage(attacker, defender);
List<ItemInstance> items = inventory.GetItems();Target-typed new() is allowed only when the type is already declared on the left-hand side:
// ✅ OK — the type is explicit on the left
Dictionary<ChunkPosition, ChunkData> chunks = new();
List<ItemInstance> items = new();
// ❌ FORBIDDEN — var + new()
var chunks = new Dictionary<ChunkPosition, ChunkData>();Why: When reading code, knowing the type immediately prevents guessing, IDE-hovering, and cognitive overhead. The few extra characters are worth the clarity.
Enforced by: .editorconfig rules csharp_style_var_for_built_in_types = false:error, csharp_style_var_when_type_is_apparent = false:error, csharp_style_var_elsewhere = false:error + IDE0007 severity error.
All control structures require braces, even for a single statement. No single-line bodies.
// ❌ FORBIDDEN
if (health <= 0) Die();
if (health <= 0)
Die();
foreach (ChunkPosition position in positions)
LoadChunk(position);
while (queue.Count > 0)
ProcessNext();
// ✅ REQUIRED
if (health <= 0)
{
Die();
}
foreach (ChunkPosition position in positions)
{
LoadChunk(position);
}
while (queue.Count > 0)
{
ProcessNext();
}else, else if, catch, finally each start on their own line:
// ❌ FORBIDDEN
if (condition) {
DoA();
} else {
DoB();
}
// ✅ REQUIRED (Allman style)
if (condition)
{
DoA();
}
else
{
DoB();
}Why: Prevents bugs when adding lines to a block. Visual consistency. No ambiguity.
Enforced by: csharp_prefer_braces = true:error + IDE0011 severity error.
All opening braces go on their own line. No K&R / Egyptian braces.
// ❌ FORBIDDEN (K&R)
public class Player {
public void Attack() {
if (CanAttack()) {
DealDamage();
}
}
}
// ✅ REQUIRED (Allman)
public class Player
{
public void Attack()
{
if (CanAttack())
{
DealDamage();
}
}
}Applies to: classes, structs, enums, interfaces, methods, properties, if/else, for, foreach, while, do-while, switch, try/catch/finally, using, lock, namespace blocks.
Enforced by: csharp_new_line_before_open_brace = all.
// ❌ FORBIDDEN — everything crammed together
public sealed class ChunkManager
{
private readonly Dictionary<ChunkPosition, ChunkData> _loadedChunks = new();
private readonly PriorityQueue<ChunkPosition, float> _loadQueue = new();
private readonly IWorldGenerator _generator;
private readonly IChunkMeshBuilder _meshBuilder;
public ChunkManager(IWorldGenerator generator, IChunkMeshBuilder meshBuilder)
{
_generator = generator;
_meshBuilder = meshBuilder;
}
public void LoadChunk(ChunkPosition position)
{
if (_loadedChunks.ContainsKey(position))
{
return;
}
ChunkData data = _generator.Generate(position);
_loadedChunks.Add(position, data);
}
public void UnloadChunk(ChunkPosition position)
{
_loadedChunks.Remove(position);
}
}
// ✅ REQUIRED — code breathes
public sealed class ChunkManager
{
private readonly Dictionary<ChunkPosition, ChunkData> _loadedChunks = new();
private readonly PriorityQueue<ChunkPosition, float> _loadQueue = new();
private readonly IWorldGenerator _generator;
private readonly IChunkMeshBuilder _meshBuilder;
public ChunkManager(IWorldGenerator generator, IChunkMeshBuilder meshBuilder)
{
_generator = generator;
_meshBuilder = meshBuilder;
}
public void LoadChunk(ChunkPosition position)
{
if (_loadedChunks.ContainsKey(position))
{
return;
}
ChunkData data = _generator.Generate(position);
_loadedChunks.Add(position, data);
}
public void UnloadChunk(ChunkPosition position)
{
_loadedChunks.Remove(position);
}
}Spacing rules:
| Rule | Blank Lines |
|---|---|
| Between each method/property | 1 |
| Between the field block and the first constructor | 1 |
| Between the constructor and the first method | 1 |
| Between distinct logical blocks inside a method | 1 |
After substantial if/for/foreach before the next statement |
1 |
| Between tightly related lines (declare + immediate assign) | 0 |
| Consecutive blank lines | Never (1 max) |
| After an opening brace | Never |
| Before a closing brace | Never |
// ❌ FORBIDDEN
int index=x+z*ChunkSizeX+y*ChunkSliceArea;
if(condition){DoSomething();}
for(int i=0;i<count;i++)
// ✅ REQUIRED
int index = x + z * ChunkSizeX + y * ChunkSliceArea;
if (condition) { DoSomething(); }
for (int i = 0; i < count; i++)| Rule | Example |
|---|---|
| Space before and after all binary operators | a + b, x == y, a && b |
Space after commas and semicolons in for |
for (int i = 0; i < n; i++) |
| Space after keywords | if (, for (, while (, return |
No space after ( or before ) |
Method(arg1, arg2) |
No space before ; |
return value; |
| Element | Convention | Example |
|---|---|---|
| Namespace | PascalCase | MineRPG.World.Generation |
| Class / Struct | PascalCase | ChunkManager, MeshData |
| Interface | I + PascalCase | IMeshBuilder, ITickable |
| Method | PascalCase | GenerateChunk(), CalculateDamage() |
| Async method | PascalCase + Async | GenerateChunkAsync() |
| Public property | PascalCase | Health, MaxStackSize |
| Private field | _camelCase | _loadedChunks, _generator |
| Parameter | camelCase | chunkPosition, blockId |
| Local variable | camelCase | meshData, neighborCount |
| Constant | PascalCase | ChunkSizeX, MaxRenderDistance |
| Static readonly | PascalCase | DefaultConfig, EmptyChunk |
| Enum type | PascalCase (singular) | BlockType, DamageType |
| Enum member | PascalCase | BlockType.Stone, DamageType.Fire |
| [Flags] Enum | PascalCase (plural) | BlockFlags |
| Signal delegate | PascalCase + EventHandler | HealthChangedEventHandler |
| Type parameter | T + PascalCase | TKey, TDefinition |
| Boolean | Prefix is/has/can/should | isLoaded, hasNeighbors, canAttack |
// ❌ FORBIDDEN — cryptic abbreviations
int cnt;
float dmg;
ChunkData cd;
Vector3 pos;
int idx;
bool chk;
// ✅ REQUIRED — full, descriptive names
int count;
float damage;
ChunkData chunkData;
Vector3 position;
int index;
bool isChecked;Tolerated exceptions: i, j, k for for loop indices. x, y, z for coordinates. e for event args.
Always follow this order, separated by blank lines:
1. Constants (const)
2. Static readonly fields
3. Readonly instance fields (_camelCase)
4. Mutable instance fields (_camelCase)
5. Constructor(s)
6. Public properties
7. Public methods
8. Private methods
9. Nested types (avoid if possible)
Within each group, sort by logical relation (not alphabetical) — related methods stay close together.
namespace MineRPG.RPG.Stats;
public sealed class StatContainer
{
// 1. Constants
private const int MaxModifiers = 64;
// 2. Static fields
private static readonly ObjectPool<List<StatModifier>> ListPool = new();
// 3. Readonly instance fields
private readonly StatDefinition _definition;
private readonly List<StatModifier> _modifiers = new();
// 4. Mutable instance fields
private float _baseValue;
private float _cachedFinalValue;
private bool _isDirty = true;
// 5. Constructor
public StatContainer(StatDefinition definition, float baseValue)
{
_definition = definition;
_baseValue = baseValue;
}
// 6. Properties
public float BaseValue => _baseValue;
public float FinalValue => _isDirty ? RecalculateFinalValue() : _cachedFinalValue;
// 7. Public methods
public void AddModifier(StatModifier modifier)
{
_modifiers.Add(modifier);
_isDirty = true;
}
public bool RemoveModifier(StatModifier modifier)
{
bool removed = _modifiers.Remove(modifier);
if (removed)
{
_isDirty = true;
}
return removed;
}
// 8. Private methods
private float RecalculateFinalValue()
{
// ...
}
}// ❌ FORBIDDEN — implicit access
class Player
{
int _health;
void TakeDamage(int amount)
{
_health -= amount;
}
}
// ✅ REQUIRED — everything explicit
public sealed class Player
{
private int _health;
public void TakeDamage(int amount)
{
_health -= amount;
}
}Always write private, public, protected, internal — even when it is the default.
Enforced by: dotnet_style_require_accessibility_modifiers = always:error + IDE0040 severity error.
- Do not use
this.unless required to resolve ambiguity (and in that case, rename the parameter first) - Do not use
base.unless explicitly calling a parent class method
// ❌ FORBIDDEN
this._health = 100;
this.TakeDamage(amount);
// ✅ REQUIRED
_health = 100;
TakeDamage(amount);Enforced by: dotnet_style_qualification_for_* = false:warning.
// ❌ FORBIDDEN — multiple types in one file
// BlockTypes.cs contains BlockDefinition + BlockFlags + BlockRegistry
// ✅ REQUIRED
// BlockDefinition.cs → contains BlockDefinition
// BlockFlags.cs → contains BlockFlags
// BlockRegistry.cs → contains BlockRegistry
The file name must exactly match the type it contains. No catch-all files.
Exception: Types declared inside a class body (syntactically nested) are allowed in their parent's file. They must be private or internal.
// ❌ FORBIDDEN — usings inside namespace, no blank lines
namespace MineRPG.World.Meshing;
using System;
// ✅ REQUIRED — file-scoped namespace, usings at top, blank line after usings
using System;
using System.Collections.Generic;
using System.Threading;
using MineRPG.Core.Events;
using MineRPG.Core.Interfaces;
namespace MineRPG.World.Meshing;Using order:
System.*(sorted alphabetically)- Blank line
- Third-party packages (sorted alphabetically)
- Blank line
- Internal projects
MineRPG.*(sorted alphabetically)
File-scoped namespaces (namespace X;) are mandatory. Block-scoped namespaces are forbidden.
Enforced by: csharp_style_namespace_declarations = file_scoped:error, csharp_using_directive_placement = outside_namespace:error, dotnet_sort_system_directives_first = true, dotnet_separate_import_directive_groups = true.
// ❌ FORBIDDEN
int lod = distance < 8 ? 0 : distance < 16 ? 1 : distance < 24 ? 2 : 3;
// ✅ REQUIRED
int lod;
if (distance < 8)
{
lod = 0;
}
else if (distance < 16)
{
lod = 1;
}
else if (distance < 24)
{
lod = 2;
}
else
{
lod = 3;
}Simple ternary is allowed only for trivial single-line cases:
// ✅ OK — simple ternary
string label = isActive ? "Active" : "Inactive";// ❌ FORBIDDEN in _Process, meshing, generation
int solidCount = blocks.Count(b => b.IsSolid);
List<ChunkPosition> nearby = positions.Where(p => p.Distance(player) < range).ToList();
// ✅ REQUIRED — explicit loop
int solidCount = 0;
for (int i = 0; i < blocks.Length; i++)
{
if (blocks[i].IsSolid)
{
solidCount++;
}
}LINQ is tolerated in initialization code (registry loading, setup) but forbidden in any code called per frame or in tight loops.
// ❌ FORBIDDEN — silently ignoring the return
_ = TryLoadChunk(position);
GetOrCreateChunk(position); // return ignored without explanation
// ✅ REQUIRED — either use the return, or comment why you ignore it
bool isLoaded = TryLoadChunk(position);
// If you truly want to discard: comment explicitly why
// Return intentionally ignored — the chunk will be retrieved via the EventBus
_ = TryLoadChunk(position);// ❌ FORBIDDEN — public method without XML doc
public MeshData BuildMesh(ChunkData chunk, ChunkNeighborData neighbors)
{
// ...
}
// ✅ REQUIRED
/// <summary>
/// Builds an optimized mesh (greedy meshing) for the given chunk.
/// Requires data from the 6 neighboring chunks for border faces.
/// </summary>
/// <param name="chunk">The chunk data to mesh.</param>
/// <param name="neighbors">Data from the 6 adjacent chunks.</param>
/// <returns>Raw mesh data (vertices, normals, UVs, indices, AO colors).</returns>
public MeshData BuildMesh(ChunkData chunk, ChunkNeighborData neighbors)
{
// ...
}/// <summary>required on: classes, interfaces, structs, enums, public methods, public properties/// <param>and/// <returns>required on public methods/// <remarks>for important implementation details- Private methods: a
//comment is sufficient if the logic is not self-evident
// ❌ FORBIDDEN — comment that repeats the code
// Increment the counter
count++;
// ❌ FORBIDDEN — stale or misleading comment
// Check if the chunk is loaded
if (chunk.State == ChunkState.Meshed)
// ✅ REQUIRED — comment that explains WHY, not WHAT
// Re-mesh neighbors because the block change may expose new faces
RemeshNeighborChunks(position);
// Budget of 3 chunks per frame prevents stutters on mid-range configs
if (meshesAppliedThisFrame >= MaxMeshesPerFrame)
{
break;
}Allowed only for simple properties and trivial methods (1 clear expression):
// ✅ OK — simple property
public int Health => _health;
public bool IsAlive => _health > 0;
public string DisplayName => $"{_firstName} {_lastName}";
// ✅ OK — trivial method (1 clear expression)
public float DistanceTo(ChunkPosition other) => MathF.Sqrt(
(_x - other.X) * (_x - other.X) + (_z - other.Z) * (_z - other.Z));
// ❌ FORBIDDEN — too complex for an expression body
public MeshData BuildMesh(ChunkData data) => new MeshData(
GenerateVertices(data),
GenerateNormals(data),
GenerateUVs(data),
GenerateIndices(data),
CalculateAO(data));If it does not fit clearly on 1-2 lines, use a regular method body with braces.
// ❌ FORBIDDEN — silent null-conditional chaining for flow control
chunk?.Mesh?.SetVisible(true);
// ✅ REQUIRED — explicit check with error handling
if (chunk == null)
{
_logger.Warning("Attempted to show null chunk");
return;
}
if (chunk.Mesh == null)
{
_logger.Warning("Chunk at {0} has no mesh", chunk.Position);
return;
}
chunk.Mesh.SetVisible(true);?. is tolerated only for callbacks and events:
// ✅ OK — classic event invocation pattern
OnChunkLoaded?.Invoke(chunkPosition);?? (null coalescing) is allowed for defaults:
// ✅ OK
string biomeName = biome?.Name ?? "Unknown";??= is allowed for lazy initialization:
// ✅ OK
_cachedMesh ??= BuildMesh();Always include a default that throws:
// ❌ FORBIDDEN — switch without default
switch (blockType)
{
case BlockType.Stone:
return 1.5f;
case BlockType.Wood:
return 1.0f;
}
// ✅ REQUIRED — exhaustive default
switch (blockType)
{
case BlockType.Stone:
return 1.5f;
case BlockType.Wood:
return 1.0f;
default:
throw new ArgumentOutOfRangeException(
nameof(blockType), blockType, "Unhandled block type");
}
// ✅ OK — switch expression (for simple mappings)
float hardness = blockType switch
{
BlockType.Stone => 1.5f,
BlockType.Wood => 1.0f,
_ => throw new ArgumentOutOfRangeException(
nameof(blockType), blockType, "Unhandled block type"),
};Forbidden. If a class needs #region to be readable, it is too large. Split it.
// ❌ FORBIDDEN
#region Fields
// ...
#endregion
#region Methods
// ...
#endregion// ❌ FORBIDDEN
int index = x + z * 16 + y * 256;
if (lightLevel < 4)
// ✅ REQUIRED — named constants
private const int ChunkSizeX = 16;
private const int ChunkSizeZ = 16;
private const int ChunkSliceArea = ChunkSizeX * ChunkSizeZ;
private const int MinHostileLightLevel = 4;
int index = x + z * ChunkSizeX + y * ChunkSliceArea;
if (lightLevel < MinHostileLightLevel)// ❌ FORBIDDEN — concatenation with +
string message = "Chunk at " + position.X + ", " + position.Z + " loaded in " + elapsed + "ms";
// ✅ REQUIRED — interpolation
string message = $"Chunk at {position.X}, {position.Z} loaded in {elapsed}ms";In hot paths — interpolation allocates. Use structured logger parameters:
// ✅ In hot paths — no allocation if log level is disabled
_logger.Debug("Chunk {0},{1} meshed in {2}ms", position.X, position.Z, elapsed);Enforced by: dotnet_style_prefer_interpolated_string = true:warning.
record struct is encouraged for small immutable data:
// ✅ ENCOURAGED for small immutable data
public readonly record struct ChunkPosition(int X, int Z);
public readonly record struct BlockPosition(int X, int Y, int Z);
public readonly record struct DamageResult(float Amount, DamageType Type, bool IsCritical);Use sealed record for DTOs that are NOT EventBus events. EventBus events must remain struct (required by IEventBus where T : struct).
// ✅ sealed record for DTOs, save data, config objects
public sealed record HitResult(
int Damage,
bool IsCritical,
DamageType Type,
int SourceEntityId);
// ✅ readonly struct for EventBus events
public readonly struct BlockMinedEvent
{
public WorldPosition Position { get; init; }
public ushort BlockId { get; init; }
public int PlayerId { get; init; }
}| Limit | Soft | Hard |
|---|---|---|
| Line length | 120 characters | 140 characters |
| File length | — | 300 lines |
| Method length | — | 40 lines |
If a method signature or call exceeds the limit, break cleanly:
// ✅ Clean line break
public MeshData BuildMesh(
ChunkData chunkData,
ChunkNeighborData neighbors,
int lodLevel,
CancellationToken cancellationToken)
{
// ...
}If a file exceeds 300 lines, question whether the class has too many responsibilities. If a method exceeds 40 lines, extract sub-methods.
Every directory must be organized by domain (what the code does for the game), not by technical pattern (Service, Provider, Controller). Files in the same folder must share a common responsibility.
// ❌ FORBIDDEN — grouping by technical pattern
Services/
├── BlockInteractionService.cs (gameplay)
├── DebugDataProvider.cs (debug)
├── HotbarController.cs (gameplay)
├── OptionsProvider.cs (settings)
├── JsonSettingsRepository.cs (settings)
└── KeybindApplicator.cs (settings)
// ✅ REQUIRED — grouping by domain
Gameplay/
├── BlockInteractionService.cs
└── HotbarController.cs
Settings/
├── OptionsProvider.cs
├── JsonSettingsRepository.cs
└── KeybindApplicator.cs
Debug/
└── DebugDataProvider.cs
Create a subfolder when files in the same directory serve distinct responsibilities. The threshold is not a fixed file count — it is a question of cohesion:
- If two files address the same concern (e.g.,
ChunkSerializer+PaletteCompressorboth handle serialization), they belong in the same subfolder - If files address unrelated concerns (e.g.,
ChunkNodefor rendering vsFileChunkStoragefor persistence), they must be separated - A single file in a subfolder is acceptable if it clearly belongs to a distinct domain (e.g.,
Debug/DebugDataProvider.cs)
When files move to a subfolder, the namespace must change to match:
// File: src/MineRPG.Core/Events/Definitions/GamePausedEvent.cs
namespace MineRPG.Core.Events.Definitions;
// File: Bootstrap/Settings/OptionsProvider.cs
namespace MineRPG.Game.Bootstrap.Settings;
// File: src/MineRPG.Godot.World/Chunks/ChunkNode.cs
namespace MineRPG.Godot.World.Chunks;A project or directory root may contain files that represent the primary entry point or cross-cutting concerns:
MineRPG.Godot.World/
├── WorldNode.cs ← root entry point (OK)
├── Chunks/ ← chunk management domain
├── Rendering/ ← rendering domain
├── Storage/ ← persistence domain
└── Pipeline/ ← worker pipeline domain
Avoid dumping all files at the root. If the root accumulates files that serve different responsibilities, create subfolders immediately.
| Pattern | Why It's Bad | What to Do Instead |
|---|---|---|
Services/ catch-all folder |
Mixes unrelated concerns under a meaningless label | Group by domain: Settings/, Gameplay/, Debug/ |
Helpers/ or Utils/ folder |
Attracts unrelated code, grows indefinitely | Place utilities next to the code they help |
| All files at project root | No visual cue about responsibilities | Create domain subfolders |
| Deep empty folder chains | A/B/C/OnlyFile.cs adds noise |
Flatten if intermediate folders have no siblings |
| Prohibited | Reason |
|---|---|
var |
Implicit typing — we want to know the type immediately |
if/for/while without braces |
Bug risk, visual inconsistency |
| K&R braces (opening brace at end of line) | We use Allman exclusively |
#region |
Symptom of a God class |
| Nested ternary | Unreadable |
| LINQ in hot paths | Hidden allocations |
| Magic numbers | Incomprehensible without context |
Cryptic abbreviations (cnt, dmg, pos) |
Full names required |
GD.Print() in production |
Centralized logger only |
| Implicit access modifier | Everything is explicit |
| Multiple types per file | 1 file = 1 type |
?. chaining for flow control |
Explicit checks with error handling |
switch without default |
Always exhaustive |
String concatenation with + |
Interpolation $"" required |
Console.WriteLine() |
Centralized logger only |
System.Diagnostics.Debug.WriteLine() |
Centralized logger only |
GetNode<T>() with hardcoded paths |
Use [Export] or injection |
this. qualification (unless ambiguous) |
Unnecessary noise |
| 2+ consecutive blank lines | 1 max |
Blank line after { or before } |
Never |
| Flat file dump at project/folder root | Group by domain, not by pattern |
Services/, Helpers/, Utils/ catch-all folders |
Meaningless labels — use domain-specific names |
The .editorconfig at the solution root encodes all rules above with error or warning severity. Combined with <TreatWarningsAsErrors>true</TreatWarningsAsErrors> in Directory.Build.props, warnings are promoted to build errors.
Key analyzer rules:
| Rule ID | Description | Severity |
|---|---|---|
IDE0007 |
Use explicit type instead of var |
error |
IDE0011 |
Add braces | error |
IDE0040 |
Add accessibility modifiers | error |
IDE0055 |
Formatting violations | warning (→ error) |
IDE0161 |
Use file-scoped namespace | error |
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest-All</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>All .NET analyzers are enabled at the highest analysis level. Code style rules are enforced during build, not just in the IDE.