A from-scratch Ultima Online (T2A, protocol 2.0.7) client written in C++17, built as the engine for an automation / bot framework with a graphical frontend that recreates the look of the original 1997-era client.
The point is not to give you another way to play UO by hand. The point is to let a bot play — pathfind, follow, open doors, react to obstacles — while you watch it happen in a faithful isometric window and step in when you want to. This is the classic UO babysitting loop: the script grinds, you keep an eye on it. Here the script is a real protocol client and the window is a software reimplementation of the original renderer.
And the "script" can be a real script: an embedded JavaScript engine lets you write the bot's high-level behaviour in JS on top of the C++ navigation core. The one that ships — a lumberjack — chops trees, banks the logs, restocks consumables from vendors, eats, and fights, flees or resurrects on its own.
The A* bot pathfinding across Britannia while the software renderer recreates the 1997-era client — click to watch on YouTube.
Scope. This client talks to one server: the reverse-engineered UO Demo shard at draxinar/ouo. Compatibility with other shards (OSI, modern emulators) is out of scope and not planned.
- Demo
- What it is / what it is not
- How it was built — LLMs + reverse engineering
- Architecture
- Networking & protocol
- Movement & the navigation 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
It is:
- A complete 2.0.7 protocol client — login handshake, Huffman-compressed game stream, packet framing, movement, mobiles, items, stats, speech/journal.
- An A* navigation bot that drives a character with predict-and-reconcile movement, door handling, dynamic obstacle avoidance, and follow logic.
- An embedded JavaScript scripting layer for writing bots as priority behaviours (cancellable async steps) with batteries-included banking, survival and vendor-restock skills — shipped with a full autonomous lumberjack.
- A software isometric renderer that draws land, statics, dynamic items, animated mobiles (with equipment, mounts, hues and night lighting), a radar minimap and a HUD — modelled directly on the original client's output.
It is not:
- A hand-playable game client. Manual controls exist (arrow-key walk, click-to-go, war/peace toggle) but only as supervision tools layered over the bot.
- A general-purpose UO client. It implements exactly what the target server speaks and is verified against that one client/server pair.
- A cheat for live/official shards. It targets a private, reverse-engineered research server and ships no game assets.
Design goals, in priority order:
- Automate flexibly. The client core (
Client) owns protocol state and feeds a navigation layer that any higher-level behaviour can drive. - Observe faithfully. The frontend should look and behave like the real 2.0.7 client so you can supervise the bot the way you'd babysit a macro in-game.
- Stay correct by construction. Behaviour is continuously checked against the decompiled original; the decompilation itself is treated as a living artifact.
This project is an experiment in LLM-driven systems development. Both the application code and the reverse-engineering of the original client are done exclusively through large language models — primarily Codex 5.5 and Claude Opus 4.7. There is no hand-written C++ baseline underneath; the LLMs read the decompiled binary, form hypotheses, write the client, and verify the result against the running original.
The methodology that keeps this honest:
- The decompiled
client_2.0.7.exeis the source of truth. Wherever behaviour is non-obvious, the implementation cites the original by address or symbol. The source is dense with references such asNetwork_ProcessBuffer @ 0x42D8E0(Huffman / packet buffering),CRadarGump_Update/CRadarGump_RenderMinimap(the radar minimap rule), andg_SittingChairTable @ 0x55DB68(chair seating) — 43 decompiled-client references across 10 source files at last count. - The decompilation is a living artifact. Reverse-engineering happens in IDA, and every newly confirmed behaviour — plus every correction to a bad decompiler guess — is written back into the IDB as comments, renamed functions, types and variables, then saved. The local C++ source is never allowed to become the only record of a 2.0.7 finding.
- Continuous verification. Renderer changes are diffed against the official client visually (see regression harnesses); protocol and pathfinding changes are checked against deterministic probes. When the model's port of a client routine diverges from the binary's behaviour, the binary wins and the code (and the IDB) are corrected.
The result is a codebase whose comments double as a reverse-engineering logbook:
reading src/render/Renderer.cpp or src/net/Huffman.cpp tells you not just what
the code does, but which part of the original it mirrors and why.
The code is C++17 written in a deliberate "C with Classes" style: no
exceptions, no RTTI (/EHs-c- /GR-), plain structs, explicit ownership via
std::unique_ptr, fixed-width type aliases (u8/i32/usize from
include/uo/types.h), PascalCase types and lowerCamelCase fields. There is no
heavy template metaprogramming and no hidden allocation in the hot paths.
┌──────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────┘
| Module | Responsibility |
|---|---|
Client (src/Client.{h,cpp}, src/client/ClientRender.cpp) |
Connection state machine, packet dispatch, player/mobile/item caches, bot command surface, per-tick render driver. ~1.8k LOC of dispatch + glue. |
net/ |
Socket (winsock wrapper), PacketStream (length-table framing), Huffman (server→client game-stream decompression). |
builders/ |
Outbound packet construction (seed, move, speech, double/single click, OpenDoor, login, etc.). |
navigation/ |
PathPlanner runs A* on a background worker thread (request/poll); NavigationState holds movement, bot route, follow and learned-blocker state. |
bot/ |
Pathfinding (the A* core + grass bias) and Blacklist (runtime walkability overlay + blacklist.mul verdata I/O). |
js/ (src/js/) |
Embedded QuickJS engine (JsEngine) plus Player / World / Mobiles / Vendor script bindings (ClientBindings). Runs the bot scripts under scripts/js/. |
mul/ |
MUL/verdata asset loaders and World::QueryCell walkability. Also builds uo_mul.lib and the uo_mul_dump CLI. |
render/ |
Software isometric Renderer (ARGB1555), Minimap/RadarColors, Text/HUD, and the MiniFBWindow host. |
A single TCP socket using the "stay-on-socket" model. The login → in-world flow is:
seed → 0x80 → 0xA8 → 0xA0 → 0x8C → (seed) 0x91 → 0xB9 → 0xA9 → 0x5D → 0x1B → 0x55
- No encryption. The server runs in nocrypt mode; the 4-byte plaintext seed
is just a relay token (the default
0xAC1CA001is literally the server IP). - Huffman decompression (
src/net/Huffman.*). The server begins compressing the game stream the moment it processes our0x91game-login; everything from0xB9onward is compressed. The decoder builds its trie from the same table the server compresses with (so the two can't drift), walks it MSB-first, and on the flush marker256discards the rest of the current byte (per-packet byte alignment) — matching the original client'sNetwork_ProcessBuffer @ 0x42D8E0. - Framing uses the
g_PacketLengthTableparity rules ininclude/uo/packet_lengths.h(fixed-length vs. self-describing packets).
Handled inbound packets include: 0x11 stats, 0x1A object, 0x1B
login-confirm, 0x1C/0xAE ASCII/Unicode messages, 0x1D delete, 0x20
draw-player, 0x21/0x22 move reject/ack, 0x2D mob attributes, 0x2E equip,
0x3A skills, 0x4E/0x4F light levels, 0x55 login-complete, 0x6E
animation, 0x72 war-mode, 0x73 ping, 0x77/0x78 mobile move/incoming,
0x81/0xA9 char lists, 0x82 login-denied, 0x8C connect-to-gameserver,
0x98 mob name, 0xA1/0xA2/0xA3 HP/mana/stamina, 0xA8 server-list, 0xAF
death, 0xB9 features, 0xBD version-query, 0xC8 view-range.
Item, container and vendor flows add 0x24 container gump, 0x3C container
contents, 0x74 vendor shop data, 0x3B vendor offer, 0x88 paperdoll (the only
client-visible carrier of an NPC's job title), 0x2C resurrect menu and 0x7C
server menu/dialog.
Outbound builders (src/builders/Builders.cpp): seed, 0x02 move, 0x03
speech, 0x06 double-click, 0x09 single-click, 0x12 OpenDoor (subcommand
0x58), 0x5D play-character, 0x73 ping, 0x80 login, 0x91 game-login,
0xA0 select-server, 0xBD version.
The client also sends 0x07/0x08 (lift / drop), 0x34 (status query, for mob
HP), 0x3B (vendor buy) and 0x98 (all-names query) for the item, vendor and
combat interactions used by the commands and scripts.
Every packet is logged to a JSONL file and to the console (gated by a verbose
toggle so the window isn't drowned in per-tick chatter).
Movement is pipelined (kMaxInFlight = 4, the "fastwalk stack"): several
0x02 moves may be in flight at once, each predicting its new position/facing
locally, then reconciled on the server's replies.
- The
0x22ack carries no position, but we never need it — position is predicted locally and only ever corrected by a reject. Pipelining is safe because the0x21reject carries the authoritative pose (see below) and the server has no step-rate anti-speedhack (the throttle still paces sends). 0x21reject snaps the client back to the server's authoritative pose and drops the in-flight queue. A blocked step makes the server deny every queued move behind it (it holds MovePrevented until we resendseq 0), so a depth-N pipeline yields N identical rejects; only the first (whose seq is still in-flight) is acted on, the rest just resync the pose. Steps that were speculatively consumed from the path are restored so a reroute/door-retry resumes from the right spot.0x20is a full resync that aborts the current path.- Turn-then-step: stepping a new direction first turns (an acked server
DoTurn) and then steps; both are predicted locally. - Cadence: canonical foot speeds — run 200 ms / walk 400 ms per step. The server has no step-timing anti-speedhack, so pacing is purely for realism.
- A 5 s watchdog aborts the path if the oldest in-flight move is never acked.
navigation::PathPlanner runs the A* search on a dedicated worker thread. The
client posts a PathRequest (start, goal, blacklist, live mobiles, dynamic items)
and later Poll()s for a PathResult — so a long cross-continent search never
stalls the render loop or the network pump.
The search itself (bot/Pathfinding) is 8-connected A* over
World::QueryCell:
- Costs 10 (straight) / 14 (diagonal); admissible Chebyshev heuristic.
- No corner-cutting: a diagonal step requires both orthogonal neighbours walkable.
- Step limits
maxStepUp/Down = 12,charHeight = 16; node cap32768. - Grass penalty biases routes onto roads/dirt/cobble (where mobs are sparser) while keeping the heuristic admissible.
- A blacklist overlay is consulted after the MUL walkability checks.
Rather than rebuild the whole route every tick, the bot previews the next few
steps of the existing path, flags any cell newly blocked by the transient
blacklist / a fresh mobile (0x77/0x78) / a blocking dynamic item (0x1A) /
plain unwalkable terrain, and tries a small, cheap A* patch around the
blockage — splicing it into the path prefix and keeping the tail. Full replanning
is the fallback.
On a 0x21 reject the bot decides, in order:
- Fatigue (stamina). A reject shortly after a "too fatigued to move" message is treated as spent stamina — wait for regen and retry, never blacklist.
- Mobile on the tile. A cached mobile on the blocked cell is a moving/shove obstacle — wait briefly and retry, never blacklist.
- Door. Send the legitimate OpenDoor action (
0x12/0x58); the server spatially searches the faced tile and opens any door there (graphic- and timing-independent). Confirm via the resulting0x1Aupdate, retry, and a cell with a known door is never blacklisted. - Wall / lamp post / unknown static. Only now add a transient (this-trip) avoid and reroute.
blacklist.mul I/O exists (verdata format) but auto-persist is disabled —
the bot uses transient avoidance only, so it can't poison real passages.
Above the C++ navigation core sits an embedded
QuickJS engine (src/js/), so the bot's
high-level behaviour is written in JavaScript — no recompile. Edit a script, type
run again, and it reloads in a fresh runtime; script errors are caught and
printed ([js]) and never crash the client.
run scripts\js\lumberjack.js :: load + run a bot script (re-run to reload)
js stop :: tear the running script down
Scripting surface (src/js/ClientBindings.cpp): Player (live state +
actions — goto, use, equip, attack, follow, say, drop, requestStatus, …),
World (statics, the stump overlay), Mobiles (live serial-backed handles
carrying HP / notoriety / body / paperdoll title), and Vendor (speech-triggered
buying). Events (on/once) surface journal lines, target cursors, arrivals,
container opens, incoming/leaving mobiles, attacks, dialogs, resurrect menus,
paperdolls and vendor windows. scripts/js/globals.d.ts is the typed source of
truth for the whole surface.
The behaviour runner (scripts/js/lib/bt.js): a bot is a flat list of
behaviours in priority order. Each tick the highest-priority behaviour whose
guard is true owns the body, and a strictly higher-priority one preempts it.
Preemption is cooperative via a cancellation token: long awaits are wrapped so
a preempted step unwinds at once (a threat interrupts chopping within a tick, not
after the 15-second chop wait). A BehaviorScript base class (lib/bot.js) adds
the lifecycle (incl. an awaited one-time onStartup), movement (walkTo) and
inventory; opt-in mixins add banking (lib/bank.js) and survival/restock
(lib/survival.js). A shared threat meter (lib/threat.js) scores nearby
danger from a body-list of aggressive creatures plus confirmed-attack signals.
The shipped bot — scripts/js/lumberjack.js — is a complete worked example:
it rotates between forest stands chopping trees, banks the logs when full, withdraws
gold, restocks bandages/food from vendors (matched by paperdoll job title, not
name), eats on a timer, and — driven by the threat meter — fights, bandages
mid-fight, flees an unwinnable foe (rotating to a fresh stand and avoiding the
mob's area), and walks to a healer to resurrect when killed.
The full guide is BT.md.
The renderer (src/render/Renderer.*) is a software isometric rasterizer that
produces an ARGB1555 framebuffer (one u16 per pixel) and hands it to a
MiniFB window, which does integer upscaling for free.
It is a deliberate reimplementation of the original client's draw pipeline, not a
generic engine — the projection, draw order, hue handling, lighting and minimap
rule are all modelled on client_2.0.7.exe.
What it draws, in painter's-algorithm order:
- Land terrain stretched across each tile's four corner z-values, sampling
art.muldiamonds andtexmaps.mulsloped textures. - Static art from the map blocks, z-sorted against mobiles so same-z mobiles draw above world items correctly.
- Dynamic server items (
0x1A) — lamp posts, doors (with their open/closed graphic offset), decor — keyed by serial. - Mobiles (players/NPCs) as
anim.mulbody animations, with:- Equipment layered over the body at the same action/frame,
- Hues from
hues.mulcolour ramps, - Walk/run cadence from
animinfo.mul, with sub-cell sliding so sprites glide between cells in sync with the walk cycle (the local player stays centred and the world scrolls under it), - Mounts & chair seating — a mount body drawn under the rider, or a rider
shifted onto a chair seat (ported from
g_SittingChairTable @ 0x55DB68), - Combat / death animation states and war-mode poses.
- Animated statics via
animdata.mul. - Procedural night lighting: a per-pixel RGB darkness map seeded by the world
light level (
0x4E/0x4F), with smooth radial coronas subtracted for each classified light source (warm for fire/candles, white for lamps; a carried torch/lantern casts a moving pool). Aday [on<code>|</code>off]toggle forces full daylight.
Overlaid on top of the world frame:
- Radar minimap (toggle
M) — an isometric orientation panel using the same projection as the 3D view, coloured fromradarcol.mulvia the real client's radar rule (topmost surface wins), cached per 8×8 map block, auto-scaled to fit the player and the whole planned route, with route/player/goal markers. Modelled onCRadarGump_Update/CRadarGump_RenderMinimap. - HUD: status bars (HP/mana/stamina), a scrolling system log / journal, overhead text, a chat input line, and the UO directional walk cursor under the mouse.
The window also accepts supervision input — see Commands & window controls.
The only supported backend is the reverse-engineered UO Demo server: github.com/draxinar/ouo. This client speaks exactly that server's flavour of the 2.0.7 protocol (nocrypt, 1997-era move packet, no client-side keepalive needed, no step-timing anti-speedhack).
Testing against other servers — official, ServUO, RunUO, etc. — is not a goal and is not planned. Pointing the client at a different shard is unsupported and will likely break at the handshake or framing layer.
-
Windows (the renderer host and build scripts target MSVC; networking uses winsock). The non-Windows compile path exists for the MUL/bot code but the renderer is Win32.
-
Visual Studio Build Tools (MSVC, 32-bit) + Ninja + CMake ≥ 3.20.
-
Original UO T2A-era MUL data files. None are distributed with this repo — you must supply them from a legitimate Ultima Online installation. The paths are configured in
src/main.cpp(defaultE:/uo/*.mul):File(s) Used for tiledata.mulTile flags (walkability, surfaces, doors, light sources) map0.mul,staidx0.mul,statics0.mulMap cells + static art (Britannia, map 0) verdata.mul(optional)Version word / patch overlay (read-only here) art.mul,artidx.mulLand + static tile bitmaps texmaps.mul,texidx.mulSloped land textures anim.mul,anim.idxMobile body animations animdata.mulAnimated static/dynamic art animinfo.mulMobile walk/run timing hues.mulColour ramps for tinted objects/mobiles radarcol.mul(optional)Per-tile minimap colours The MUL files are loaded lazily on the first navigation/render that needs them.
scripts\build.batThis runs vcvars32 → cmake -G Ninja → ninja and builds every target. Output
lands in build\:
build\uo_client.exe— the client.uo_mul.lib— the MUL loader static library.uo_mul_dump.exe— a CLI for dumping tiledata / map cells / walkability.
Manual configure/build (with the MSVC environment already active):
cmake -S . -B build -G Ninja
cmake --build buildBuild notes:
- Flags are
/W4 /EHs-c- /GR- /utf-8 /permissive-— exceptions and RTTI are off, so the C4530 warnings from STL headers are expected and harmless. - If linking fails with LNK1168, a previous
uo_client.exeis still running and holding the file — close it and rebuild.
build\uo_client.exe :: use built-in defaults + renderer
build\uo_client.exe --headless :: pure console client, no window
build\uo_client.exe <host> <port> <user> <pass> [gamePort] [gameHost]Configuration lives in src/main.cpp as a Client::Config. The defaults are
local placeholders for the maintainer's LAN (host 172.28.160.1, login
xrip/xrip) and are overridable on the command line — edit them locally or pass
args rather than committing your own environment. Key fields:
| Field | Default | Notes |
|---|---|---|
loginHost / loginPort |
172.28.160.1 / 2593 |
override via argv[1..2] |
username / password |
xrip / xrip |
override via argv[3..4] |
version |
2.0.7 |
reported in 0xBD |
plaintextSeed |
0xAC1CA001 |
= server IP; nocrypt relay token |
sendSeed |
true |
4-byte seed prefix on connect |
legacyMovePacket |
false |
move-packet variant for the demo protocol |
enableKeepalive |
false |
no client 0x73 keepalive |
acceptDoors |
true |
A* routes through doors, opened at runtime |
enableRenderer |
true |
open the world window (--headless disables) |
*Path (MUL files) |
E:/uo/*.mul |
see assets |
renderWidth/Height/Scale |
960×540 ×2 |
framebuffer + integer upscale |
stdin commands (typed in the console while in-world):
| Command | Effect |
|---|---|
goto <x> <y> [z] |
One-shot A* path to fixed coordinates |
follow <name|0xserial> [distance] |
Follow a mobile; chase only when farther than distance (default 1) |
follow off |
Stop following |
mobiles |
Query nearby names (0x98), then list name serialId |
cast <spellId> |
Cast a spell (1-based id) via 0x12/0x56 |
skill <skillId> |
Use a skill (0-based id) via 0x12/0x24 |
use <0xserial|type|'name'> [pack] |
Double-click an item by serial, graphic id, or name; searches backpack → worn → nearest world item |
arm|disarm [weapon|shield|both] |
Move weapon/shield to backpack and back |
pickup <target> |
Lift nearest matching world item (0x07) into backpack |
drop <target> <x> <y> [z]|<0xcontainer> |
Move backpack item to tile or container |
equip <target> [pack] |
Wear an item (layer from tiledata quality) |
unequip <weapon|shield|target> [pack] |
Take a worn item off; drops to world or backpack |
stop |
Abort the current path |
pos |
Print the player's position |
day [on|off] |
Force full daylight / restore server light levels |
verbose [on|off] |
Toggle per-packet console chatter |
target ... |
Set target cursor |
run <script.js> |
Load + run a JS bot script in a fresh runtime (re-run to reload) |
js stop |
Stop the running JS script |
| anything else | Sent as 0x03 ASCII speech |
Item-target tokens: 0x… ≥ 0x40000000 → serial; smaller → graphic id; else → tiledata name. Multi-word names need quotes.
Render-window controls (supervision while the bot drives):
| Input | Effect |
|---|---|
| Right-click a tile | goto that cell |
| Arrow keys | Manual single-step walk (throttled) |
| M | Toggle the radar minimap panel |
| SPACE | Send OpenDoor for the faced tile |
| TAB | Toggle war / peace mode |
| Type | Enter the on-screen chat input |
Focused probes live in tests/, each with a build/run script in scripts/:
| Script / test | What it checks |
|---|---|
scripts\build_hufftest.bat |
Huffman compress/decompress round-trip |
scripts\build_bltest.bat |
blacklist.mul (verdata) round-trip |
scripts\build_pathprobe.bat <sx sy sz gx gy [margin]> |
A* against the real MULs (path length / node-cap behaviour) |
scripts\build_viewer.bat [args] |
World-viewer probe (still renders) |
scripts\build_animprobe.bat |
Animation decode probe |
Two regression harnesses guard the behaviour-critical paths:
scripts\path_regression.bat— runs two long cross-continent routes (Trinsic bridge ↔ Britain basement, both directions) and regeneratestests\path_regression.txt. Treatresult/steps/expanded/pathCostas deterministic signals (any diff is a real behaviour change to explain);searchUsis wall-clock timing, read as a performance trend only. Run this after any change undersrc\bot\or toWorld::QueryCell/walkability, and only commit the baseline when the change is intended.scripts\render_regression.bat— Windows-only; dumps PNG scenes tobuild\regression\for visual comparison against the official 2.0.7 client (watch07_negz_interior.pngfor negative-Z interiors). Run this after renderer changes.
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)
- Combat in C++ is a hook; the policy lives in JS. The C++ core only halts the path safely on a HP drop and logs the threat. The actual engage / flee / resurrect behaviour is implemented in the JS layer (the threat meter + the lumberjack's DPS-race assessment). Recall (spell + reagent/rune handling) is still TODO.
- Road bias uses a minimal grass tile set; expand it with this shard's exact grass/road IDs to sharpen routing.
blacklist.mulauto-persist is disabled (read-only) to avoid poisoning passages — the bot uses transient avoidance only.verdata.mulpatching is not applied (the target server only reads its version word, so base MULs already match).- Map 0 only (Britannia, 768×512 blocks) is assumed.
- Single backend. Only the UO Demo server is supported.
Several in-repo docs go deeper than this README:
AGENTS.md— repository guidelines: structure, runtime notes, build/test commands, coding style, and the reverse-engineering workflow (including the rule that IDA findings are written back into the IDB).CLAUDE.mdis a symlink to this file.bot-client.md— the design & state notes: protocol flow, movement model, pathfinding internals, obstacle/door/mobile/fatigue handling, the full tunables table, and the known-limitations list.BT.md— the bot-scripting guide: the priority behaviour runner, cancellation tokens, theBehaviorScriptbase class and skill mixins, with a full worked example.scripts/js/globals.d.tscarries the matching TypeScript types (the source of truth for the scripting surface).JS-BIBLE.md— the deeper JS scripting reference.
Licensed under a custom non-commercial MIT-style license — © 2026 Ilia
Maslennikov (xrip). You may use, modify and distribute it for non-commercial
purposes only, must retain the copyright/permission notice (including a link to
this repository), and may not sell or monetize it (or derivatives/services) without
prior written permission. See LICENSE for the exact terms.
Ultima Online is a trademark of its respective owners; this is an independent, educational reverse-engineering project and is not affiliated with or endorsed by them. No game data files (MULs) are included or distributed — you must provide your own from a legitimate installation. The client is intended for use against your own copy of the reverse-engineered UO Demo server.
