Files
mudserver/AGENTS.md
AI Agent 3f164e4697 Add README and agent guidelines documentation
README covers building, running, connecting, world data format,
game mechanics, database schema, mudtool usage, and admin system.
AGENTS.md provides architecture overview, concurrency model, coding
conventions, and step-by-step guides for extending the codebase.

Made-with: Cursor
2026-03-14 15:12:49 -06:00

6.3 KiB

Agent Guidelines — MUD Server

Instructions for AI coding agents working on this codebase.

Project Overview

This is a Rust MUD server that accepts SSH connections. The architecture separates the binary server/game engine from data-driven world content defined in TOML files.

Key design decisions:

  • Game runs on a tick-based loop (~3s). Combat, status effects, NPC AI, and regen resolve on ticks, not on player input.
  • Any NPC can be attacked. Hostility is a consequence system, not a permission system.
  • Status effects persist in the database and continue ticking while players are offline.
  • The GameDb trait abstracts the database backend (currently SQLite).
  • World data is loaded from TOML at startup. Content changes don't require recompilation.

Architecture

src/
├── main.rs        Entry point: arg parsing, world/db init, tick spawn, SSH listen
├── lib.rs         Library crate — exports all modules for shared use by mudserver + mudtool
├── ssh.rs         russh server/handler: connection lifecycle, chargen flow, command dispatch
├── game.rs        Core runtime state: Player, GameState, SharedState, XorShift64 RNG
├── commands.rs    Player command parsing and execution (immediate + queued actions)
├── combat.rs      Tick-based combat resolution: attack, defend, flee, item use
├── tick.rs        Background tick engine: NPC AI, combat rounds, effects, respawns, regen
├── admin.rs       Admin command implementations
├── chargen.rs     Character creation state machine
├── db.rs          GameDb trait + SqliteDb implementation
├── world.rs       TOML schema types, runtime types, World::load()
├── ansi.rs        ANSI escape code helpers
└── bin/
    └── mudtool.rs External DB management tool (CLI + TUI)

Concurrency Model

  • SharedState = Arc<Mutex<GameState>> (tokio mutex)
  • The tick engine and SSH handlers both lock GameState. Locks are held briefly.
  • Player output from ticks is collected into a HashMap<pid, String>, then sent after dropping the lock.
  • Never hold the game state lock across .await points that involve network I/O.

How Combat Works

  1. Player types attack <npc> → enters CombatState with action: Some(Attack)
  2. Player can queue a different action before the tick fires (defend, flee, use <item>)
  3. Tick engine iterates all players in combat, calls combat::resolve_combat_tick()
  4. Resolution: execute player action → NPC counter-attacks → check death → clear action
  5. If no action was queued, default is Attack
  6. NPC auto-aggro: hostile NPCs initiate combat with players in their room on each tick

How Status Effects Work

  • Stored in status_effects table: (player_name, kind, remaining_ticks, magnitude)
  • tick_all_effects() decrements all rows, returns them, then deletes expired ones
  • Online players: HP modified in-memory + saved to DB
  • Offline players: HP modified directly in players table via DB
  • Effects cleared on player death

Adding New Features

New command

  1. Add handler function in commands.rs (follow cmd_* pattern)
  2. Add match arm in the execute() dispatch
  3. If it's a combat action, queue it on combat.action instead of executing immediately
  4. Update cmd_help() text
  5. Add to TESTING.md checklist

New status effect kind

  1. Add a match arm in tick.rs under the effect processing loop
  2. Handle both online (in-memory) and offline (DB-only) players
  3. The kind field is a free-form string — no enum needed

New NPC behavior

  1. NPC AI runs in tick.rs at the top of the tick cycle
  2. Currently: hostile NPCs auto-engage players in their room
  3. Add new behaviors there (e.g. NPC movement, dialogue triggers)

New world content

  1. Add TOML files under world/<region>/ — no code changes needed
  2. NPCs without a [combat] section get default stats (20hp/4atk/2def/5xp)
  3. Room IDs are <region_dir>:<filename_stem>
  4. Cross-region exits work — just reference the full ID

New DB table

  1. Add CREATE TABLE IF NOT EXISTS in SqliteDb::open()
  2. Add trait methods to GameDb
  3. Implement for SqliteDb
  4. Update mudtool if the data should be manageable externally

Conventions

  • No unnecessary comments. Don't narrate what code does. Comments explain non-obvious intent only.
  • KISS. Don't add abstraction layers unless there's a concrete second use case.
  • Borrow checker patterns: This codebase frequently needs to work around Rust's borrow rules when mutating GameState. Common pattern: read data into locals, drop the borrow, then mutate. See cmd_attack for examples.
  • ANSI formatting: Use helpers in ansi.rs. Don't hardcode escape codes elsewhere.
  • Error handling: Game logic uses Option/early-return patterns, not Result chains. DB errors are silently swallowed (logged at debug level) — the game should not crash from a failed DB write.
  • Testing: There is no automated test suite. TESTING.md contains a manual checklist and smoke test script. Run through relevant sections before committing.

Common Pitfalls

  • Holding the lock too long: state.lock().await grabs a tokio mutex. If you .await network I/O while holding it, the tick engine and all other players will block. Collect data, drop the lock, then send.
  • Borrow splitting: GameState owns world, players, npc_instances, and db. You can't borrow players mutably while also reading world through the same &mut GameState. Extract what you need from world first.
  • Tick engine ordering: The tick processes in this order: respawns → NPC aggro → combat rounds → status effects → passive regen → send messages. Changing this order can create bugs (e.g. regen before combat means players heal before taking damage).
  • Offline player effects: Status effects tick in the DB for ALL players. If you add a new effect, handle both the online path (modify in-memory player) and offline path (load/modify/save via SavedPlayer).

Build & Run

cargo build
./target/debug/mudserver --world ./world --db ./mudserver.db --port 2222
ssh testplayer@localhost -p 2222

Git

  • Remote: https://git.coven.systems/lily/mudserver
  • Commit messages: imperative mood, explain why not what
  • Update TESTING.md when adding features
  • Run through the relevant test checklist sections before pushing