Reverse-engineering the 1998 Ultima Online demo server

Reverse-engineering the 1998 Ultima Online demo server

Reverse-engineering the 1998 Ultima Online demo server 逆向工程 1998 年《网络创世纪》(Ultima Online)演示服务器

May 1, 2026 After 10 years of on-and-off work, I’m releasing a full reverse-engineering of the 1998 Ultima Online demo server: https://github.com/draxinar/ouo. About 5,000 functions disassembled from MSVC x86 and translated into portable C99, with each function compared instruction-by-instruction against the binary. UoDemo.exe 2026 年 5 月 1 日。经过 10 年断断续续的工作,我发布了 1998 年《网络创世纪》(Ultima Online,简称 UO)演示服务器的完整逆向工程成果:https://github.com/draxinar/ouo。我从 MSVC x86 二进制文件中反汇编了约 5,000 个函数,并将它们翻译为可移植的 C99 代码,每个函数都与原始二进制文件进行了逐条指令的对比。

For those who don’t know, Ultima Online is a 1997 MMORPG developed by Origin Systems Inc. It was one of the first commercially successful MMORPGs. The client ran on Windows and each server (so called “shards”) ran on multiple Solaris machines (the map was split by regions). The first release of “Ultima Online: The Second Age” expansion (October 1998) shipped with a standalone Ultima Online demo (UoDemo.exe and UoDemo.dat), which bundled a client and a Windows port of the full server code and data. 对于不了解的人,简单介绍一下:《网络创世纪》是 Origin Systems Inc. 于 1997 年开发的一款大型多人在线角色扮演游戏(MMORPG)。它是最早获得商业成功的 MMORPG 之一。其客户端运行在 Windows 上,而每个服务器(即所谓的“碎片/Shard”)则运行在多台 Solaris 机器上(地图按区域划分)。1998 年 10 月发布的《网络创世纪:第二纪元》(Ultima Online: The Second Age)资料片附带了一个独立的演示版(UoDemo.exe 和 UoDemo.dat),其中捆绑了客户端以及服务器代码和数据的 Windows 移植版本。

UoDemo.exe is dated 1998-09-02, and the server data were extracted from the production server on June 2nd, 1998. A handful of features were stubbed for the demo and the playable map was reduced to the island of Ocllo (a small NPC town off the southern coast of Britannia), but the rest is the actual production server code that was running on live UO in mid-1998. The demo featured a simple quest to kill a dragon on the island of Ocllo, with an overview of the basic game mechanisms (speech, trading, combat, etc.). Many UO server emulators reused parts of it, but so far none have ever fully reverse-engineered it. UoDemo.exe was compiled with Microsoft Visual C++ 5.0 (Visual Studio 97), targeting a pre-C++98 dialect of C++. I worked on this project intermittently for 10 years, until recent developments in LLMs finally made it possible to complete this seemingly never-ending task. UoDemo.exe 的日期为 1998 年 9 月 2 日,服务器数据则提取自 1998 年 6 月 2 日的生产服务器。演示版中删减了少量功能,可玩地图也缩减为 Ocllo 岛(不列颠尼亚南部海岸外的一个小型 NPC 城镇),但其余部分均为 1998 年年中在 UO 正式服务器上运行的实际生产代码。该演示版包含一个在 Ocllo 岛上击杀巨龙的简单任务,并展示了基本游戏机制(对话、交易、战斗等)。许多 UO 服务器模拟器曾复用过其中的部分代码,但至今无人将其完全逆向工程。UoDemo.exe 使用 Microsoft Visual C++ 5.0 (Visual Studio 97) 编译,针对的是 C++98 标准之前的 C++ 方言。我断断续续地进行了 10 年的项目开发,直到近期大语言模型(LLM)的发展,才终于让我完成了这项看似永无止境的任务。

Methodology

方法论

Disassembly with radare2. Symbol names deduced from an experimental Linux port of UO client 1.25.37, which was shipped with C++ symbols. Each function is translated by hand into C99, keeping the same control flow, struct layout and branches as the binary. When something differs, it’s either a fix for a real bug in the demo or a platform adaptation, and it’s flagged in the source. To verify, I re-disassemble the C build under r2 and compare it against the original. A function is only marked done once the two match. Helper functions are used only for repeated inline patterns, and only when the helper expands back to the same code as the inline version. 使用 radare2 进行反汇编。符号名称推导自 UO 客户端 1.25.37 的实验性 Linux 移植版,该版本附带了 C++ 符号。每个函数均由人工翻译为 C99 代码,保持与二进制文件相同的控制流、结构体布局和分支。如果存在差异,要么是为了修复演示版中的实际 Bug,要么是为了进行平台适配,并在源码中进行了标记。为了验证,我会在 r2 下重新反汇编生成的 C 构建版本,并将其与原始版本进行对比。只有当两者完全匹配时,函数才会被标记为完成。辅助函数仅用于重复的内联模式,且仅在辅助函数展开后与内联版本代码一致时才使用。

The class hierarchy was the most important thing to get right early on: CEntity (0x10) -> CResourceEntity (0x1C) -> CItem (0x50) -> CContainer (0x5C) -> CMobile (0x37C) -> CPlayer (0x458), with virtual dispatch through vtable slots (vtable[0x18] is IsPlayer, [0xD0] is IsMobile, [0xE4] is IsNPC, etc.). Once those layouts were nailed down, most of the binary was straightforward to translate. The result is an almost perfect replica of a 1998 Ultima Online server, though there are some differences. 类层次结构是早期最需要确定的核心:CEntity (0x10) -> CResourceEntity (0x1C) -> CItem (0x50) -> CContainer (0x5C) -> CMobile (0x37C) -> CPlayer (0x458),并通过 vtable 插槽进行虚函数分发(vtable[0x18] 为 IsPlayer,[0xD0] 为 IsMobile,[0xE4] 为 IsNPC 等)。一旦这些布局确定,大部分二进制文件的翻译就变得直截了当了。最终成果是一个近乎完美的 1998 年《网络创世纪》服务器复刻版,尽管仍存在一些差异。

Findings

研究发现

Compared to the original code, I’ve fixed stability issues (crashes, overflows, uninitialized variables, etc.) and gameplay issues (skill gain, fame/notoriety direction, spawn density, etc.). Each fix is tagged in the source, so anyone diffing against UoDemo.exe can see precisely what changed and why. Some features were broken, like the spawn system and the decay system, probably because they were partially disabled or stubbed out specifically for the demo release: the code is intact but no live call site reaches it. Decompiling them in isolation and re-wiring the dispatch is enough to make them work again. 与原始代码相比,我修复了稳定性问题(崩溃、溢出、未初始化变量等)和游戏性问题(技能获取、声望/恶名走向、刷怪密度等)。每个修复都在源码中进行了标记,因此任何对比 UoDemo.exe 的人都能清楚地看到修改的内容和原因。一些功能(如刷怪系统和衰减系统)在演示版中是损坏的,这可能是因为它们在演示版发布时被部分禁用或存根化:代码虽然完整,但没有实际的调用点。将它们单独反编译并重新连接分发逻辑,就足以让它们恢复工作。

Some data was missing too: for example, the game map only covered the island of Ocllo. I’ve written a full tooling suite to manipulate the server data formats, and fully reconstructed doors, signs, decorations, teleporters, traps, chests and spawn locations for the rest of the world. More surprisingly, the famous retired ecology system was still present in the code, even though the functions were no longer called. I’ve re-wired the predator/prey/scavenger system, which is very cool: you can now see wolves chasing rabbits or crows eating items. However, this doesn’t implement the full resource/production system, because of the lack of precise data. 部分数据也存在缺失:例如,游戏地图仅覆盖了 Ocllo 岛。我编写了一套完整的工具集来处理服务器数据格式,并为世界其他地区完全重建了门、标志、装饰、传送门、陷阱、宝箱和刷怪点。更令人惊讶的是,著名的已废弃生态系统仍然存在于代码中,尽管相关函数已不再被调用。我重新连接了捕食者/猎物/食腐动物系统,这非常酷:你现在可以看到狼追逐兔子,或者乌鸦吃掉物品。不过,由于缺乏精确数据,这并未实现完整的资源/生产系统。

I’ve also added a few straightforward new features, like the Meditation, Stealth and Remove Trap skills, which were added by OSI in February 1999, though some early traces were already present in the code. Most of the new features can be enabled or disabled at startup using the -features parameter. Since the account system was completely absent from the demo server, I’ve re-implemented it by guessing how the original developers would have done it, though slightly modernized. While the demo server only supported client 1.25.33, I’ve added support for all clients from 1.25.30 up to 5.0.9.1 (2007-03-27), with and without encryption. Since there were five completely different encryption mechanisms over the years, I had to reverse-engineer each of them in the client binaries. 我还添加了一些简单的新功能,例如冥想(Meditation)、潜行(Stealth)和拆除陷阱(Remove Trap)技能,这些是 OSI 在 1999 年 2 月添加的,尽管代码中已经存在一些早期痕迹。大多数新功能可以在启动时通过 -features 参数启用或禁用。由于演示服务器完全没有账户系统,我通过猜测原始开发者的实现方式重新编写了它,并进行了轻微的现代化改造。虽然演示服务器最初仅支持 1.25.33 客户端,但我增加了对从 1.25.30 到 5.0.9.1(2007-03-27)所有客户端的支持,包括加密和非加密版本。由于多年来存在五种完全不同的加密机制,我必须在客户端二进制文件中对它们逐一进行逆向工程。

The original binary is 32-bit, but the default build now targets 64-bit. The class hierarchy uses C struct embedding to reproduce the original C++ inheritance, so a CMobile* can still be passed where a CContainer* is expected. Pointer widening on 64-bit would otherwise shift the inherited fields out of place, so some structs are deliberately padded to keep the inheritance and vtable layout matching the binary in both modes. 原始二进制文件是 32 位的,但现在的默认构建目标是 64 位。类层次结构使用 C 结构体嵌入来重现原始的 C++ 继承,因此在需要 CContainer* 的地方仍然可以传入 CMobile*。否则,64 位下的指针扩展会导致继承字段偏移,因此一些结构体被特意填充,以确保继承和 vtable 布局在两种模式下都与原始二进制文件匹配。

链接