| name | uo-login |
| description | Start the ClassicUO headless client daemon, log in, load map data, and load POI data. Use whenever the user asks to run/start/log in to the headless UO client, or before any goto/pos/canwalk/tiles/items/closest/findpoi command that needs the map or POI data loaded. |
Logging in and initializing the map
The headless client lives in headless/. It runs as a background daemon controlled
through two files:
- Commands go in:
echo 'cmd' >> /tmp/cuocmd
- Output streams to:
/tmp/cuolog (tail -f /tmp/cuolog)
1. Check for an existing daemon first
Never blindly start a second daemon — two processes fighting over the same
socket/command files causes confusing interleaved output.
ps aux | grep -i "cuoheadless\|dotnet run" | grep -v grep
If one is running and you just need to send commands, skip straight to step 3.
If you need a clean session (e.g. after a code change was rebuilt), kill it first:
kill <pid1> <pid2>
2. Start the daemon
Credentials and connection settings live in headless/.env (host, port, user,
pass, version, server/char auto-select index, UO_MAP_DIR). Don't hardcode
credentials on the command line — the daemon reads .env itself.
cd "$(git rev-parse --show-toplevel)"
[ -d headless ] && cd headless
rm -f /tmp/cuocmd /tmp/cuolog
nohup dotnet run -- --daemon > /tmp/cuostdout.log 2>&1 &
disown
sleep 9
tail -30 /tmp/cuolog
Login (server select, character select) is automatic if UO_SERVER / UO_CHAR
are set in .env. Watch the log for Entered the world! — that confirms the
character is in-game. A couple of dismissable gumps (server news/MOTD, a
"client out of date" notice) show up right after login and are auto-dismissed
by the client; no action needed.
3. Load the map
Almost nothing spatial works until the map is loaded — goto, pos, canwalk,
tiles, items, line-of-sight, and pathfinding all depend on it. Load it once
per daemon session:
echo "loadmap" >> /tmp/cuocmd
sleep 5
Confirm with:
echo "pos" >> /tmp/cuocmd
sleep 1
tail -5 /tmp/cuolog
You should see Position: (x, y, z) ... — if instead you see Map not loaded. Run: loadmap, the load didn't finish or wasn't sent.
4. Load POI data (optional)
poi/*.json are optional points-of-interest files (banks, healers, moongates,
shops, dungeon features, etc.) that power the closest <category> and
findpoi <text> commands — see the uo-navigation skill for how those are used.
This data may not exist on a given checkout; load it the same way every session
regardless, since it's harmless (and quick) when absent:
echo "loadpoi" >> /tmp/cuocmd
sleep 1
tail -3 /tmp/cuolog
With no argument this reads UO_POI_DIR from .env, falling back to ./poi
relative to the client. Look for either:
[POI] Loaded 755 POIs (68 categories) from 2 file(s) in /path/to/poi
or, if the directory/files just aren't present — not an error, just means
closest/findpoi won't have data to search:
[POI] POI directory not found (optional, skipping): /path/to/poi
Gotchas
- The daemon prints a LOT of chatter (gump layouts, animations) interleaved with
command output —
tail -N right after sending a command, don't try to parse
the whole log.
grep -c on /tmp/cuolog can silently return 0 matches even when lines are
clearly present in tail output — the file contains non-UTF8 bytes (arrow
glyphs in animation-related log lines) that make plain grep treat it as
binary in some locales. Use grep -a to force text mode when counting/searching.
- If you rebuilt the code (
dotnet build), you must kill and restart the
daemon — it's running the old binary until restarted.