These rules apply to all code changes in this project. Read before you commit.
The rules apply to both the C++ client and the C# ClientLibrary. Where the
right idiom differs between the two languages, the rule below calls out each
language explicitly.
- A function does one thing. If you can describe it with "and", split it.
- When touching a large function, extract at least one logical block into its own function before you're done. You don't have to refactor everything — just leave it better than you found it.
- If the extracted code doesn't belong in the current file, move it to a new file in an appropriate folder. Over time this breaks apart monolithic files naturally.
- Aim for functions that fit on one screen (~40 lines). Logic-heavy functions get shorter limits.
- Handle error cases and edge conditions at the top of a function.
- Avoid deep nesting. Prefer
if (bad) return;over wrapping the entire body inif (good) { ... }. - Each early exit should have a clear reason — don't silently swallow errors.
- Numbers, strings, paths, sizes, timers, colors — if it's not self-evident, give it a name.
- Use constants, enums, or config values. A magic number buried in logic is a bug waiting to happen.
0and1in boolean/trivial contexts are fine.3.14f,1000,0xFF00FFare not.
- Before writing new logic, check if something similar already exists. Don't duplicate.
- Extract shared logic into utility functions or helpers when two or more callers need it.
- Design parameters to be general where it costs nothing, but don't over-abstract for hypothetical future use.
- Rendering, game logic, UI, data, networking, and input are distinct layers. Don't cross these boundaries.
- UI must not contain game logic. Rendering must not parse network packets. Keep each layer self-contained.
- When touching existing code that mixes concerns, extract toward the correct layer.
- New systems should be composable, self-contained components with clear ownership hierarchies.
- Group related data and behavior together. Don't scatter related logic across unrelated files.
- Name things precisely — if a name needs a comment to explain it, pick a better name.
- Each new class or significant struct/type gets its own file, named after the class.
- C++: header and implementation separated (
.h/.cpp). Small helper structs tightly coupled to a single class may stay in that class's header. - C#: one public type per
.csfile. Small helper types (private nested types, tightly coupledrecords/enums) may stay in the owning class's file.
- Follow the style of the surrounding code. Don't introduce a new convention in one file.
- Use the existing naming conventions for the module you're working in.
- Keep includes minimal — only what the file actually uses.
When extracting free functions from a monolithic file into a new file:
-
Wrap them in a namespace following
<Layer>::<Concern>[::<SubConcern>]. The hierarchy is domain-first: code about skills lives underUI::Skillsregardless of which HUD area renders it; code about combat lives underUI::Combat; etc. Examples (fully-qualified function calls, parentheses show where the function name ends and the namespace ends):UI::Skills::Tooltip::Render(),UI::Items::Tooltip::Render(),GameLogic::Combat::CalculateDamage(). The last::Tooltip/::Combatsegment is a sub-concern namespace; the verb (Render,CalculateDamage) is the function. Don't nest aRendernamespace just to hold aRender()function. -
Drop the legacy
NewUIprefix from the new file name. New extractions use bare names likeSkillTooltip.cpp/SkillTooltip.h, notNewUISkillTooltip.cpp. TheNewUIprefix on existing files is technical debt from a prior UI generation and conveys nothing useful; new files start clean. Existing files keep their names until a separate broader rename.
These conventions apply only to new files containing extracted free
functions. Existing global functions and existing C*-style classes stay
as they are; they would be migrated as part of a future broader modernization,
not piecemeal. Anonymous namespaces for file-private helpers remain unchanged.
The first file using this convention is
src/source/UI/NewUI/HUD/Skills/SkillTooltip.cpp (namespace UI::Skills::Tooltip). Subsequent extractions should follow the same shape so
the pattern accretes consistently.
- Every allocation/resource has a clear owner and a clear cleanup path.
- C++: prefer RAII and smart pointers (
std::unique_ptr,std::shared_ptr) over manualnew/delete. - C#: implement
IDisposablefor types that own unmanaged resources, and consume them withusingstatements/blocks. - Don't leave resources (handles, connections, textures, streams) open on early-exit paths.
- Write code that is obvious to read, even if a "clever" version is shorter.
- Complex expressions get broken into named intermediate variables.
- If a block of code needs a comment to explain what it does, refactor it so it doesn't.
- Every new dependency, abstraction, or pattern has a maintenance cost. Justify it.
- Smaller diffs are easier to review. Keep changes focused on one concern per commit.
- If you're unsure about a structural decision, ask before building on top of it.
- New, changed, or removed systems get a docs update, but only at the usage level: what the feature does and how players or admins use it.
- In scope: gameplay features, admin/editor workflows (e.g. editor usage), UI behaviour, configuration the user touches.
- Out of scope: function signatures, class diagrams, internal data flow. The code is the source of truth for those.
- The one exception is game/business logic and non-obvious calculations - the rules a system enforces (drop rates, EXP curves, combat resolution, trade flow) and the math behind them (damage formulas, collision math). Document the rules and the why of any formula, so a reader understands the behaviour without having to read the code.
- When you change a rule or formula like this (not just edit nearby code), add the docs if they're missing or update them if behaviour shifts. Most of this is currently undocumented, so treat first contact as the chance to capture it.
- Where it helps, note how the original S6 client handled it vs. how we do it now - the contrast is often the most useful part for a future reader.
- Extend an existing file in
docs/for related content rather than starting a new one. A distinct, self-contained system (e.g.camera-system.md,options-window.md) can have its own file. - If it doesn't help a player, an admin, or someone reaching for a formula, don't write it.
- Don't micro-optimize cold paths (startup, one-shot setup, save/load) - clarity wins over saving 1-iteration-out-of-10.
- DO be careful on hot paths. When adding or touching code, ask: is this called per-frame, per-network-packet, per-character-tick, per-entity, or inside another tight loop?
- On hot paths, avoid:
- Heap allocations (
new,std::vector/std::stringconstructed per call) - prefer small stack arrays (keep them small to avoid stack-overflow risk), thread-local or pooled buffers, or caches that only rebuild on state change. Plain file-scopestaticbuffers are fine for known single-threaded paths (e.g. the main render loop) but risky in worker threads - default to thread-local when in doubt. - I/O inside the loop (file reads, registry queries, network calls).
- O(N²) when O(N log N) or O(N) is straightforward.
- String formatting in inner loops - cache or pre-build where possible.
- Heap allocations (
- When existing hot-path code is wasteful on a path you're touching, fix it or file a follow-up. Don't add new abstractions that slow it further without justification.
- "Hot path" is judged by call frequency and N, not by code prominence. A render function called 60×/sec for 50 visible objects is hot. A config reader called once at boot is not.