Skip to content
game-server · rust · arpg

Authoritative grid hostARPG Server

The authoritative shared-world host for the ARPG — a Diablo-style isometric action RPG. An axum WebSocket router paired with the simgrid headless Bevy grid sim, keeping movement, occupancy, and snapshots server-owned on its own Agones Fleet.

Orthogonal tile authority

The server keeps everything in orthogonal tile (x, y) space with integer grid math — collision is tile occupancy. Isometric projection is a client-side render transform only; the server predicts nothing, the client does.

  • Protocol — simgrid JSON, PROTOCOL_VERSION 14.
  • Z-floors — endless seeded multi-floor dungeon.
arpg-serverApp
axum + BevyStack
simgrid v14Protocol
0.0.0.0:7979Listen

What it gives you

Features

Terminate /ws + auth

Terminates the WebSocket upgrade and verifies the inbound Supabase JWT (dev-accept fallback when the secret is unset).

Drive the sim tick

Drives the simgrid sim tick on a tokio multi-thread runtime, fanning out authoritative snapshots to all clients.

Admit & spawn

Admits players into roster slots and spawns the procedural world — an open grid seeded with wandering hostiles — shared across the map.

Agones lifecycle

Runs the Agones SDK lifecycle (Ready / Health / Shutdown) so the Fleet can manage the pod; degrades gracefully for local dev.

Responsibilities

Overview & layout

arpg-server is the authoritative shared-world host for the ARPG (Diablo-style isometric action RPG client under astro-kbve arcade). It pairs an axum WebSocket router with the simgrid headless bevy grid simulation, so movement validation, occupancy, and snapshot framing stay server-owned. Like cryptothrone-server it is grid-authoritative — collision is tile occupancy and “physics” is integer grid math — but runs an isolated world (its own seed, registry, and spawn tables) on its own Agones Fleet.

  • Terminate the /ws WebSocket upgrade and verify the inbound Supabase JWT (dev-accept fallback when the secret is unset).
  • Drive the simgrid sim tick on a tokio multi-thread runtime, fanning out authoritative snapshots.
  • Admit players into roster slots and spawn the procedural world — an open grid seeded with wandering hostiles — shared across the map.
  • Run the Agones SDK lifecycle (Ready / Health / Shutdown) so the Fleet can manage the pod; degrades gracefully outside Agones for local dev.

The browser client lives at apps/agones/arpg/web/src/game/ (the standalone vite app — see ARPG Web) and connects over PUBLIC_ARPG_GAME_WS (default wss://arpg.kbve.com/ws). Isometric projection is a client-side render transform only; the server keeps everything in orthogonal tile (x, y) space and speaks the shared simgrid JSON protocol (PROTOCOL_VERSION 14).

apps/agones/arpg/server/
├── Cargo.toml # package manifest (path-dep to ../../../../packages/rust/simgrid)
├── Dockerfile # multi-stage chisel build (cargo-chef + sccache); no webkit stage
├── project.json # nx build / run / container targets
├── version.toml # ci-publish version sentinel
└── src/
├── main.rs # tokio entry + Agones lifecycle + graceful shutdown
├── agones.rs # Agones SDK heartbeat
├── dungeon.rs # seeded room-corridor generator (mirrors client systems/dungeon.ts)
└── game.rs # ARPG spawn config (player tile, wandering hostiles)

Sim core & wire

Protocol, z-floors & authority

Buildnx run arpg-server:build-release (or cargo build --release -p arpg-server); the release binary lands at dist/target/release/arpg-server. Containernx run arpg-server:container drops kbve/arpg-server:latest + version tag. CI uses the ci configuration with sccache + registry buildcache (ghcr.io/kbve/arpg-server:buildcache) on the shared ghcr.io/kbve/chisel-ubuntu-axum base. Default listen 0.0.0.0:7979 (override via ARPG_SERVER_ADDR).

Shares the simgrid crate (packages/rust/simgrid): postcard/JSON wire types, Supabase HS256 auth, axum WS transport, and the bevy headless build_app / run_sim_loop. The host registers ARPG spawn systems into simgrid’s ordered SimSet stages; transport and movement stay generic. The dungeon generator is shared logic — the server must produce the identical floor set the client’s systems/dungeon.ts does for a given DUNGEON_SEED, so collision agrees on both sides (Mulberry32 PRNG, domain-separated per-chunk seeds, CHUNK_SIZE=20). Server is authoritative for occupancy; client predicts.

Speaks the shared simgrid JSON protocol (PROTOCOL_VERSION 14). The client wire surface (@kbve/laser GameClient) is complete and the server answers it.

Client → server

FrameSent whenServer duty
Join { jwt, kbve_username }on socket openverify JWT (dev-accept fallback), admit to a roster slot, reply Welcome
Step { dir }float body crosses a tile linevalidate one cardinal tile move vs occupancy, advance the player
MoveTo { tile }left-click destinationserver-side pathfind/route the player toward tile (or accept as intent)
Action { id, target }attackresolve combat for action id (ACTION_ATTACK) against entity target
Face { facing }aim turnrecord 16-dir facing so remote clients render the right pose
Heartbeat, Leavekeepalive / disconnectliveness + slot release

Server → client

EventCarriesClient handler
Welcomeyour_slot, kind registryseed registry + slot
Snapshotplayers (slot→username), entities (eid, kind, tile, owner, hp, max_hp, z)applyEntitySync + reconcileFloat, filtered to the local floor
Ephemeral(Combat)target eid, dmg, critfloating damage + flash
Ephemeral(Projectile)shooter, origin, dir, kindspawns the arrow visual from the server, not a local tween
Ephemeral(Floor){ z, tile }local player took a stair — snap to tile, switch floor, re-stream dungeon
Ephemeral(Stats / Pickup / ...)per-eventfollow-up (loot, inventory)

The dungeon has a z-axis: each floor is its own endless seeded dungeon (floor_seed = mix(world_seed, z), floor 0 = the ground layout / identity). This is a shared simgrid feature, not ARPG-only — single-floor games (cryptothrone) attach no Floor component and behave exactly as before (every entity on z = 0, z omitted on the wire).

  • Collision is z-aware: WalkableMap::is_walkable_z(z, tile) / find_path_z. A Floor(i32) component (default 0) on each entity selects its layer; sim systems read it via Option<&Floor>.
  • Stairs are a generic Stairs resource — Explicit(Vec<StairLink>) for hand-placed maps or Dungeon { seed, descend_key } for the endless ARPG, which derives two seed-driven stairs (down/up) per floor. Stepping a stair tile moves the player to the linked tile on the adjacent floor and emits the Floor ephemeral.
  • Locks gate a stair on a key item: descending is gated on descend_key (the player must hold it in inventory); ascending is free. The ground floor seeds a dungeon-key near spawn.

Cross-language parity: the Rust arpg_dungeon and the client systems/dungeon.ts produce byte-identical floors (frozen FNV-1a fingerprint — 3726471204 for seed 0x5eed1, floor 0, window (0,0)..(80,80)), asserted on both sides.

The offline client (DEBUG_LOCAL_PLAYER) resolves combat and movement locally; online, authority is the server’s. The handoff is in place:

  1. Single position truthapplySnapshot reconciles the server tile into floatState via reconcileFloat.
  2. Combat over the wirefireBowAt(aim, target?) sends action(ACTION_ATTACK, target) with the real target eid (null = server resolves the aim line); the click-attack no longer double-fires.
  3. Server-driven projectiles — the arrow visual spawns from the Projectile ephemeral; applyLocalHit is gated behind localMode.
  4. Facing syncclient.face(facing) fires on aim/movement change (cardinal, only on change).
  5. Floor + event subscriptionsprojectile and floor join combat; snapshot entities are filtered to the local floor.

Offline mode keeps the local resolution (gated on localMode) so single-player testing works with no server.

Embed

Discord Activity

The game runs as a Discord Activity by reusing the existing web tier — no new app. The game already lives at kbve.com/arcade/arpg; the Activity adds a dedicated embed page + a session-exchange backend on axum-kbve, and the game WebSocket rides the existing arpg.kbve.com.

Pieces:

  • Embed pagearpg.kbve.com/discord/arpg/ (index.html + arpg.js, built by nx run arpg:web-build-discord from apps/agones/arpg/web/src/embed/discord.tsx into the vite app’s dist/discord/arpg/, served by the arpg nginx). Boots the Discord SDK, runs OAuth, fetches a session, then mounts ReactIsoArpgApp with embedSession (skips the Supabase gate + name prompt). The whole Activity now lives on the arpg origin alongside the app + art.
  • Session backendaxum-kbve POST /api/v1/discord/session (on kbve.com — it holds DISCORD_CLIENT_SECRET). Exchanges the OAuth code → Discord token → user → linked KBVE profile (fast-registers via GoTrue admin + tracker.* RPCs on the shared Supabase DB) → mints the Supabase-valid HS256 JWT the game server accepts. Reached cross-origin from the Activity via the /arpg-session mapping. Ported from axum-cryptothrone.
  • Game WS — the existing wss://arpg.kbve.com/ws, reached through a portal URL Mapping /arpg-game → arpg.kbve.com (patchUrlMappings), so the socket is allowed inside the Activity iframe.

Portal config (Discord Developer Portal → Activities → URL Mappings):

MappingTargetPurpose
/ (root)arpg.kbve.com/discord/arpg/Activity page + arpg.js (same-origin, loaded relatively)
/arpg-gamearpg.kbve.comgame WebSocket (/ws, a sibling of the /discord/arpg/ dir)
/arpg-assetsarpg.kbve.comgame art at /assets/... (CORS-enabled); the embed sets the client asset base to /arpg-assets
/arpg-sessionkbve.comthe axum session backend (cross-origin); the embed fetches /.proxy/arpg-session/api/v1/discord/session

Everything the Activity loads is proxied through *.discordsays.com, so each external host (game WS, art, session backend) needs a URL Mapping. The page + arpg.js are same-origin under the portal root, so they load relatively.

Env (axum-kbve): DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, SUPABASE_JWT_SECRET (required); GOTRUE_INTERNAL_URL / SUPABASE_URL for fast-register; leave DISCORD_REDIRECT_URI unset (the RPC authorize flow rejects an explicit redirect_uri). Build (discord): PUBLIC_DISCORD_CLIENT_ID baked in by the --mode discord build in apps/agones/arpg/web/vite.config.ts (web Dockerfile ARG) — must be set or terser dead-code-eliminates the boot body. Register a Discord Activity application pointing its root at arpg.kbve.com/discord/arpg/.

Status — Alpha: endless seeded multi-floor dungeon (stairs + key-locked descent) with one hostile kind, proving end-to-end multiplayer; Discord Activity embed + session backend in place (point a Discord app at it once the image is baked). World content (npcdb/itemdb, zones, loot tables) is a follow-up.

Questions

Frequently asked

What is the ARPG Server?

ARPG Server is the authoritative shared-world host for KBVE's ARPG, a Diablo-style isometric action RPG. It pairs an axum WebSocket router with the simgrid headless Bevy grid simulation, keeping movement validation, occupancy, and snapshot framing server-owned.

How does the ARPG Server stay authoritative?

The server keeps everything in orthogonal tile (x, y) space with integer grid math; collision is tile occupancy. It validates each move against occupancy, drives the sim tick on a tokio multi-thread runtime, and fans out authoritative snapshots. Isometric projection is a client-side render transform only.

Which wire protocol does the ARPG Server speak?

It speaks the shared simgrid JSON protocol (PROTOCOL_VERSION 14), handling Join, Step, MoveTo, Action, Face, Heartbeat, and Leave frames and emitting Welcome, Snapshot, and Ephemeral events for combat, projectiles, floor changes, and stats.