Recap of 2023 - 2026
Almost four years without a post - but not without work. A recap of what happened to OpenMU since 2022, and of the project I never mentioned here: my fork of the MU Online client sources.
Hey folks, the last post on this blog is from December 2022. That's a long time
to say nothing, and it's not because nothing happened - quite the opposite. In
the meantime OpenMU moved on by three .NET versions, and I started a second
project which I never wrote a single word about here: a fork of the MU Online
client sources.
So, this post is the overdue recap. It's a long one, because there are almost
four years to cover. I'll go into the details of the individual topics in
separate posts.
The big one: we have our own client now
Since I started OpenMU, the client was always the fixed point. It was a binary we couldn't change, so every design decision on the server side had to bend around what the original client expected. In March 2023 I started to work on my fork of the Season 5.2 client sources which were uploaded by Luois, with the goal to clean them up and make them feature complete for Season 6 Episode 3.
To be clear about one thing, because I get asked: this doesn't replace the original client. Full support of the original Season 6 client stays a goal of OpenMU. The fork is an addition, not a substitute.
It connects to OpenMU and it's playable. Here are the parts I consider most interesting:
More than 25 frames per second
This one is older than the rest and it's still the change I'm most fond of.
The original client was never designed to run at more than about 25 fps. The frame rate wasn't just a display property - it was the unit of time of the whole game logic. Every movement, every rotation, every animation step and every effect assumed that one frame equals one fixed slice of time. Run it faster and characters sprint, effects race, water boils.
My approach was FPS_ANIMATION_FACTOR: a factor derived from the actual frame time, by which all these per-frame calculations get scaled. The idea is simple. Applying it was not - it means touching every movement and rotation calculation of every model in the game, and the effects are the really nasty part, because some of them are built from several animations which have to stay in sync with each other. A few thousand lines of code were changed for it, and to this day that factor appears about 3,300 times across 59 files.
All of that was done by hand, years before I let any AI agent near this codebase.
As far as I know this was the first implementation of a higher frame rate for MuMain in the MU Online community, and it was adopted by other forks pretty quickly after that.
Today the client runs with V-Sync and without an fps limit by default, and if V-Sync isn't available it falls back to 60 fps. There are chat commands to hange that ($fps, $vsync) and to look at what you're getting ($fpscounter, $details).
Unicode
The original client is an ANSI code page application from another era. In autumn 2023 I converted it to use UTF-16LE in memory - all strings and char arrays - while strings coming from files and from the network are handled as UTF-8. If you ever wondered why some server names or chat messages look broken in older clients: that's why. It was a huge, boring diff, and it fixed a whole class of problems at once.
The network stack is C# now
Instead of maintaining a second implementation of the protocol in C++, the client uses MUnique.OpenMU.Network - the same library the server uses. It's built as a .NET 10 Native AOT library which the C++ client loads, and the bindings between both worlds are generated by XSLT from the packet definitions, just like the packet structs on the server side (see Generating message structs by data).
The practical effect: when I add or change a packet, I change one XML file and both sides get it. The packet definitions come in as a NuGet package, so client and server are coupled by a version number instead of by a copy-paste.
The protocol isn't standard anymore
Once both ends are under our control, the old limits are just old limits. So the protocol got extended:
- Damage, experience etc. can exceed 16 bit now.
- Item serialization got improved.
- Appearance serialization got improved.
- Monsters show a health status bar after they were attacked.
Rendering: the first steps
The client's renderer was pure fixed-function OpenGL from around 2005. It isn't anymore: bone skinning moved to the GPU instead of transforming vertices on the CPU (which alone was a 15-20x win on the CPU side), the client asks for a Core Profile context, and there's a small render hardware interface and uniform buffer objects behind it now.
Getting rid of the fixed function pipeline is a big step on the way to a renderer that's fully built on vertex and pixel shaders - which is where this should end up. Most of the drawing still works the way it always did, just through a different door.
The optimization work which followed is more tangible: ring-buffer streaming for uniform blocks instead of orphaning a buffer on every update, terrain draw calls collapsed by bucketing tiles per texture pair (~25x fewer draws), and a lot of redundant per-draw state changes removed. Measured on dev hardware, the net result of that series was avg FPS +4.4%, 1% lows +28.0% and frame time -4.1%.
It runs on Linux
In June 2026 the client booted into the login scene, rendered through SDL and OpenGL and connected to a server - on Linux, natively. That was mostly the work of @Mosch0512, and it surfaced exactly the kind of bugs you'd expect from a 20-year-old Win32 codebase: LP64 width bugs when reading binary assets, a double fclose, paths split on backslashes only, and a wchar_t that is 4 bytes wide on Linux while the .NET side decoded UTF-16.
And a lot of smaller things
- A new translation system:
.resxfiles which generate C++ code, replacing
text.bmdand the old JSON translations. - The master skill tree was upgraded to Season 6.
- Inventory and vault extensions.
- The MU Helper UI and logic.
- An auto-reconnect system.
- A DevEditor to tune camera, fog, render distances and debug visualisations
live, without rebuilding. - A window which shows the chat commands the server allows you to use, instead
of expecting you to know them by heart.
Meanwhile, on the server
OpenMU didn't stand still either - 337 pull requests were merged since the beginning of 2023.
Keeping up with .NET. We went to .NET 8 in November 2023, .NET 9 in November 2024 and .NET 10 at the end of 2025. These upgrades are boring by now, which is a good sign.
Tools. The releases v0.8.x and v0.9.0 were mostly tools updates: the network analyzer learned a lot of new packet definitions, got better protocol detection, shows a short summary of packet values instead of raw bytes, and it supports the extended protocol of the new client. The client launcher got fixes and became more compatible with other main.exe versions.
Gameplay. Too much to list completely, but some highlights: experience and Zen loss when a player dies, template accounts, a warning on double-login, improved item storage handling, fixes for pets, jewels, combos and the Force skills - and monsters don't walk through walls anymore.
Castle Siege - work in progress. The biggest feature we're currently working on, and it's nowhere near finished. It's a huge one: the state machine, the registration and mark handling, the NPC lifecycle, the persistence and the Season 6 configuration around it are in place, but there's a lot left to do before it behaves like a real siege. Don't expect to run it on a live server yet.
Bots. OpenMU can now run bots which log in, walk, fight, use skills and go shopping. They're configurable through the admin panel. Originally a testing tool, but they're also quite useful to make an empty server look alive.
Anti-cheat as plugins. Walk and attack speedhack detection, and a configurable limit of concurrent connections per IP - both as plugins you can switch on, off and tune, instead of hardcoded behaviour.
Admin panel. A live log viewer with search, account and character search, and the live map now shows levels and master levels.
Refactoring. The Player class grew over the years into the thing every long-lived codebase has: the class that knows everything. It's being taken apart step by step - movement, persistence, summons and storages are extracted, and map changes and logout are observable through the state machine now.
About AI
I might as well be honest about it, since anyone browsing the repositories will notice the branch names: a considerable part of the work in the last few months was done with AI coding agents.
Both repositories have an AGENTS.md and a docs/CODING_RULES.md which apply to humans and agents alike, and larger changes start with a written plan that gets reviewed before a single line is changed. It works well for the kind of mechanical, wide-reaching work these codebases are full of - extracting classes, or grinding through a renderer regression series with a milestone list. It works a lot less well where the invariants only exist in someone's head, which is why writing them down became part of the workflow.
This deserves its own post, including the parts that didn't work.
Thanks
Both projects live from the people who send pull requests. Since 2023 that's 337 merged PRs in OpenMU and 347 in MuMain, from far more people than I can list here. Thank you - and if you filed one of the issues that made me stop and rethink something: that counts too.
Outlook
The same disclaimer as always applies: this is a hobby project and my time is limited. Still, this is what I'm aiming at:
- Get Castle Siege into a state where you can actually play it.
- Get the client feature complete for Season 6 Episode 3 - while keeping full
support for the original Season 6 client on the server side. - Finish the Player refactoring on the server side.
- Keep going on the client's renderer. Fixed function is gone, a real shader
pipeline is not there yet. - Write more, and shorter, posts here. I have a list, starting with how a .NET
Native AOT library ended up inside a C++ game client.
If you want to follow along or help out, the repositories are
MUnique/OpenMU and
sven-n/MuMain.