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
This commit is contained in:
120
AGENTS.md
Normal file
120
AGENTS.md
Normal file
@@ -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<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
|
||||
|
||||
```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
|
||||
276
README.md
Normal file
276
README.md
Normal file
@@ -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
|
||||
│ └── ...
|
||||
└── <region>/ # 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 `<region>:<filename_stem>`.
|
||||
|
||||
### 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 <item>` | 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 <player> Grant admin
|
||||
admin demote <player> Revoke admin
|
||||
admin kick <player> Disconnect player
|
||||
admin teleport <room_id> Warp to room
|
||||
admin registration on|off Toggle new player creation
|
||||
admin announce <message> Broadcast to all
|
||||
admin heal [player] Full heal (self or target)
|
||||
admin info <player> Detailed player info
|
||||
admin setattitude <player> <npc> <value> Set attitude
|
||||
admin list All players (online + saved)
|
||||
```
|
||||
|
||||
The first admin must be set via `mudtool players set-admin <name> 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.
|
||||
Reference in New Issue
Block a user