GodotUO

Classic Ultima Online on Godot 4 .NET


Project maintained by DatMoshu Hosted on GitHub Pages — Theme by mattgraham

Modding

How to write a GUO mod, test it, ship it to your players, and what to do when a mod makes the game slow.

A GUO mod teaches the client a packet your shard sends that ClassicUO does not know. Shards with their own server packages (POL, ServUO and RunUO forks) used to need a forked client for that. With a mod, players keep the normal GUO and add your mod. The design is ADR-0038; what else you can change without a fork (assistant plugins, scripts, packs, server gumps) is listed in the modding survey, docs/modding/surface_2026-10-18.md.

What a mod is, plainly: a small .NET assembly that runs inside GUO with the player’s rights. GUO only loads it on a desktop (Windows or Linux), only when the player has turned Load mods on, and only on the shards the mod names. It never downloads one by itself.

What a mod can do today

Not yet: opening a gump or changing the world directly (a later ABI version), and anything on Android or the web build. GUO keeps 0xBF subcommand 0x4755 for itself (the shard offer); no mod can take it.

Write one

Start from the sample, docs/modding/samples/announce: it adds packet 0xF9, a shard announcement shown in the journal, and its README shows the bytes a shard sends.

  1. Make a .NET 8 class library that references godot/GUO.ModApi (GUO.ModApi.csproj, with Private="false" so the client’s copy is used). That assembly is everything a mod sees of GUO.
  2. Write a public class with a parameterless constructor that implements IGuoMod. In Register, call registrar.AddPacket once per packet:

    public void Register(IModRegistrar registrar) =>
        registrar.AddPacket(new PacketRegistration { Id = 0xF9, Length = -1, Handler = OnAnnounce });
    

    Register runs once at start. Do nothing else there: no threads, files or sockets.

  3. The handler gets the whole packet, id and length bytes included, on the game thread. If it throws, that one packet is dropped and the log says so; the game carries on.
  4. Write mod.json next to the assembly. Every packet you register must be declared there, with the same length and override; anything undeclared is refused. shards lists your login host or shard name (or "*"). The fields are in docs/data_formats.md, section 42.

A registration can carry MinClientVersion / MaxClientVersion (encode a version with ModAbi.Encode(7, 0, 15, 1)) when a packet differs between client versions.

Test it

Unit-test your handler by calling it with bytes, the way tools/network_tests does for the sample:

dotnet run --project tools\network_tests

Then try it in the client:

  1. Build your mod, and copy the .dll and mod.json into Data\Mods\<your-mod>\ beside the client.
  2. Options > Load mods on the next start, then restart. The line under the box says how many mods loaded and how many were refused.
  3. A refused mod’s reason is in the client log, as [mods] refused ...: an undeclared packet, a packet another mod already has, a typo in mod.json, the wrong ABI.
  4. For one run without changing your settings, start the client with --load-mods, and --mods-dir FOLDER to load from another folder.

Your shard then has to send the packet. Send it only to players who have the mod: a client without it misreads the stream.

Ship it

Two ways, both installed by the player:

A mod made the game slow

Measure it with the perf lab (launchers\dev\perf_lab.bat, on the Performance page once it is published), once without the mod and once with it, on the same machine at the same size:

launchers\dev\perf_lab.bat --label without
launchers\dev\perf_lab.bat --label with --args "--load-mods --mods-dir C:\path\to\mods"

The lab plays on the local dev shard, which does not send your shard’s packets, so it shows the cost of having the mod loaded, not of handling your traffic. If the drop only happens on your shard, say so, and say which packet the mod handles and how often your shard sends it.

Report it with both report files the lab names on its last line (share this file), the mod’s name and version, and its mod.json. They carry no paths or names. Post it where you report other GUO problems (Community and Support).

See also