Repository navigation
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.
- Add a packet the client does not know, with its length (fixed, or dynamic with the length in bytes 1-2).
- Take a 0xBF extended-command subcommand.
- Replace one of the client's own packet handlers, when it says so
(
"override": true). - In its handler: print a system message, send a packet to the server, write to the client log.
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.
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.
-
Make a .NET 8 class library that references
godot/GUO.ModApi(GUO.ModApi.csproj, withPrivate="false"so the client's copy is used). That assembly is everything a mod sees of GUO. -
Write a public class with a parameterless constructor that implements
IGuoMod. InRegister, callregistrar.AddPacketonce per packet:public void Register(IModRegistrar registrar) => registrar.AddPacket(new PacketRegistration { Id = 0xF9, Length = -1, Handler = OnAnnounce });
Registerruns once at start. Do nothing else there: no threads, files or sockets. -
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.
-
Write
mod.jsonnext to the assembly. Every packet you register must be declared there, with the same length and override; anything undeclared is refused.shardslists your login host or shard name (or"*"). The fields are indocs/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.
Unit-test your handler by calling it with bytes, the way
tools/network_tests does for the sample:
dotnet run --project tools\network_testsThen try it in the client:
- Build your mod, and copy the
.dllandmod.jsonintoData\Mods\<your-mod>\beside the client. - Options > Load mods on the next start, then restart. The line under the box says how many mods loaded and how many were refused.
- A refused mod's reason is in the client log, as
[mods] refused ...: an undeclared packet, a packet another mod already has, a typo inmod.json, the wrong ABI. - For one run without changing your settings, start the client with
--load-mods, and--mods-dir FOLDERto 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.
Two ways, both installed by the player:
-
A folder. Give players the
.dllandmod.jsonto put inData\Mods\<your-mod>\. Simple, but unsigned: they have to trust where the files came from. -
A Store pack. Publish the mod as a
modcomponent in a content pack (the contract is indocs/asset_pack_ecosystem.md, Mod components). The pack is signed with your publisher key (tools/asset_store), lists the packets it touches, and installing it runs nothing. The player sees your key fingerprint, the packets and the shards in the Store, and approves the mod; it loads on the next start with Load mods on. An update or a new key asks again. Publish your key fingerprint where your players can check it. Store mod packs need the owner's decision first (ADR-0038's amendment), so this route may not be open yet.
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).
- Asset Store: installing and approving packs.
- Server Compatibility: which shards GUO plays.
-
Scripted Runs and Probes:
--load-mods,--mods-dirand--mods-probe.
Start
- Getting Started
- Configuration
- Launchers and Tools
- What is GUO?
- FAQ
- Known Issues
- Troubleshooting
- Performance
- Glossary
Builds
Playing
- Servers and Accounts
- Controller
- Mobile UI
- Gump Size and Screen
- Canvas Background
- Screen Effects
- Water
- Asset Store
- Content Packs
- Dev Shard
- Server Compatibility
Developing
- Scripted Runs and Probes
- Parity and Drift
- Editor
- World Editing
- Manage Your Shard From the Editor
- Admin Tab
- Author UO Data
- Building Multis
- Modding
- Architecture
- Feature Layers
- Working with AI Agents
- Contributing
Project