Moongate v2: Rewriting a Ultima Online Server From Scratch (Because I Wanted To)
![]()
When I was 14, my best friend Marco and I used to go to a place called Area 51, a local LAN gaming center that unfortunately doesn't exist anymore. We'd sit there for hours playing Ultima Online, losing track of time in a world that felt genuinely alive. Later, we kept playing from home over a 56K modem, suffering every packet drop and rubber-banding character like it was completely normal. Because back then, it was.
That was the early 2000s. UO was the MMORPG. Not one of many, the one. A persistent open world with real consequences, a skill-based system with no class locks, player-driven economy, housing, PvP that actually meant something. For its era, it was technically staggering. Thousands of players in a shared world, real-time synchronization, a server architecture that had to handle all of it with hardware that would embarrass a modern Raspberry Pi.
That part, the architecture, always fascinated me. How do you manage thousands of concurrent connections? How do you keep a game world consistent across all clients? How do you build something that feels alive at scale?
Twenty-six years later, the itch came back.
Moongate v1: The First Attempt

About eight months ago I started Moongate, a personal project to build a UO server emulator from scratch using modern .NET. There are already mature emulators out there (ModernUO, RunUO, ServUO, POL) with large communities and years of work behind them. I'm not competing with any of them. I don't care about market share in a game where maybe a thousand people worldwide still play actively. I'm doing this to learn. Full stop.
v1 got me pretty far: character creation, world segmentation into sectors, item pickup, paperdoll, backpack management, a JavaScript scripting engine via Jint, binary persistence, a command system. It worked. Then I took a job at a company doing contract work for the Italian army.
I'll be direct: it was one of the worst professional experiences of my life. The codebase was a catastrophe. Not "legacy code that evolved organically" catastrophe, but "nobody here has ever heard of separation of concerns and nobody cares" catastrophe. The people responsible for reviewing code, the ones sitting at the top of the chain, knew less about software engineering than a kid who just finished their first online tutorial. I'm not exaggerating for effect. I genuinely believe a motivated 13-year-old with six months of YouTube videos would have written better code than what I was expected to maintain, ship and review code.
There's a specific kind of depression that comes from working in that kind of environment long enough. It's not burnout exactly. It's more like craft death: you stop caring about doing things well because doing things well is completely invisible to the people around you. Nobody notices. Nobody rewards it. Nobody even understands what you're talking about when you bring it up. You start to lose the part of yourself that gave a damn.
I dropped Moongate. I dropped most things.
Eventually I left, got my head back, and reopened the project. When I looked at the v1 code again I felt that specific discomfort every developer knows: I know more now than when I wrote this. Not because v1 was bad. Because I'd grown. And the gap was too wide to bridge with a refactor.
So I rewrote everything. Starting from v1 as a reference, but with a cleaner head and better instincts.
What Moongate v2 Is Built On
The stack upgrade starts with .NET 10 and NativeAOT. The server compiles to a native binary with no JIT at runtime. Startup is fast, memory footprint is predictable, and there's no GC warmup period to worry about. For a game server that needs consistent tick timing, that matters.
The scripting engine switched from JavaScript/Jint to Lua via MoonSharp. Less friction, better performance, and more idiomatic for game scripting. Modules are exposed through attributes ([ScriptModule], [ScriptFunction]) and the engine auto-generates .luarc metadata for editor tooling.
The Architecture I Cared About Most
The part I spent the most time getting right in v2 is the event and packet separation. In v1 it was muddled. In v2 it's explicit:
IPacketListenerhandles inbound packets only (Client to Server) and applies domain logic.IGameEventBusServiceis where domain services publish typed events (PlayerConnectedEvent, etc.)IOutboundEventListener<TEvent>reacts to domain events and enqueues outbound packets.IOutgoingPacketQueue/IOutboundPacketSenderdelivers packets at the game-loop/network boundary.
The inbound path and outbound path never directly talk to each other. They go through the domain event bus. This makes the code significantly easier to reason about and test.
Packet registration uses source generation with [PacketHandler(...)] attributes. No runtime reflection tables, no manual registration lists. The source generator builds the packet table at compile time.
The Game Loop
UO servers have a tick-based game loop. The naive implementation is Thread.Sleep(targetMs) between ticks, which drifts badly under load. Moongate v2 uses a timestamp-driven loop instead:
GameLoopServicecomputes elapsed time via a monotonicStopwatchTimerWheelServiceaccumulates elapsed milliseconds and advances only the required number of wheel ticks- Optional idle CPU throttling when there's no work to process
This keeps timer semantics stable regardless of how long individual ticks take. If a tick runs long, the next one compensates. The wheel doesn't drift.
Persistence Without a Database
I deliberately avoided a database. Moongate uses a snapshot + journal model implemented with MemoryPack:
world.snapshot.binis a full world state checkpointworld.journal.binis an append-only log of incremental operations between snapshots- Per-entry checksums in the journal detect truncated or corrupted tails
On startup: load snapshot (if present), replay journal. During runtime: append to journal. On save/stop: write new snapshot, reset journal. It's the same pattern that Redis uses (RDB + AOF), and for a game server with bounded world state it's a solid fit. No query planner overhead, no schema migrations, no connection pool to manage.
Relations between entities (mobile to backpack, item to container, equipped item to mobile) are stored as serial references, not nested objects. This keeps the serialization flat and makes cross-entity queries efficient.
What's Actually Working
As of today, v2 has:
- TCP connection lifecycle and session management
- Packet framing/parsing (fixed and variable sizes)
- Attribute-based packet mapping with source generation
- Inbound message bus (network thread to game loop crossing)
- Domain event bus with initial events
- Outbound event listener abstraction
- Split sessions (transport vs gameplay/protocol context)
- Lua scripting with module binding and
.luarcgeneration - Embedded HTTP host (health, OpenAPI, Scalar UI)
- Snapshot + journal persistence with thread-safe repositories
- Timer wheel with runtime metrics
- Interactive console UI (Spectre.Console, fixed
moongate>prompt) - Unit tests covering packet infrastructure, scripting engine, persistence
Character movement, mobile rendering, item interactions: that's the next sprint.
Why Bother
I get asked this, usually by people who think the right answer is "use ModernUO." And ModernUO is excellent, genuinely impressive work from people who clearly know what they're doing. But that's not the point.
Building Moongate is how I study the internals of MMO servers. It's how I push my understanding of .NET performance, async networking, event-driven architecture, and binary serialization. Every decision I make, and then sometimes unmake, teaches me something I couldn't get from reading a blog post.
It's also, honestly, a way to reclaim something. Programming stopped being fun for a while. Moongate is part of how I got it back. Building something nobody asked for, in a niche so small it's basically invisible, with technology choices made purely out of curiosity. No deadlines, no PRs from people who shouldn't be reviewing code, no "why does this need tests."
Just me, a codebase, and a 26-year-old memory of two kids in a LAN center who had absolutely no idea what they were doing but were having the time of their lives.
Marco would probably find this simultaneously impressive and completely unnecessary.
Which is exactly how the best side projects should feel.
Moongate v2 is open source under GPL-3.0. Source: github.com/moongate-community/moongatev2