In August 2021, I wanted to learn the Rust programming language. For me, the best way to learn a new language or paradigm is by working on an actual project, rather than just following tutorials. At that time, I felt nostalgic for when I played the MMORPG Ragnarok Online.
Ragnarok Online is an MMORPG released in 2002. From the start, there were communities centered around private servers. Emulators were developed that allowed players who couldn’t afford subscriptions to play on unofficial servers.
When I was 14, I spent a lot of time playing on those servers and even worked as a staff member on several private servers, where I implemented scripts. This was my first programming experience.
Back to 2021, as a project to learn Rust, I decided to implement a proxy between a Ragnarok Online client and a private server emulator to decode packets and reverse-engineer the netcode.
This was a perfect project to learn Rust because it involved networking, multi-threading, async code, and byte decoding.
Once I succeeded in implementing this proxy, I decided to extend the scope to include authentication.
Then, I implemented the character creation/selection packet handling.
Next, I wanted to be able to join the game with a character, walk through a map, change maps, and see monsters on the map.
That’s how it all started.
What has been done:
At the beginning of 2022, I faced a dilemma: I wanted to start working on NPCs (non-player characters) and wondered if I should:
I spent weeks on this question, then decided to reuse the script language used by other implementations. The "specifications" of the language are available here.
To interpret this language, I built a parser, compiler, and virtual machine. I defined the grammar using ANTLR, allowing ANTLR to generate the lexer/parser.
The language itself sometimes lacks consistency: almost 99% of the time, function arguments are passed by value, but in rare cases, some arguments are passed by reference, using the same syntax for both. This means the compiler must emit specific opcodes based on the function being called, which doesn’t make much sense.
Overall, the language isn’t too bad, and I learned a lot while building the virtual machine.
It took several months to integrate the VM into rust-ro and interpret the first full script. While not all "native functions" are implemented yet, I do have a few working scripts:
In 2023, I focused on covering every feature with unit tests, which led to a big refactor to make the code testable.
I also started implementing some core gameplay features:
Previously, the threading model was quite simple: each time a request was received, a thread handled it, and if the state was read or mutated, a lock was used. The code became messy and untestable, making it hard to track where the state was mutated, and there were too many locks to manage.
I switched to a new model: state mutations only occur in one place, with a single owner of the state. If another thread needs to mutate the state, it sends a message to a task queue.
Additionally, a game loop is responsible for dequeuing tasks and routing them to the appropriate "service." Services receive a snapshot of the state for the current game loop iteration.
In 2024, I focused on implementing skills and battle mechanisms.
Battle mechanics include systems like:
Ragnarok Online’s battle system is complex, and I wanted to ensure the formulas were implemented correctly. But how can I be sure I got them right? With unit tests!
However, writing correct assertions for these tests would take hours.
In most RPGs, the community includes theory crafters. They reverse-engineer formulas and create tools to optimize character builds.
In Ragnarok Online, tools like RO Calculator exist, which allow to simulate character in action based on equipments, for example it can give how much damage are dealt per attack against a given target. I decided to download the JavaScript from such a site, but the code was almost unreadable due to obfuscation. So, I cleaned it up.
Once the code was clean, I modified it to generate test cases.
I also reworked the UI to integrate this test case generation feature.
In Ragnarok Online, most of character status come from equipment. In existing server implementation, information of bonuses granted by equipments are not directly attached to equipment. Instead there are scripts attached to each equipment.
For example item Glove gives +2 dex, this information is attached to the item with a script bonus bDex,2;
There are more complex script which result can only be evaluated during combat or at runtime, for example Blue Acidus Card if(getrefine()<=4) { bonus bSPrecovRate,5; bonus bMaxSP,80; } else { bonus bMaxSP,40; }
.
So I decided to implement two things:
In both cases I reused the virtual machine I wrote for NPC scripts. It was simply a native method handler to implement.
One of the features that makes Ragnarok Online unique is its variety of skills. However, I wanted the skill system code to be as generic as possible. I spent hours figuring out the right abstractions for skills (and I’m not done yet).
The approach I took was to generate most of the skill-related code from a static configuration file.
To modify such files, I tried several JSON editors and visualizers. Unfortunately, none provided a smooth experience, as I needed features like the ability to compare nested JSON objects and search/replace specific fields.
I ended up building my own JSON table editor.
Returning to skills, I made good progress on Offensive, Supportive, and Passive skills this year.