xrip/uo-client
GitHub: xrip/uo-client
基于 C++17 从零重建的《Ultima Online》T2A 2.0.7 协议客户端,内置 A* 导航 bot 引擎、QuickJS 脚本层和忠实复刻原版的等距渲染器,专注于游戏自动化与逆向工程研究。
Stars: 15 | Forks: 4
# uo-client
一个从零开始使用 C++17 编写的**Ultima Online (T2A, protocol 2.0.7)** 客户端,
作为**自动化 / bot 框架**的引擎构建,配备了一个**重现 1997 年代初代客户端外观的图形
前端**。
它的重点不是为你提供另一种手动*玩* UO 的方式。它的目的是
让 bot 来玩——寻路、跟随、开门、对障碍物做出反应——同时你
在一个忠实的等距视角窗口中**观看整个过程**,并在你想介入时随时手动操作。
这就是经典的 UO *代看* 循环:脚本负责肝,你负责盯着。
这里的脚本是一个真正的协议客户端,而窗口则是原版渲染器的软件
重新实现。
并且这个“脚本”可以是**真正的脚本**:一个内嵌的 JavaScript 引擎允许你
在 C++ 导航核心之上用 JS 编写 bot 的高级行为。内置的脚本——一个
**伐木工 (lumberjack)**——会砍树、把木头存进银行、从商人处
补充消耗品、吃饭、战斗,并能自动逃跑或复活。
## 演示
[](https://www.youtube.com/watch?v=0YYXLrZHQfE)
*A\* bot 在不列颠尼亚进行寻路,同时软件渲染器重现了 1997 年代初期的客户端——点击在 YouTube 上观看。*
## 目录
- [演示](#demo)
- [它是什么 / 它不是什么](#what-it-is--what-it-is-not)
- [构建方式 — LLMs + 逆向工程](#how-it-was-built--llms--reverse-engineering)
- [架构](#architecture)
- [网络与协议](#networking--protocol)
- [移动与导航 bot](#movement--the-navigation-bot)
- [Bot 脚本](#bot-scripting-javascript)
- [渲染器 — 观察前端](#renderer--the-observation-frontend)
- [目标服务器](#the-target-server)
- [要求与游戏资源](#requirements--game-assets)
- [构建](#build)
- [运行与配置](#run--configuration)
- [命令与窗口控制](#commands--window-controls)
- [测试与回归工具](#testing--regression-harnesses)
- [项目结构](#project-layout)
- [状态、限制与路线图](#status-limitations--roadmap)
- [开发者文档](#developer-documentation)
- [许可证与免责声明](#license--disclaimer)
## 它是什么 / 它不是什么
**它是:**
- 一个完整的 **2.0.7 协议客户端**——登录握手、Huffman 压缩游戏
流、数据包组帧、移动、生物、物品、属性、说话/日志。
- 一个 **A\* 导航 bot**,通过预测与协调移动、门处理、动态避障和
跟随逻辑来控制角色。
- 一个 **内嵌的 JavaScript 脚本层**,用于将 bot 编写为优先级
行为(可取消的异步步骤),内置了银行、生存和
商人补给技能——附带了一个完全自主的 **伐木工 (lumberjack)**。
- 一个 **软件等距渲染器**,可绘制地形、静态物、动态物品、
动画生物(包含装备、坐骑、色调和夜间光照)、雷达
小地图和 HUD——直接模仿原版客户端的输出进行建模。
**它不是:**
- 一个可以手动游玩的游戏客户端。虽然存在手动控制(方向键行走、点击移动、
战斗/和平切换),但它们仅作为叠加在 bot 之上的*监督*工具。
- 一个通用的 UO 客户端。它仅实现了目标服务器所使用的协议,
并且仅针对那一个客户端/服务器对进行了验证。
- 一个用于官方正式 shard 的作弊工具。它的目标是私有的、逆向工程的
研究服务器,并且不附带任何游戏资源。
**设计目标,按优先级排列:**
1. **灵活自动化。** 客户端核心 (`Client`) 拥有协议状态,并为
任何更高级别的行为可以驱动的导航层提供支持。
2. **忠实观察。** 前端的外观和行为应该像真正的 2.0.7
客户端一样,这样你就可以像在游戏中照看宏一样监督 bot。
3. **通过构造保持正确。** 行为会不断与
反编译的原版进行对比检查;反编译本身被视为一个活跃的工件。
## 构建方式 — LLMs + 逆向工程
这个项目是一个 **LLM 驱动系统开发**的实验。无论是应用程序代码
**还是**原版客户端的逆向工程,都是**完全通过大型语言模型**完成的——
主要是 **Codex 5.5** 和
**Claude Opus 4.7**。底层没有任何手写的 C++ 基准代码;LLM
阅读反编译的二进制文件,提出假设,编写客户端,并将
结果与运行中的原版进行验证。
保证这种做法真实可信的方法论:
- **反编译的 `client_2.0.7.exe` 是事实来源。** 无论在哪里,只要行为
不明显,实现就会通过地址或符号引用原版。
源代码中充满了诸如 `Network_ProcessBuffer @ 0x42D8E0`
(Huffman / 数据包缓冲)、`CRadarGump_Update` / `CRadarGump_RenderMinimap`
(雷达小地图规则)和 `g_SittingChairTable @ 0x55DB68`(椅子座位)等引用——
上次统计时,**在 10 个源文件中包含了 43 处反编译客户端的引用**。
- **反编译是一个活跃的工件。** 逆向工程在 IDA 中进行,
每一个新确认的行为——以及对反编译器错误猜测的每一次修正——都会作为注释、重命名的函数、类型
和变量写**回 IDB** 中,然后保存。本地的 C++ 源代码永远不会成为
某个 2.0.7 发现的*唯一*记录。
- **持续验证。** 渲染器的更改会在视觉上与官方
客户端进行差异对比(参见 [回归测试工具](#testing--regression-harnesses));
协议和寻路的更改会通过确定性探针进行检查。当
模型对客户端例程的移植偏离了二进制文件的行为时,二进制
文件胜出,代码(以及 IDB)将被修正。
结果是一个其注释兼作逆向工程日志的代码库:
阅读 `src/render/Renderer.cpp` 或 `src/net/Huffman.cpp` 不仅会告诉你代码
做了什么,还会告诉你*它镜像了原版的哪一部分以及为什么*。
## 架构
代码采用 C++17 编写,刻意使用了 **"C with Classes"** 风格:没有
异常,没有 RTTI (`/EHs-c- /GR-`),使用普通结构体,通过
`std::unique_ptr` 进行显式的所有权管理,固定宽度的类型别名(来自
`include/uo/types.h` 的 `u8`/`i32`/`usize`),使用 `PascalCase` 命名类型和 `lowerCamelCase` 命名字段。没有
沉重的模板元编程,热路径中也没有隐藏的内存分配。
```
┌──────────────────────────────────────────┐
│ Client │
│ protocol state machine · packet dispatch │
│ movement · bot commands · render ticks │
└───────┬───────────────┬───────────────┬────┘
│ │ │
┌────────────────┘ │ └─────────────┐
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌────────────────┐
│ net │ │ navigation │ │ render │
│ Socket │ │ PathPlanner │ │ Renderer (iso) │
│ PacketStream │ │ (worker thread)│ │ Minimap/Radar │
│ Huffman │ │ NavigationState │ │ Text / HUD │
└───────────────┘ │ + bot/ (A*, │ │ MiniFBWindow │
┌───────────────┐ │ Blacklist) │ └────────┬───────┘
│ builders │ └────────┬────────┘ │
│ outbound pkts │ │ │
└───────────────┘ ▼ ▼
┌─────────────────────────────────────────┐
│ mul │
│ Map · TileData · World (walkability) │
│ Art · Texmap · Anim · AnimData · Hues │
│ Verdata · RadarColors │
└─────────────────────────────────────────┘
```
| 模块 | 职责 |
|---|---|
| `Client` (`src/Client.{h,cpp}`, `src/client/ClientRender.cpp`) | 连接状态机、数据包分发、玩家/生物/物品缓存、bot 命令接口、每帧渲染驱动。约 1.8k 行代码的分发与胶水逻辑。 |
| `net/` | `Socket` (winsock 包装器)、`PacketStream` (长度表组帧)、`Huffman` (服务器→客户端游戏流解压)。 |
| `builders/` | 出站数据包构建(seed、移动、说话、双击/单击、OpenDoor、登录等)。 |
| `navigation/` | `PathPlanner` 在**后台工作线程**上运行 A\*(请求/轮询);`NavigationState` 保存移动、bot 路线、跟随和已学习阻挡物的状态。 |
| `bot/` | `Pathfinding` (A\* 核心 + 草地权重) 和 `Blacklist` (运行时可行走性叠加层 + `blacklist.mul` verdata I/O)。 |
| `js/` (`src/js/`) | 内嵌的 **QuickJS** 引擎 (`JsEngine`) 以及 `Player` / `World` / `Mobiles` / `Vendor` 脚本绑定 (`ClientBindings`)。运行 `scripts/js/` 下的 bot 脚本。 |
| `mul/` | MUL/verdata 资源加载器和 `World::QueryCell` 可行走性。同时构建 `uo_mul.lib` 和 `uo_mul_dump` CLI。 |
| `render/` | 软件等距 `Renderer` (ARGB1555)、`Minimap`/`RadarColors`、`Text`/HUD,以及 `MiniFBWindow` 宿主。 |
## 网络与协议
使用单个 TCP socket,采用 **"stay-on-socket"** 模型。登录 → 进入游戏的
流程如下:
```
seed → 0x80 → 0xA8 → 0xA0 → 0x8C → (seed) 0x91 → 0xB9 → 0xA9 → 0x5D → 0x1B → 0x55
```
- **无加密。** 服务器在 *nocrypt* 模式下运行;4 字节的明文 seed
只是一个中继令牌(默认的 `0xAC1CA001` 实际上就是服务器 IP)。
- **Huffman 解压** (`src/net/Huffman.*`)。服务器在处理完我们的
`0x91` 游戏登录的那一刻就开始压缩游戏流;从
`0xB9` 开始的所有内容都是压缩的。解码器使用**与服务器压缩时相同的表**构建其字典树(这样两者就不会产生偏差),以 MSB 优先的方式遍历它,并在
flush 标记 `256` 处丢弃当前字节的剩余部分(每个数据包的字节对齐)——与原版客户端的 `Network_ProcessBuffer @ 0x42D8E0` 相匹配。
- **组帧**使用
`include/uo/packet_lengths.h` 中的 `g_PacketLengthTable` 奇偶规则(定长与自描述数据包)。
**已处理的入站数据包**包括:`0x11` 属性、`0x1A` 对象、`0x1B`
登录确认、`0x1C`/`0xAE` ASCII/Unicode 消息、`0x1D` 删除、`0x20`
绘制玩家、`0x21`/`0x22` 移动拒绝/确认、`0x2D` 怪物属性、`0x2E` 装备、
`0x3A` 技能、`0x4E`/`0x4F` 光照等级、`0x55` 登录完成、`0x6E`
动画、`0x72` 战斗模式、`0x73` ping、`0x77`/`0x78` 生物移动/进入视野、
`0x81`/`0xA9` 角色列表、`0x82` 登录拒绝、`0x8C` 连接游戏服务器、
`0x98` 怪物名称、`0xA1`/`0xA2`/`0xA3` 生命/法力/体力、`0xA8` 服务器列表、`0xAF`
死亡、`0xB9` 特性、`0xBD` 版本查询、`0xC8` 视野范围。
物品、容器和商人流程增加了 `0x24` 容器界面、`0x3C` 容器
内容物、`0x74` 商人商店数据、`0x3B` 商人报价、`0x88` 人物面板(NPC 职业称号在客户端中唯一的
可见载体)、`0x2C` 复活菜单和 `0x7C`
服务器菜单/对话框。
**出站构建器** (`src/builders/Builders.cpp`):seed、`0x02` 移动、`0x03`
说话、`0x06` 双击、`0x09` 单击、`0x12` OpenDoor (子命令
`0x58`)、`0x5D` 游玩角色、`0x73` ping、`0x80` 登录、`0x91` 游戏登录、
`0xA0` 选择服务器、`0xBD` 版本。
客户端还会发送 `0x07`/`0x08` (拾起 / 放下)、`0x34` (状态查询,用于怪物
生命值)、`0x3B` (商人购买) 和 `0x98` (所有名称查询),用于命令和脚本所使用的物品、商人和
战斗交互。
每个数据包都会记录到 JSONL 文件和控制台中(受 `verbose`
开关控制,以免窗口被每帧的刷屏信息淹没)。
## 移动与导航 bot
### 预测与协调移动
移动是**流水线化**的 (`kMaxInFlight = 4`,即fastwalk stack"):多个
`0x02` 移动指令可能同时在传输中,每一个都会在本地预测其
新位置/朝向,然后根据服务器的回复进行协调。
- `0x22` **确认不携带位置信息**,但我们永远不需要它——位置是在
本地预测的,并且只会被拒绝指令纠正。流水线操作是安全的,因为 `0x21` 拒绝携带了权威姿态(见下文),并且
服务器没有步频反加速作弊机制(尽管节流阀仍然会控制*发送*速率)。
- `0x21` **拒绝**会将客户端拉回服务器的权威姿态,并
丢弃传输中的队列。被阻挡的步骤会导致服务器拒绝其后的每一个排队
移动(它会保持 MovePrevented 状态,直到我们重新发送 `seq 0`),因此深度为 N 的
流水线会产生 N 个相同的拒绝;只有第一个(其序列号仍在
传输中)会被执行,其余的只是重新同步姿态。那些被推测性
从路径中消耗掉的步骤会被恢复,以便重新规划路线/重试开门时
能从正确的位置恢复。
- `0x20` 是一次完全的重新同步,会中止当前路径。
- **转向后步进:** 朝新方向迈出一步时,首先会转向(经过服务器确认的
`DoTurn`),然后步进;两者都在本地进行预测。
- **节奏:** 标准的步行速度——**跑 200 毫秒 / 步行 400 毫秒** 每步。
服务器没有步长时序反加速作弊机制,因此节奏纯粹是为了
真实感。
- 一个 **5 秒的 watchdog** 会在最早的传输中移动始终未被确认时
中止路径。
### 线程化 A\* 规划器
`navigation::PathPlanner` 在一个**专用工作线程**上运行 A\* 搜索。客户端发布一个 `PathRequest`(起点、目标、黑名单、实时生物、动态物品),随后通过 `Poll()` 获取 `PathResult`——因此漫长的跨大陆搜索永远不会
阻塞渲染循环或网络泵。
搜索本身 (`bot/Pathfinding`) 是基于
`World::QueryCell` 的 8 向连通 A\*:
- 代价 **10** (直行) / **14** (对角线);可采纳的切比雪夫启发式函数。
- **无切角:** 对角线步骤要求两个正交相邻节点均可通行。
- 步长限制 `maxStepUp/Down = 12`,`charHeight = 16`;节点上限 `32768`。
- **草地惩罚**会使路线偏向道路/泥土/鹅卵石(那里的怪物更稀少),
同时保持启发式函数可采纳。
- 在进行 MUL 可行走性检查之后,会查阅**黑名单叠加层**。
### 前瞻修补
Bot 不是每帧都重建整个路线,而是预览现有路径的后续几个
步骤,标记出任何被临时黑名单 / 新出现的生物 (`0x77`/`0x78`) / 阻挡性动态物品 (`0x1A`) /
纯不可行走地形新阻挡的单元格,并尝试在阻挡物周围进行一次小规模的、低成本的 A\* **修补**——将其拼接到路径前缀中并保留尾部。完全重新规划是最后的兜底方案。
### 障碍、门、生物与疲劳处理
在收到 `0x21` 拒绝时,bot 会**按顺序**决定:
0. **疲劳(体力)。** 在收到“*太累了无法移动*”消息后不久发生的拒绝会被
视为体力耗尽——等待恢复并重试,**绝不**
加入黑名单。
1. **格子上有生物。** 被阻挡格子上的缓存生物属于移动/推挤类
障碍物——短暂等待并重试,**绝不**
加入黑名单。
2. **门。** 发送合法的 **OpenDoor 动作** (`0x12`/`0x58`);服务器会在空间中搜索面朝的格子,并打开那里的任何门(与图像和时间无关)。通过随后的 `0x1A` 更新进行确认、重试,并且已知有门的单元格**绝不**会被列入黑名单。
3. **墙壁 / 路灯柱 / 未知静态物。** 只有在这时才会添加**临时**(本次行程)回避并重新规划路线。
存在 `blacklist.mul` 的 I/O (verdata 格式),但**自动持久化已被禁用**——bot 仅使用临时回避,因此不会误伤真实的通道。
## Bot 脚本
在 C++ 导航核心之上是一个内嵌的
**[QuickJS](https://bellard.org/quickjs/)** 引擎 (`src/js/`),因此 bot 的*高级*行为是用 JavaScript 编写的——无需重新编译。编辑脚本后再次输入
`run`,它就会在一个全新的 runtime 中重新加载;脚本错误会被捕获并打印出来 (`[js]`),且永远不会导致客户端崩溃。
```
run scripts\js\lumberjack.js :: load + run a bot script (re-run to reload)
js stop :: tear the running script down
```
**脚本接口** (`src/js/ClientBindings.cpp`):`Player` (实时状态 +
动作——goto、使用、装备、攻击、跟随、说话、丢弃、`requestStatus` 等)、
`World` (`statics`、树桩叠加层)、`Mobiles` (基于 serial 的实时句柄,包含生命值 / 善恶度 / 身体 / 人物面板称号) 和 `Vendor` (由说话触发的
购买)。事件 (`on`/`once`) 暴露了日志行、目标指针、到达、
容器打开、生物进出视野、攻击、对话框、复活菜单、
人物面板和商人窗口。`scripts/js/globals.d.ts` 是整个接口类型的真实事实来源。
**行为运行器** (`scripts/js/lib/bt.js`):一个 bot 是一个按**优先级顺序**排列的
行为扁平列表。每一帧,守卫条件为真的最高优先级行为拥有对身体的控制权,而严格更高优先级的行为可以**抢占**它。抢占是通过一个**取消令牌**协作完成的:长时间的等待被包装起来,因此被抢占的步骤会立即展开(威胁会在一帧之内打断砍伐动作,而不是在 15 秒的砍伐等待之后)。`BehaviorScript` 基类 (`lib/bot.js`) 增加了生命周期(包括等待完成的单次 `onStartup`)、移动 (`walkTo`) 和物品栏功能;可选的 mixin 增加了银行 (`lib/bank.js`) 和生存/补给 (`lib/survival.js`) 功能。一个共享的**威胁计量表** (`lib/threat.js`) 会根据具有攻击性的生物的身体列表以及确认的攻击信号来评估附近的危险程度。
**内置的 bot** —— `scripts/js/lumberjack.js` —— 是一个完整的实战示例:
它会在不同的林地之间轮换砍伐树木,装满后把木头存入银行,提取
金币,从商人那里补充绷带/食物(通过人物面板的*职业*头衔而不是
名字进行匹配),定时进食,并且——在威胁计量表的驱动下——战斗,在战斗中包扎,逃离无法战胜的敌人(轮换到新的林地并避开该
怪物的区域),以及在被杀时走向治疗者进行复活。
完整指南见 **[`BT.md`](BT.md)**。
## 渲染器 — 观察前端
渲染器 (`src/render/Renderer.*`) 是一个**软件等距光栅化器**,它
生成一个 **ARGB1555** 的帧缓冲区(每个像素一个 `u16`),并将其交给
[MiniFB](include/win32/MiniFB.h) 窗口,后者免费提供整数放大。
这是对原版客户端绘制流程的刻意重新实现,而不是一个
通用引擎——投影、绘制顺序、色调处理、光照和小地图
规则都是基于 `client_2.0.7.exe` 建模的。
按照画家算法的顺序,它绘制的内容包括:
- **陆地地形**跨越每个图块的四个角 z 值进行拉伸,采样
`art.mul` 菱形和 `texmaps.mul` 倾斜纹理。
- **静态物**来自地图块,与生物进行 z 排序,使得同 z 轴的生物能够正确地绘制在世界物品之上。
- **动态服务器物品** (`0x1A`) —— 路灯柱、门(带有打开/关闭的
图像偏移)、装饰品 —— 通过 serial 进行键索引。
- **生物** (玩家/NPC) 作为 `anim.mul` 身体动画,带有:
- **装备**以相同的动作/帧叠加在身体之上,
- **色调**来自 `hues.mul` 颜色渐变,
- **步行/跑步节奏**来自 `animinfo.mul`,带有亚图块滑动效果,使得精灵图在
图块之间滑动时与步行周期同步(本地玩家保持在中心,世界在其下方滚动),
- **坐骑与椅子座位** —— 坐骑身体绘制在骑手下方,或者将骑手
移到座椅上(从 `g_SittingChairTable @ 0x55DB68` 移植),
- **战斗 / 死亡**动画状态和战斗模式姿态。
- **动画静态物**通过 `animdata.mul` 实现。
- **程序化夜间光照:** 由世界光照等级 (`0x4E`/`0x4F`) 播种的每像素 RGB 黑暗贴图,并为每个
分类的光源(火/蜡烛为暖色,灯为白色;携带的
火把/提灯会投射出移动的光池)减去平滑的径向日冕。一个 `day [on [gamePort] [gameHost]
```
配置位于 `src/main.cpp` 中的 `Client::Config`。默认值是
**本地占位符**,用于维护者的局域网(主机 `172.28.160.1`,登录
`xrip`/`xrip`),并且可以在命令行上覆盖——请在本地编辑它们或传递
参数,而不是提交你自己的环境。关键字段:
| 字段 | 默认值 | 说明 |
|---|---|---|
| `loginHost` / `loginPort` | `172.28.160.1` / `2593` | 通过 `argv[1..2]` 覆盖 |
| `username` / `password` | `xrip` / `xrip` | 通过 `argv[3..4]` 覆盖 |
| `version` | `2.0.7` | 在 `0xBD` 中报告 |
| `plaintextSeed` | `0xAC1CA001` | = 服务器 IP;nocrypt 中继令牌 |
| `sendSeed` | `true` | 连接时的 4 字节 seed 前缀 |
| `legacyMovePacket` | `false` | demo 协议的移动数据包变体 |
| `enableKeepalive` | `false` | 无客户端 `0x73` 保活 |
| `acceptDoors` | `true` | A\* 路线穿过门,在运行时打开 |
| `enableRenderer` | `true` | 打开世界窗口(`--headless` 禁用) |
| `*Path` (MUL 文件) | `E:/uo/*.mul` | 参见 [资源](#requirements--game-assets) |
| `renderWidth/Height/Scale` | `960×540 ×2` | 帧缓冲区 + 整数放大 |
## 命令与窗口控制
**stdin 命令**(在游戏中时于控制台输入):
| 命令 | 效果 |
|--------------------------------------------|---|
| `goto [z]` | 通往固定坐标的一次性 A* 寻路 |
| `follow [distance]` | 跟随一个生物;仅在距离大于 `distance` 时追逐(默认为 1) |
| `follow off` | 停止跟随 |
| `mobiles` | 查询附近名称 (`0x98`),然后列出 `name serialId` |
| `cast ` | 通过 `0x12`/`0x56` 施放法术(从 1 开始的 id) |
| `skill ` | 通过 `0x12`/`0x24` 使用技能(从 0 开始的 id) |
| `use <0xserial\|type\|'name'> [pack]` | 通过 serial、图像 id 或名称双击物品;搜索范围:背包 → 装备 → 最近的世界物品 |
| `arm\|disarm [weapon\|shield\|both]` | 将武器/盾牌移至背包并装备回来 |
| `pickup ` | 将最近匹配的世界物品 (`0x07`) 拾起放入背包 |
| `drop [z]\|<0xcontainer>` | 将背包物品移至地图格或容器 |
| `equip [pack]` | 穿戴物品(层级来自 tiledata 的 `quality`) |
| `unequip [pack]` | 脱下装备;掉落到世界或背包 |
| `stop` | 中止当前路径 |
| `pos` | 打印玩家位置 |
| `day [on\|off]` | 强制全日光 / 恢复服务器光照等级 |
| `verbose [on\|off]` | 切换每个数据包的控制台刷屏 |
| `target ...` | 设置目标指针 |
| `run ` | 在全新的 runtime 中加载并运行 JS bot 脚本(重新运行即可重载) |
| `js stop` | 停止正在运行的 JS 脚本 |
| *其他任何内容* | 作为 `0x03` ASCII 说话发送 |
**物品目标令牌:** `0x…` ≥ `0x40000000` → serial;较小的 → 图像 id;否则 → tiledata 名称。多词名称需要加引号。
**渲染窗口控制**(在 bot 驱动时进行监督):
| 输入 | 效果 |
|---|---|
| **右键点击** 地图格 | `goto` 该单元格 |
| **方向键** | 手动单步行走(已限速) |
| **M** | 切换雷达小地图面板 |
| **空格键** | 为面朝的格子发送 OpenDoor |
| **TAB** | 切换战斗 / 和平模式 |
| **输入文字** | 进入屏幕上的聊天输入框 |
## 测试与回归工具
专门的探针位于 `tests/` 中,每个探针在 `scripts/` 中都有一个构建/运行脚本:
| 脚本 / 测试 | 检查内容 |
|---|---|
| `scripts\build_hufftest.bat` | Huffman 压缩/解压往返测试 |
| `scripts\build_bltest.bat` | `blacklist.mul` (verdata) 往返测试 |
| `scripts\build_pathprobe.bat ` | 针对真实 MUL 的 A\*(路径长度 / 节点上限行为) |
| `scripts\build_viewer.bat [args]` | 世界查看器探针(仍在渲染) |
| `scripts\build_animprobe.bat` | 动画解码探针 |
两个回归工具用于保护行为关键的路径:
- **`scripts\path_regression.bat`** —— 运行两条漫长的跨大陆路线(Trinsic
桥 ↔ Britain 地下室,双向)并重新生成
`tests\path_regression.txt`。将 `result` / `steps` / `expanded` / `pathCost`
视为**确定性**信号(任何差异都是需要解释的真实行为变化);
`searchUs` 是挂钟时间,仅作为性能趋势来解读。**在对 `src\bot\` 或
`World::QueryCell`/可行走性进行任何更改后运行此工具**,并且仅在
更改是有意为之的情况下才提交基准线。
- **`scripts\render_regression.bat`** —— 仅限 Windows;将 PNG 场景导出至
`build\regression\`,以便与官方 2.0.7 客户端进行视觉对比
(关注 `07_negz_interior.png` 查看负 Z 轴的室内)。**在对渲染器
进行更改后运行此工具。**
## 项目结构
```
src/
Client.{h,cpp} connection state machine, dispatch, movement, bot logic
client/ClientRender.cpp per-tick world drawing + HUD glue
main.cpp hardcoded Config + entry point
Logger.cpp JSONL + console packet log
net/ Socket · PacketStream (framing) · Huffman (decompression)
builders/Builders.cpp outbound packet builders
navigation/ PathPlanner (threaded A*) · NavigationState
bot/ Pathfinding (A* + grass bias) · Blacklist (overlay + I/O)
js/ QuickJS engine (JsEngine) + Player/World/Mobiles/Vendor bindings
mul/ File · TileData · Map · World · Art · Texmap · Anim ·
AnimData · AnimInfo · Hues · Verdata · RadarColors · dump CLI
render/ Renderer (iso) · Minimap · RadarColors · Text · MiniFBWindow
include/
uo/ shared headers (types, packet ids/lengths, mul, ...)
win32/MiniFB.h windowing
tests/ huffman / blacklist / path-probe / viewer / anim probes
scripts/ build + regression batch files
js/ JS bot scripts (lumberjack) + lib/ (behaviour runner + skills)
AGENTS.md · BT.md · ... developer + design documentation (see below)
```
## 状态、限制与路线图
- **C++ 中的战斗只是一个钩子;策略位于 JS 中。** C++ 核心仅在
生命值下降时安全停止路径,并记录威胁。实际的*交战 / 逃跑 /
复活*行为在 JS 层实现(威胁计量表 + 伐木工的 DPS 竞争评估)。**召回** (法术 + 试剂/符文处理) 仍然待实现。
- **道路偏好**使用了最少的草地图块集;根据该 shard 的确切
草地/道路 ID 扩展它,以优化路线规划。
- **`blacklist.mul` 自动持久化已禁用**(只读),以避免破坏
通道 —— bot 仅使用临时回避。
- **未应用 `verdata.mul` 补丁**(目标服务器仅读取其
版本字,因此基础的 MUL 文件已匹配)。
- **仅限地图 0**(不列颠尼亚,768×512 个区块)。
- **单一后端。** 仅支持 [UO Demo server](#the-target-server)。
## 开发者文档
仓库内的几份文档比此 README 探讨得更深入:
- **[`AGENTS.md`](AGENTS.md)** —— 代码库指南:结构、运行时说明、
构建/测试命令、代码风格以及逆向工程工作流
(包括将 IDA 发现写回 IDB 的规则)。
`CLAUDE.md` 是此文件的符号链接。
- **[`bot-client.md`](bot-client.md)** —— 设计与状态说明:协议流程、
移动模型、寻路内部原理、障碍/门/生物/疲劳处理、
完整的可调参数表以及已知限制列表。
- **[`BT.md`](BT.md)** —— bot 脚本指南:优先级行为运行器、
取消令牌、`BehaviorScript` 基类和技能 mixin,包含一个完整的
实战示例。`scripts/js/globals.d.ts` 附带匹配的 TypeScript 类型
(脚本接口的事实来源)。
- **[`JS-BIBLE.md`](JS-BIBLE.md)** —— 更深层的 JS 脚本参考。
## 许可证与免责声明
基于**自定义的非商业 MIT 风格许可证**授权 — © 2026 Ilia
Maslennikov (xrip)。你可以**仅出于非商业目的**使用、修改和分发它,
必须保留版权/许可声明(包括指向
此存储库的链接),未经事先书面许可,不得出售或将其变现(或衍生服务/作品)。确切条款请参见 [`LICENSE`](LICENSE)。
*Ultima Online* 是其各自所有者的商标;这是一个独立的、
教育性的逆向工程项目,**与其没有任何隶属关系或受其认可**。
不包含或分发任何游戏数据文件 (MULs) —— 你必须从合法的安装中提供自己的
文件。此客户端旨在与你自己的
逆向工程 [UO Demo server](https://github.com/draxinar/ouo) 副本配合使用。
|off]` 开关可以强制开启全日光。
叠加在世界帧之上的内容:
- **雷达小地图** (切换 `M`) —— 一个使用与 3D 视图相同投影的等距方向面板,通过真正客户端的
雷达规则(最顶层表面优先)从 `radarcol.mul` 着色,按 8×8 地图块进行缓存,自动缩放以适应
玩家和整个计划路线,带有路线/玩家/目标标记。基于
`CRadarGump_Update` / `CRadarGump_RenderMinimap` 建模。
- **HUD:** 状态栏 (生命/法力/体力)、滚动系统日志 / 日志记录、
头顶文字、聊天输入行,以及鼠标下方的 UO 方向性**行走指针**。
窗口还接受监督输入——参见
[命令与窗口控制](#commands--window-controls)。
## 目标服务器
唯一受支持的后端是逆向工程的 **UO Demo** 服务器:
**[github.com/draxinar/ouo](https://github.com/draxinar/ouo)**。这个客户端完全使用
该服务器的 2.0.7 协议变体(nocrypt,1997 年代的移动
数据包,不需要客户端保活,没有步长时序反加速作弊机制)。
针对其他服务器——官方、ServUO、RunUO 等进行测试——**不是目标**
也没有计划。将客户端指向不同的 shard 是不受支持的,并且
很可能会在握手或组帧层发生中断。
## 要求与游戏资源
- **Windows**(渲染器宿主和构建脚本以 MSVC 为目标;网络使用
winsock)。存在针对 MUL/bot 代码的非 Windows 编译路径,但
渲染器是 Win32 平台的。
- **Visual Studio Build Tools** (MSVC, 32 位) + **Ninja** + **CMake ≥ 3.20**。
- **原版 UO T2A 时代的 MUL 数据文件。** 本仓库不分发任何此类文件——你必须从合法的 Ultima Online 安装中提供它们。相关路径在 `src/main.cpp` 中配置(默认为 `E:/uo/*.mul`):
| 文件 | 用途 |
|---|---|
| `tiledata.mul` | 图块标志(可行走性、表面、门、光源) |
| `map0.mul`, `staidx0.mul`, `statics0.mul` | 地图单元格 + 静态艺术(不列颠尼亚,地图 0) |
| `verdata.mul` *(可选)* | 版本字 / 补丁覆盖(此处为只读) |
| `art.mul`, `artidx.mul` | 陆地 + 静态图块位图 |
| `texmaps.mul`, `texidx.mul` | 倾斜陆地纹理 |
| `anim.mul`, `anim.idx` | 生物身体动画 |
| `animdata.mul` | 动画静态/动态艺术 |
| `animinfo.mul` | 生物行走/奔跑时间 |
| `hues.mul` | 着色物体/生物的颜色渐变 |
| `radarcol.mul` *(可选)* | 每个图块的小地图颜色 |
MUL 文件会在第一次需要它们的导航/渲染时进行懒加载。
## 构建
```
scripts\build.bat
```
这会运行 `vcvars32` → `cmake -G Ninja` → `ninja` 并构建所有目标。输出
位于 `build\` 目录下:
- `build\uo_client.exe` —— 客户端。
- `uo_mul.lib` —— MUL 加载器静态库。
- `uo_mul_dump.exe` —— 一个用于导出图块 / 地图单元格 / 可行走性的 CLI。
手动配置/构建(需已激活 MSVC 环境):
```
cmake -S . -B build -G Ninja
cmake --build build
```
构建说明:
- 标志为 `/W4 /EHs-c- /GR- /utf-8 /permissive-` —— **异常和 RTTI 均
已关闭**,因此来自 STL 头文件的 C4530 警告是预期内的且无害的。
- 如果链接失败并出现 **LNK1168**,说明之前的 `uo_client.exe` 仍在运行
并占用了该文件——请关闭它并重新构建。
## 运行与配置
```
build\uo_client.exe :: use built-in defaults + renderer
build\uo_client.exe --headless :: pure console client, no window
build\uo_client.exe 标签:A*寻路算法, C++17, HTTP头分析, JavaScript引擎, 云资产清单, 内核驱动, 游戏客户端, 网络协议, 自动化机器人, 逆向工程