A lightweight EQEmu client. It connects to an
EQEmu server, relies on eqoxide asset server for content, renders the zone in 3D (wgpu), and exposes a local HTTP API so an agent (or
any script) can drive the client — move, hail NPCs, target, fight, buy, capture frames, and more.
It targets EQEmu servers only (it is not for, and does not work with, official EverQuest servers).
It can log in as a GM observer or as a regular player character that actually plays
(see docs/autonomous-play.md).
Legend: ✅ working (implemented & verified) · 🟡 in progress · 🔵 planned · 🐞 known bug/limitation
| Feature | Status | Notes |
|---|---|---|
| Login as GM observer | ✅ | Direct connect to local EQEmu (login 127.0.0.1:5999) |
| Login as a regular player character | ✅ | Plays for real; per-character config in ~/.config/eqoxide/ |
| Multiple instances side-by-side | ✅ | Auto-binds next free HTTP port from 8765; prints API_PORT= |
Graceful shutdown (POST /v1/lifecycle/exit) |
✅ | Per-instance; never pkill |
Zone-line travel (POST /v1/navigate/zone_cross) |
✅ | Sends target zone id; verified both ways (e.g. qcat↔qeynos) |
| Auto walk-into a zone line | 🐞 | Rarely fires — server sends arrival, not trigger, coords; use /v1/navigate/zone_cross |
Derived-asset sync from eqoxide_asset_server |
✅ | Models, textures, string table, maps fetched over HTTP into XDG cache |
| Feature | Status | Notes |
|---|---|---|
| Zone terrain + placed objects/buildings | ✅ | ActorInstance placements; NPCs sit among buildings |
| Per-race + per-gender character models | ✅ | All 15 playable races, skinned animation, idle/walk/combat clips |
| Correct relative race sizes (EQ feet) | ✅ | From GetRaceGenderDefaultHeight; robust true_height (no stray-vert inflation) |
| Equipment armor textures | ✅ | Material-driven swaps, tint, WearChange |
| Weapon models in hand + swing animation | ✅ | Verified on elf-female; NPC weapons & generic hand-joint = follow-up |
| Doors: clickable, animated, portal-zoning | ✅ | Geometry/placement correct; untextured (texture pass pending) |
| Smooth NPC movement (velocity dead-reckoning) | ✅ | Receives unreliable position updates |
Frame capture (GET /v1/observe/frame → PNG) |
✅ | Used for visual verification |
| World handedness / left-right mirror | ✅ | Fixed (clip-space X + A/D controls) |
| Helms / hair / head armor | 🔵 | Attached-model subsystem not built yet |
| Outdoor-zone vertical placement | 🐞 | Some outdoor zones (e.g. qeytoqrg) render terrain with a Z offset |
| Feature | Status | Notes |
|---|---|---|
Movement: walk POST /v1/navigate/goto (A* pathfinding) |
✅ | Routes around walls within connected areas; stalls across closed doors |
Movement: teleport POST /v1/navigate/warp |
✅ | Anti-cheat capped (~50–95u/hop); small hops from a synced state |
| Combat: auto-attack / auto-face / auto-retarget | ✅ | Heading-scale fix made melee land; hands-free grinding works |
Spell casting (POST /v1/combat/cast, gems, cast bar) |
✅ | OP_CastSpell + begin/mana/interrupt feedback |
Scribe / memorize spells (POST /v1/combat/scribe, /v1/combat/memorize) |
✅ | |
| Pets: tracking, auto-pet-combat, recall | ✅ | OP_PetCommands; squishy classes stand off |
| Target / consider / hail / say | ✅ | /v1/combat/target, /v1/combat/target/name, /v1/combat/consider, /v1/interact/hail, /v1/interact/say |
| Merchant buy / sell / trade window | ✅ | /v1/merchant/buy, /v1/merchant/sell, /trade/*; live-verified buy (item + coin) |
Loot corpses (POST /v1/interact/loot) |
✅ | Auto-loot queues own kills; takes listed items |
Quest hand-in (POST /v1/interact/give) + quest log (/v1/quests/log) |
✅ | Trade items to an NPC for turn-ins; also: completed history, task offer/accept/cancel |
Inventory read / move (GET /v1/observe/inventory, /v1/inventory/move) |
✅ | |
Doors API (GET /v1/observe/doors, POST /v1/interact/click_door) |
✅ | |
| Water-region detection + swim navigation | ✅ | .wtr BSP; swim-descent in find_path |
| Controlled-fall navigation + fall damage | 🟡 | Drop off ledges to a lower floor + client-side OP_EnvDamage; not yet exhaustively live-tested (curve tuning, water/levitate negation) |
| Feature | Status | Notes |
|---|---|---|
| Movable / resizable / persistent windows | ✅ | Per-character ui_layout_<Name>.json; Ctrl+L lock |
| Action grid (attack, sit/stand, target, consider, spell gems) | ✅ | Real TGA gem icons + cast bar |
Map window (toggle, M key) |
✅ | Closeable; default closed |
| Feature | Status | Notes |
|---|---|---|
--testzone offline zone/asset debugging (no server) |
✅ | |
render_model --race <CODE> skinned model viewer |
✅ | Renders a character exactly like the client (login-free); GPU skinning readback diagnostic |
--profile / EQ_PROFILE=1 frame-timing overlay |
✅ | Per-phase timings; see docs/dev-workflow.md |
cargo build --release
./dev-run.sh # release build; auto-restarts on rebuild/crash
./dev-run.sh debug # debug buildRun dev-run.sh in your own terminal, not from an agent/Bash tool call — the harness reaps
GUI child processes. Logs go to /tmp/eqoxide.log. Per-character server/credentials and
renderer asset paths live in ~/.config/eqoxide/ (honoring XDG_CONFIG_HOME). Credential files
are kept out of source control — copy a template into that directory and edit it locally.
Offline asset/zone debugging (no server): ./target/release/eqoxide --testzone.
Add --profile (or EQ_PROFILE=1) for a per-phase frame-timing overlay; see docs/dev-workflow.md.
The account + character to log in as is not a CLI name argument — it comes from the login
config file. Login configs live in ~/.config/eqoxide/. Pass one with --config:
./target/release/eqoxide --config durgan # ~/.config/eqoxide/config-durgan.yaml--config accepts:
- a profile name (
durgan) →~/.config/eqoxide/config-durgan.yaml - a bare filename (
config-durgan.yaml) → looked up in~/.config/eqoxide/ - an explicit path (
./foo.yaml,~/elsewhere/x.yaml) → used as-is
With no --config, the client loads ~/.config/eqoxide/config.yaml. Each config-<name>.yaml
sets its own account.username, account.password, and account.character_name. To add a
character, copy an existing file in ~/.config/eqoxide/, edit those three fields (the
account/character must already exist on the EQEmu server), and pass its name to --config.
ls ~/.config/eqoxide/config-*.yaml # available login profiles
grep character_name ~/.config/eqoxide/config-*.yamldev-run.sh assumes its own terminal. If you must launch from a Bash tool call (where the harness
reaps GUI children), detach with setsid so the process survives, then read the printed
API_PORT= line:
setsid bash -c 'XDG_RUNTIME_DIR=/run/user/$(id -u) DISPLAY=:0 WAYLAND_DISPLAY=wayland-0 \
exec ./target/release/eqoxide --config durgan' \
> /tmp/eq_durgan.log 2>&1 < /dev/null &
disown
sleep 12
PORT=$(grep -m1 -oP 'API_PORT=\K[0-9]+' /tmp/eq_durgan.log) # do not hardcode 8765
grep -E "entering world as|sent ReqClientSpawn" /tmp/eq_durgan.log # confirm zone-inRequires the local EQEmu server (login 127.0.0.1:5999) and a running X/Wayland session on
display :0.
The client supports several instances side by side — e.g. one per git worktree, so multiple agents can work on different features simultaneously without interfering.
-
Auto-port binding. Each instance binds the next free HTTP API port starting at
config.yamlhttp_port(default 8765), scanning upward: 8765, 8766, 8767, … -
Port is printed to stdout. On launch the client prints a single parseable line (also in
/tmp/eqoxide.log). Always read this — do not hardcode 8765:API_PORT=8766PORT=$(grep -m1 -oP 'API_PORT=\K[0-9]+' /tmp/eqoxide.log) curl "http://127.0.0.1:$PORT/v1/observe/debug"
-
Shut down your own instance with
POST /v1/lifecycle/exit— neverpkill eqoxide, which could kill another worktree's client./v1/lifecycle/exitcleanly stops only the instance on the port you call:curl -X POST "http://127.0.0.1:$PORT/v1/lifecycle/exit" # 200 "shutting down", then the process exits
See docs/http-api.md for the full endpoint reference and
docs/dev-workflow.md for the build/verify loop.
docs/architecture.md— thread model, shared types, data flowdocs/http-api.md— REST API reference (port discovery,/v1/lifecycle/exit, all endpoints)docs/dev-workflow.md— building, running, multi-instance, verify loopdocs/autonomous-play.md— playing as a real characterdocs/protocol-notes.md— EQ/Titanium wire protocol notesdocs/collision-system.md,docs/zone-rendering.md,docs/character-models.md— rendering internals