OpenMU

This project aims to create an easy to use, extendable and customizable server for a MMORPG called "MU Online".

View on GitHub

Coding Rules

These rules apply to all code changes in this project, by humans and by AI agents alike. Read before you commit.

They are not a general C# style guide — StyleCop and the surrounding code cover that. They capture the conventions which are easy to get wrong because they involve generated code, persisted data, client compatibility or the flexibility of the configuration, and the ones which come up again and again in code review.


1. Stay in scope

A pull request does one thing. Everything you change should be explainable by the task.

If you notice something else that needs fixing, say so in the pull request instead of fixing it in the same diff.

2. Packet definitions live in the XML

The packet structures, their send extension methods, their markdown documentation and their tests are generated from the XML sources in src/Network/Packets through XSLT.

The same applies to the other generated code in the solution: the persistence model classes (*.Generated.cs under Persistence/BasicModel and Persistence/EntityFramework/Model) and the output of src/SourceGenerators. Change the source, not the output.

Protocol compatibility

The name of a packet tells you which client it has to work with:

So if the open source client needs more than an original packet offers, add or change an …Extended packet — don’t change the original one. The view plugins and packet handlers choose between the variants with [MinimumClient(…)] (rule 8).

See Packet documentation and Packet structure tests.

3. Configuration data, not hard-coded values

OpenMU’s flexibility comes from its configuration. GameLogic should not contain values which are — or could be — configuration data. Work down this ladder and stop at the first rung that works:

  1. Read it from the configuration. GameConfiguration, ItemDefinition, MonsterDefinition, SkillDefinition, GameMapDefinition, CharacterClass, … Stat and damage maths belongs in AttributeRelationships configured in Persistence/Initialization, not in C# branches. Before adding a new Stats attribute, check whether an existing attribute plus a relationship or an ItemDefinition.BasePowerUpAttributes entry already expresses it — those support per-level configuration too. Don’t introduce new stats for one specific item or skill.
  2. Give the plugin its own configuration class, via ISupportCustomConfiguration<T> and ISupportDefaultCustomConfiguration. This makes the values editable in the admin panel instead of baked into the binary. Around 30 plugins already do this — PeriodicSaveProgressPlugIn is a small, clean template.
  3. Only then a named constant — and only in a feature plugin, never in the generic game logic. internal const, named, with an XML doc comment saying why it cannot come from configuration. Put it in a central constants class where one exists (ItemConstants, KalimaConstants, the per-map constants in the map initializers) so the remaining hard-coded values stay trackable.

internal const is the right shape for a fixed value. static readonly is for public “constants” and for things which genuinely cannot be a const.

Where does a new setting belong? Prefer GameConfiguration. Putting it on GameMapDefinition or on a character attribute means extra handling every time a map or a player is added.

Being hard-coded in the game client is not an exemption. It is a reason the server’s value has to match the client, which you note in the doc comment — it is not a reason to hard-code it on the server. The client is a moving target (see rule 9), and configuration may eventually be served to it from here.

A magic number that StyleCop happily accepts is still a magic number.

4. Changing configuration data takes two edits

The data in Persistence/Initialization is only applied to freshly initialized databases. Existing installations get changes through configuration update plugins. So a data change nearly always means both:

  1. the initialization data, so new servers are correct, and
  2. an IConfigurationUpdatePlugIn, so existing servers are fixed.

Doing only one of the two leaves half the users broken. A freshly initialized database marks every update known at that time as already installed, so it will never run your update plugin — the initializer is its only source. An update plugin that happens to cover a case does not excuse leaving the initializer wrong.

When writing the update plugin:

Two related traps:

See Configuration updates for the operator’s view.

5. Persisted identifiers and values are a contract

Anything which ends up stored in a user’s database by value is part of a contract with every existing installation. It can be appended to, never renumbered or reused.

6. The model navigates by references, not by ids

DataModel classes hold real object references. How an object is identified is an implementation detail of the persistence layer.

Guid Id comes from MUnique.OpenMU.Persistence.IIdentifiable and is implemented by the derived model classes — the generated Entity Framework model and the BasicModel (which the in-memory persistence uses) — together with the foreign keys and Raw… properties they need. Compare Character.CurrentMap:

So: don’t add a Guid Id or a Guid <Something>Id to a DataModel class, and don’t look an object up by id in game logic when you already have (or can have) the reference.

The rationale is in the remark on src/Interfaces/Guild.cs:

You may wonder where the Id is. … I could’ve used an integer Id as primary key in the persistent Guild class, but don’t do it by purpose: We would lose the ability to easily merge two databases (realms), if we do that.

The exception is identity which crosses a process or persistence context boundary — the guild server, friend server and the other subsystems behind Interfaces exchange identifiers, not object graphs, so they carry a Guid on purpose. GuildMember.Id, for example, is by design the id of the character it belongs to.

Some of the remaining Guid properties in DataModel are leftovers rather than intentional, and may be cleaned up later; they are not a pattern to copy. CastleSiegePendingReward is one of them — its ItemDefinitionId should be a reference to the ItemDefinition instead.

Don’t confuse either of these with GuildStatus.GuildId: that is the short in-memory guild id used by the guild server and the client protocol, deliberately non-persistent, and unrelated to persistence identity.

7. Respect the layers

The architecture exists so the game logic doesn’t depend on the protocol or the database. See Architecture.

Admin operations and agents

Implement new admin functionality in the UI independent MUnique.OpenMU.Web.AdminPanel.Operations layer; Razor pages and REST controllers call that layer. Expose every operation through an MCP tool with a description, a minimum role and read-only, destructive and idempotent annotations. Link the tool with [ExposesOperation(typeof(Service), nameof(Service.OperationAsync))], or mark the operation [NotExposedToAgents("reason")] when it has no useful agent equivalent, such as a graphical editor. Update the tool in the same change when an operation’s parameters or meaning change. The parity and tool schema tests in MUnique.OpenMU.Web.Tests/Mcp enforce this for operations in the new layer. Existing UI-bound operations are migrated one area at a time; this rule does not require a retroactive rewrite. Phase one exposes only reads: adding write tools also requires the authorization, audit and preview safeguards of issue #1257.

8. Extend through the existing seam

Before adding a special case to shared code, look for the extension point that already exists.

9. Sources for game behaviour: inspiration, not facts

Third-party server sources (zTeamS6.3, emu-server, and similar) were not written by Webzen and routinely contain their authors’ own customizations. Treat them as inspiration, not as facts — they tell you what somebody did, not what the original did. They frequently disagree with each other and with the client.

The same caution applies to the open source client (MuMain): useful for observing behaviour and protocol facts, but it is being actively refactored, and its legacy code hard-codes a great deal without so much as a constant name. It is not a design or style reference for the server.

Document the why of a non-obvious formula next to it, and note where the original behaved differently from what we do.

10. No user-facing literals

Text a player can see is localized. Don’t put English strings in the code.

11. Async and concurrency

12. Style

The project uses StyleCop.Analyzers and the Visual Studio threading analyzers, configured in src/Directory.Build.props and src/.editorconfig. A change should not add warnings. Fix the finding rather than suppressing it; if a suppression is genuinely right, explain why in a comment.

StyleCop already enforces the copyright file header, the using directives after the file-scoped namespace, the this. prefix, XML documentation on exposed members and one type per file — the warnings tell you what to fix. Beyond that:

13. Test it, and document what a user would notice

Run your change before submitting it. For anything non-trivial, say in the pull request what you ran.

Documentation follows the split described in docs/Readme.md: