# 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>` (tokio mutex) - The tick engine and SSH handlers both lock `GameState`. Locks are held briefly. - Player output from ticks is collected into a `HashMap`, 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 ` → enters `CombatState` with `action: Some(Attack)` 2. Player can queue a different action before the tick fires (`defend`, `flee`, `use `) 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//` — no code changes needed 2. NPCs without a `[combat]` section get default stats (20hp/4atk/2def/5xp) 3. Room IDs are `:` 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 ```bash 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