Julian-adv / OpenMMO
- пятница, 24 июля 2026 г. в 00:00:06
An MMORPG where AI agents and human players are treated as equals.
Agents and humans connect to the same world, act under the same rules, and interact with each other without distinction. No privileged API is given to agents — they participate through the same interface as human players.
Play it now: openmmo.to.nexus — sign in with Google and jump right in.
Solo-developed and vibe-coded. Assets are a mix of AI-generated, procedurally/programmatically created, and sourced from the internet. PRs are welcome!
World & Terrain
Gameplay Systems
Engine & Performance
Assets & Agents
Client:
Agent Client:
Server:
cargo install cargo-watch| Port | Service |
|---|---|
| 10004 | Client (Vite dev) |
| 10005 | GLB Editor |
| 10006 | Server WebSocket (binds 127.0.0.1; reached through the vite proxy in dev, nginx in prod) |
| 10007 | Server Terrain/Housing/NPCs API (binds 127.0.0.1; writes require auth) |
Both server ports are loopback-only by default (
--bind/--api-bind). Pass--bind 0.0.0.0only to serve clients on other machines directly — that path has no TLS and no proxy in front of it.
Proxy Rule: Vite dev server proxies
/ws→ws://localhost:10006and/api(all REST endpoints) →http://localhost:10007automatically (seeclient/vite.config.ts).
This project is organized as a Cargo Workspace. The shared Rust crate (shared/) is used by the server, the client via WASM, and the agent client. Source game data lives in data-src/ and is converted to generated JSON in data/ during the Cargo build. To rebuild the server only when the server crate (server/), the shared crate, or source data changes, run the watch command from the root directory.
cargo watch -w server -w shared -w data-src -x "run -p onlinerpg-server"The server listens on port 10006 by default. The terrain/housing/NPCs REST API starts automatically on port 10007 (game port + 1), bound to 127.0.0.1 (--api-bind to override). Reads are public; writes (PUT/POST/DELETE) require a bearer token: either the NPC token (local scripts) or a Google ID token whose email is in ADMIN_EMAILS / --admin-emails (comma-separated) — the map editor sends the signed-in user's token automatically.
WebSocket and terrain API proxying is handled by Vite's dev server proxy (see client/vite.config.ts), so no separate socat or SSL proxy is needed.
Google sign-in: browser login uses Google OAuth. Pass the same Web client ID
to the server (GOOGLE_CLIENT_ID env / --google-client-id) and the client
(VITE_GOOGLE_CLIENT_ID, see step 4). Without it the server runs but rejects
browser logins. The NPC/bot token is auto-generated at data/npc_token on first
run; override with NPC_AUTH_TOKEN / --npc-token (min 16 chars).
An agent-client running on someone else's machine signs in with its own Google
account through the device flow, which needs a second OAuth client of type "TV
and Limited Input" (a headless client cannot use the Web one). Pass that client
ID as GOOGLE_CLI_CLIENT_ID / --google-cli-client-id; the server accepts
tokens from either client. See doc/REMOTE_AGENT_CLIENT.md.
cd client
cp .env.example .env.local # then set VITE_GOOGLE_CLIENT_ID (required for login)
npm install
npm run dev -- --port 10004Edit agent-client/data/config.toml to set the correct port numbers, then run:
cd agent-client
cargo watch -i "data/prompts/memory/" -x runTo have Rust code changes in the shared library reflected in the browser immediately during client development, run the following command in a separate terminal:
# Run from the root directory
cargo watch -w shared -s "npm run build:wasm --prefix client"cd tools/glb-editor
npm install
npm run dev -- --port 10005Prod runs both binaries as systemd units (tools/systemd/), with the client bundle served statically from /var/www/openmmo.
| Unit | Binary | Syslog identifier |
|---|---|---|
openmmo-server |
onlinerpg-server |
openmmo |
openmmo-agent-client |
agent-client |
openmmo-agent |
Deploy by running tools/deploy-prod.sh on the prod host — it pulls master, builds both binaries and the client bundle, publishes the static files, then restarts both units.
The server handles systemd's SIGTERM gracefully: it shows connected players a restart notice, closes its listeners and periodic tasks, waits for any in-flight batch save, persists every connected character and inventory plus the world clock, then exits. systemctl restart waits for that drain before starting the new binary.
Admin characters can use /notice <message> to raise the same banner by hand (this is the live in-game banner, not the login-screen announcements served from data/announcements/). /notice with no message clears it; players who enter while one is active receive it on join.
Over SSH, detach it from the session so a dropped connection cannot kill the build midway:
ssh prod 'setsid nohup bash ~/work/OnlineRPG/tools/deploy-prod.sh > ~/deploy-latest.log 2>&1 < /dev/null &'
ssh prod 'tail -f ~/deploy-latest.log' # follow; ends at "==> deployed <commit>"Losing the connection to a foreground run costs the whole build but never a half-deploy: the script builds everything first and only touches live state at the end (rsync to the webroot, then the restarts), so an interruption before that leaves the old bundle and the old server process running as a matched pair.
Both units log to journald (StandardOutput=journal); there are no separate log files.
journalctl -u openmmo-server -f # follow the game server
journalctl -u openmmo-agent-client -f # follow the NPC agent client
journalctl -u openmmo-server -n 200 --no-pager # recent history
journalctl -u openmmo-server --since "1 hour ago" -p err # errors onlyBoth binaries build their subscriber from RUST_LOG, defaulting to info when it is unset or unparseable. Per-action detail — monster spawn/despawn, combat dice/HP/XP rolls — sits at debug to keep the journal rate sane at high player counts, so raise the level to see it:
sudo systemctl edit openmmo-server # or write /etc/openmmo/server.env
# [Service]
# Environment=RUST_LOG=debug
sudo systemctl restart openmmo-serverEach unit reads EnvironmentFile=-/etc/openmmo/{server,agent-client}.env, so RUST_LOG can go there instead. Note that a systemd unit does not inherit your shell environment — exporting RUST_LOG in a terminal only affects binaries you launch by hand.
The movement warnings (rejected move target, waypoint queue full, blocked move) deliberately stay at warn: they fire when the server and client step checks disagree, which is a bug signal rather than normal play.