Files
mudserver/AGENTS.md
AI Agent 005c4faf08 Flexible race system with slot-based equipment and dragon race
- Expand race TOML schema: 7 stats, body shape (size/weight/custom slots),
  natural armor and attacks with damage types, resistances, traits/disadvantages,
  regen multipliers, vision types, XP rate, guild compatibility
- Replace equipped_weapon/equipped_armor with slot-based HashMap<String, Object>
- Each race defines available equipment slots; default humanoid slots as fallback
- Combat uses natural weapons/armor from race when no gear equipped
- DB migration from old weapon/armor columns to equipped_json
- Add Dragon race: huge body, custom slots (forelegs/wings/tail), fire breath,
  natural armor 8, fire immune, slow XP rate for balance
- Update all existing races with expanded fields (traits, resistances, vision, regen)
- Objects gain optional slot field; kind=weapon/armor still works as fallback
- Update chargen to display race traits, size, natural attacks, vision
- Update stats display to show equipment and natural bonuses separately
- Update TESTING.md and AGENTS.md with race/slot system documentation

Made-with: Cursor
2026-03-14 15:37:20 -06:00

7.7 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.
  • Races are deeply data-driven: body shape, equipment slots, natural weapons/armor, resistances, traits, regen rates — all defined in TOML. A dragon has different slots than a human.
  • Equipment is slot-based: each race defines available body slots. Items declare their slot. The engine validates compatibility at equip time.
  • Items magically resize — no size restrictions. A dragon can wield a human sword.

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 race

  1. Create world/races/<name>.toml — see dragon.toml for a complex example
  2. Required: name, description
  3. All other fields have sensible defaults via #[serde(default)]
  4. Key sections: [stats] (7 stats), [body] (size, weight, slots), [natural] (armor, attacks), [resistances], [regen], [misc], [guild_compatibility]
  5. traits and disadvantages are free-form string arrays
  6. If [body] slots is empty, defaults to humanoid slots
  7. Natural attacks: use [natural.attacks.<name>] with damage, type, optional cooldown_ticks
  8. Resistances: damage_type → multiplier (0.0 = immune, 1.0 = normal, 1.5 = vulnerable)
  9. xp_rate modifies XP gain (< 1.0 = slower leveling, for powerful races)

New equipment slot

  1. Add the slot name to a race's [body] slots array
  2. Create objects with slot = "<slot_name>" in their TOML
  3. The kind field (weapon/armor) still works as fallback for main_hand/torso
  4. Items can also have an explicit slot field to target any slot

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