Classic Ultima Online on Godot 4 .NET
Two things are measured, and neither is estimated. Parity is whether GUO draws and behaves as ClassicUO does in the same place. Drift is how far the ported files have moved from upstream’s, and whether upstream has moved under them.
launchers\pipeline\03_port_audit.bat REM writes docs\port_status.md
tools\port_audit classifies every upstream file by how tightly it binds to
FNA (verbatim, shim, rewrite) from its imports, and matches filenames
in godot\GUO\src. It says which files exist. It does not say they work:
“Ported” is not “working” (AGENTS.md rule 6). CI fails if the committed
docs\port_status.md differs from a fresh run. Files that will never be
ported are listed with their reason in docs\port_waivers.toml; a waiver
needs an ADR, a PORT GAP comment or a AGENTS.md rule behind it and does not
improve the number.
Ported files are copied faithfully: no reformatting, renaming, modernising or opportunistic bug fixing, because every gratuitous edit has to be reconciled by hand on every upstream merge. Where a change is unavoidable, it is marked in the source:
// PORT DEVIATION (GUO): <why>
The marker is what a reviewer greps for, what an upstream merge stops at, and what a drift tool counts. A hook into ported code that is a no-op off the feature (the touch layer, the second display, the canvas background) still gets the marker. Bugs found in upstream code are noted, not fixed mid-port; fixes are proposed separately (rule 3, parity before improvement).
launchers\dev\port_drift.bat [--strict] [--verbose]
tools\port_drift finds each ported file’s upstream original, ignores line
endings, namespaces and shim imports, and reports files that are identical,
changed with a PORT DEVIATION or PORT GAP marker, changed with no marker,
and new. --strict exits 1 if any change is unmarked. Matching is by
filename and longest path suffix, so check a surprising diff by hand.
The layer rules in Feature Layers say what a change to ported code needs: a reason, the marker, and preferably one seam.
Upstream does all of its input in one SDL event filter. GUO replaces it
with src/Input/GodotInput.cs, copied in upstream’s order, and two things SDL
did on its own have to be done there by hand:
GodotInput.ReleaseHeldKeys). Before, Alt, Ctrl or Shift stayed held after
Alt-Tab and the next click was a modified click.GodotInput.StartTextInput). Chinese, Japanese and Korean input
methods compose, and an AltGr character (the @ of a German keyboard) is
typed instead of arriving as Ctrl+Alt and being dropped. One small
difference remains: upstream also blanks the key itself for an AltGr
character, and Godot gives no way to tell it apart, so a hotkey bound to the
bare key (Q alone, for AltGr+Q) can fire as the character is typed.--keyboard-probe checks both without a shard (scenario
client.input.keyboard_mouse, and a step of launchers\dev\smoke.bat).
The full list of input differences is docs/input_audit_2026-10-18.md.
launchers\dev\sync_upstream.bat REM commits since the pin, flagging ported files
launchers\dev\sync_upstream.bat --pin REM record the current head as reviewed
docs\upstream\UPSTREAM_PIN.json records the ClassicUO commit reviewed up
to. The tool lists what landed upstream since, and flags commits touching
files already ported, the ones that rot silently. Assess, port across, then
re-pin.
Every visual fault found so far was found by eye, and arguing about one is
slow while nobody has the original in front of them. ab_compare stands both
clients in the same spot on the same shard with the same profile (the same
gumps open, the same render options, the same game-window size) and takes a
picture with each.
launchers\dev\ab_compare.bat [--only guo|cuo|both|compose] [--place P]... [--character C] [--build] [--login-wait S] [--settle S] [--no-open] [--allow-foreground]
Output: build\screenshots\ab\<place>\guo.png, cuo.png and ab.png (the
two stacked, labelled).
--play, --shard-command "[go X Y" and
--screenshot-name: one process per place, no window handling.--allow-foreground is the
gate; an agent must not take the keyboard from whoever is working. The
ClassicUO-side counterpart is commit “Refuse to type unless ClassicUO has
the foreground”.--build builds the reference ClassicUO first (build\cuo-bin).The recorded sweeps: docs\parity_2026-09-23.md (eight places, five
matching, the rest listed with causes) and docs\parity_2026-09-26_night.md
(the overnight fixes and the ADR-0007 measurements). Findings there are
numbered (F1 and so on) and each says whether it is fixed and how it was
verified.
| Tool | Does |
|---|---|
launchers\dev\sweep.bat |
Photographs the same five places every run (a town by day, a forest with buildings, a coast, a dungeon mouth, a town at ground level), so two sweeps can be compared. Needs the shard. |
launchers\dev\side_by_side.bat [--place P \| --at X Y [Z]] [--monitor M] [--dump LABEL] |
Both clients live on one monitor in one place; --dump makes both write a render dump. |
launchers\dev\render_diff.bat LABEL |
Compares a ClassicUO render dump with a GUO one taken in the same place; --list shows the dumps there are. |
tools\render_dump |
The dump format both clients write on renderdump LABEL. |
guoasset (MCP, tools\guoasset) |
Renders any art, gump or multi from the client data with the reference reader, so an agent can compare a frame against the source art. |
tools\world_parity |
The editor’s world view against the client’s, see Editor. |
--zoom-probe and the renderer probes |
The numbers in the ADRs: frame cost per zoom, batcher checks, blend states, atlas pages. See Scripted Runs and Probes. |
A sweep row says matches when the stacked frames differ only in moving things: NPCs, animals, weather text, hover tooltips. Anything else is a finding with a number, a cause and, once fixed, the tool run that showed it gone. That is the standard a parity claim in a commit message is held to.