diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..35e5ce3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,120 @@ +# 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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..35f02d3 --- /dev/null +++ b/README.md @@ -0,0 +1,276 @@ +# MUD Server + +A text-based multiplayer RPG (MUD) that accepts connections over SSH. Written in Rust with a data-driven world definition system — rooms, NPCs, objects, races, and classes are all defined in TOML files and can be changed without recompiling. + +## Requirements + +- Rust toolchain (edition 2021+) +- SQLite is bundled via `rusqlite` — no system SQLite needed + +## Building + +```bash +cargo build # builds both mudserver and mudtool +cargo build --release # optimized build +``` + +This produces two binaries: +- `mudserver` — the game server +- `mudtool` — database management CLI/TUI + +## Running the Server + +```bash +./target/debug/mudserver +``` + +### Options + +| Flag | Default | Description | +|------|---------|-------------| +| `--port`, `-p` | `2222` | SSH listen port | +| `--world`, `-w` | `./world` | Path to world data directory | +| `--db`, `-d` | `./mudserver.db` | Path to SQLite database file | + +The server generates a random SSH host key on each startup. The database is created automatically if it doesn't exist. + +### Connecting + +Any SSH client works. The username becomes the player's character name: + +```bash +ssh mycharacter@localhost -p 2222 +``` + +Password and key auth are both accepted (no real authentication — this is a game server, not a secure shell). + +### Environment + +Set `RUST_LOG` to control log verbosity: + +```bash +RUST_LOG=info ./target/release/mudserver # default +RUST_LOG=debug ./target/release/mudserver # verbose +``` + +## World Data + +The world is defined entirely in TOML files under a `world/` directory. The server reads this at startup — no recompilation needed to change content. + +### Directory Structure + +``` +world/ +├── manifest.toml # world name and spawn room +├── races/ # playable races +│ ├── dwarf.toml +│ ├── elf.toml +│ └── ... +├── classes/ # playable classes +│ ├── warrior.toml +│ ├── mage.toml +│ └── ... +└── / # one directory per region + ├── region.toml # region metadata + ├── rooms/ + │ ├── town_square.toml + │ └── ... + ├── npcs/ + │ ├── barkeep.toml + │ └── ... + └── objects/ + ├── rusty_sword.toml + └── ... +``` + +### manifest.toml + +```toml +name = "The Shattered Realm" +spawn_room = "town:town_square" +``` + +### Room + +```toml +name = "Town Square" +description = "A cobblestone square with a fountain." + +[exits] +north = "town:tavern" +south = "town:gate" +east = "town:market" +``` + +Room IDs are `:`. + +### NPC + +```toml +name = "Town Guard" +description = "A bored guard." +room = "town:gate" +base_attitude = "neutral" # friendly, neutral, wary, aggressive, hostile +faction = "guards" # optional — attitude shifts propagate to faction +respawn_secs = 90 # optional — respawn timer after death + +[dialogue] +greeting = "Move along." + +[combat] # optional — omit for weak default stats (20hp/4atk/2def/5xp) +max_hp = 60 +attack = 10 +defense = 8 +xp_reward = 25 +``` + +### Object + +```toml +name = "Rusty Sword" +description = "A battered iron blade." +room = "town:cellar" +kind = "weapon" # weapon, armor, consumable, treasure, or omit +takeable = true + +[stats] +damage = 5 # for weapons +# armor = 4 # for armor +# heal_amount = 30 # for consumables +``` + +### Race + +```toml +name = "Dwarf" +description = "Stout and unyielding." + +[stats] +strength = 1 +dexterity = -1 +constitution = 2 +``` + +### Class + +```toml +name = "Warrior" +description = "Masters of arms and armor." + +[base_stats] +max_hp = 120 +attack = 14 +defense = 12 + +[growth] +hp_per_level = 15 +attack_per_level = 3 +defense_per_level = 2 +``` + +## Game Mechanics + +### Tick System + +The game runs on a **3-second tick cycle**. Combat actions, status effects, NPC AI, and passive regeneration all resolve on ticks rather than immediately. + +### Combat + +Combat is tick-based. When a player enters combat (via `attack` or NPC aggro), they choose actions each tick: + +| Command | Effect | +|---------|--------| +| `attack` / `a` | Strike the enemy (default if no action queued) | +| `defend` / `def` | Brace — doubles effective defense for the tick | +| `flee` | Attempt to escape (success chance based on DEF stat) | +| `use ` | Use a consumable during combat | + +Any NPC can be attacked. Attacking non-hostile NPCs carries attitude penalties (-30 individual, -15 faction) and a warning message but is not blocked. + +### Attitude System + +Every NPC tracks a per-player attitude value from -100 to +100: + +| Range | Label | Behavior | +|-------|-------|----------| +| 50 to 100 | Friendly | Will talk | +| 10 to 49 | Neutral | Will talk | +| -24 to 9 | Wary | Will talk | +| -25 to -74 | Aggressive | Won't talk, attackable | +| -75 to -100 | Hostile | Attacks on sight | + +Attitudes shift from combat interactions and propagate through NPC factions. + +### Status Effects + +Effects like poison and regeneration are stored in the database and tick down every cycle — including while the player is offline. Effects are cleared on death. + +### Passive Regeneration + +Players out of combat regenerate 5% of max HP every 5 ticks (~15 seconds). + +## Database + +SQLite with WAL mode. Tables: + +- `players` — character data (stats, inventory, equipment, room, admin flag) +- `npc_attitudes` — per-player, per-NPC attitude values +- `server_settings` — key-value config (e.g. `registration_open`) +- `status_effects` — active effects with remaining tick counters + +The database is accessed through a `GameDb` trait, making backend swaps possible. + +## mudtool + +Database management tool with both CLI and TUI modes. + +### CLI + +```bash +mudtool --db ./mudserver.db players list +mudtool --db ./mudserver.db players show hero +mudtool --db ./mudserver.db players set-admin hero true +mudtool --db ./mudserver.db players delete hero +mudtool --db ./mudserver.db settings list +mudtool --db ./mudserver.db settings set registration_open false +mudtool --db ./mudserver.db attitudes list hero +mudtool --db ./mudserver.db attitudes set hero town:guard 50 +``` + +### TUI + +```bash +mudtool --db ./mudserver.db tui +``` + +Interactive interface with tabs for Players, Settings, and Attitudes. Navigate with arrow keys, Tab/1/2/3 to switch tabs, `a` to toggle admin, `d` to delete, Enter to edit values, `q` to quit. + +## Admin System + +Players with the `is_admin` flag can use in-game admin commands: + +``` +admin promote Grant admin +admin demote Revoke admin +admin kick Disconnect player +admin teleport Warp to room +admin registration on|off Toggle new player creation +admin announce Broadcast to all +admin heal [player] Full heal (self or target) +admin info Detailed player info +admin setattitude Set attitude +admin list All players (online + saved) +``` + +The first admin must be set via `mudtool players set-admin true`. + +## Registration Gate + +New player creation can be toggled: + +```bash +mudtool settings set registration_open false # block new players +mudtool settings set registration_open true # allow new players (default) +``` + +Or in-game: `admin registration off` / `admin registration on`. Existing players can always log in regardless of this setting.