The Attribute System, five years later

Five years after the attribute system post: the whole Season 6 skill set went through it, and the core barely changed. A look at what that took.

In April 2020 I claimed on this blog that OpenMU expresses the game's mechanics as data instead of as code, and I called that post the power of the attribute system. It's an easy claim to make while a system is young and the database holds three item options and a handful of relationships.

Since then the complete Season 6 skill set has gone through it, including all six master skill trees. That's enough contact with the actual game to see whether the promise survived it.

A short refresher

For those who don't want to read the old post first, the vocabulary in three sentences. An attribute definition describes a kind of value - "Strength", "Maximum Physical Damage", "Ice Resistance". An attribute relationship says how one value feeds another ("every point of Strength adds 1/4 to maximum damage"), and an aggregate type says how it's applied: added to the raw value, multiplied, or added to the final value. Power-ups from items, buffs and passive skills plug into the same graph.

The important consequence: nothing in the game logic computes a character's damage from a formula. It asks the attribute system for a number, and the number is the result of whatever relationships the configuration happens to contain.

The real test: master skill trees

Master skills are a good stress test, because they're not skills in the usual sense. Most of them modify something that already exists: more damage on a skill you already have, more defense, longer buff duration, and sometimes something structural, like an additional arrow on Triple Shot.

In a hardcoded design, every single one of those is a special case somewhere in the combat code. Here's how the additional arrow works instead. The master skill is declared like this:

this.AddMasterSkillDefinition(
    SkillNumber.TripleShotMastery, /* ... */,
    Stats.ExtraProjectiles, AggregateType.AddRaw);

Higher-level bows grant the same attribute, as a plain item power-up:

item.BasePowerUpAttributes.Add(
    this.CreateItemBasePowerUpDefinition(Stats.ExtraProjectiles, 1, AggregateType.AddRaw));

And the code which actually fires the arrows contains exactly this:

extraProjectiles += (int)player.Attributes![Stats.ExtraProjectiles];

Three places that know nothing about each other: a master skill tree, an item definition, and a skill action. The skill action doesn't know whether the extra arrow comes from the master tree, from the bow, or from something we haven't invented yet - a buff, an event, a server-specific item. Add another source in the configuration and it just works.

There are over 170 master skill definitions in the Season 6 configuration now, and that's how all of them work.

What the system needed to grow

It would be a boring post if I claimed nothing had to change. The core stayed, but it needed a few extensions, and each of them came from a mechanic that couldn't be expressed otherwise:

  • A Maximum aggregate type. Some values must not add up. If you wear three rings with ice resistance, MU doesn't sum them - only the highest one counts. So next to "add to raw", "multiply" and "add to final", there's now "take only the highest value".
  • Relationships whose operand is an attribute. In 2020 a relationship multiplied an input attribute by a constant. Now the operand itself can be an attribute, so "this value scales with that other value" is expressible in data instead of in code. Several master formulas need exactly that.
  • More operators. Multiply and add weren't enough - the official formulas contain exponentiation (in both directions), and minimum and maximum, so those became input operators too.
  • A lot more attributes. Stats holds over 280 attribute definitions today. Among them the seven element resistances, each mapped to its damage bonus counterpart, which is how the jewelry system ended up being pure configuration as well.

That's the honest list. Four extensions in five years, all of them additive, none of them a redesign. Meanwhile the project files themselves - the composable attribute, the elements, the relationship evaluation - are still the ones the 2020 post described, and in the last year they only saw trivial changes.

I'll take that as the promise holding.

The unglamorous half: migrating data

There's a part of the data-driven approach which I underestimated in 2020.

If the game's mechanics are configuration, then adding a mechanic means changing configuration - and the configuration of a running server lives in its database, not in the source code. Shipping a new attribute in Stats.cs does nothing for anyone who already has a server. Their database has to learn about it.

So OpenMU grew a mechanism for that: configuration update plugins. Each one has a version number, a description, a creation date, and an ApplyAsync which does the change against an existing configuration. The elf one, for example, creates the new attribute definitions, adds power-ups to existing magic effects and fixes skill values:

var extraProjectiles = context.CreateNew<AttributeDefinition>(
    Stats.ExtraProjectiles.Id, Stats.ExtraProjectiles.Designation, Stats.ExtraProjectiles.Description);
gameConfiguration.Attributes.Add(extraProjectiles);

There are over 100 of these updates by now. It's the price of the design: data which behaves like code has to be migrated like a schema. If you build something similar, plan for this from day one - it's much less pleasant to add later.

Thanks, ze-dom

The system only proves anything if someone fills it with the actual game, and since September 2024 that someone has mostly been ze-dom. He worked his way from spawn points and jewelry options through the damage and defense calculations right into the master skill trees, each as an update plugin, each with whatever the attribute system was still missing for it.

That was the last big gap in Season 6 feature completeness, and it is exactly the kind of work nobody sees: no new subsystem, no impressive screenshot, just hundreds of small values which have to be right, in a game where the community notices immediately when one of them isn't. Thank you.

Would I build it the same way again?

Yes - with open eyes about the cost.

The cost is indirection. When a damage number is wrong, you don't read a formula; you walk a graph of relationships that four different configuration files contributed to. The tooling for that is better than it was (the admin panel can show you the attributes of a player), but it's still harder than reading twenty lines of arithmetic. And, as above, the configuration needs migrations forever.

The benefit is everything in this post. Six master skill trees, elemental jewelry, pets, buffs, event modifiers and a good number of server-specific customizations went in without touching the core of the system, and mostly without touching the game logic either. Server owners change values in the admin panel that would be a source code change in most other emulators.

Five years in, the attribute system is the OpenMU design decision I'd defend the most.