From c82f57a720338f182a4fa4e2704a33cb265353d9 Mon Sep 17 00:00:00 2001 From: AI Agent Date: Sat, 14 Mar 2026 13:24:34 -0600 Subject: [PATCH] Initial commit: SSH MUD server with data-driven world MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rust-based MUD server accepting SSH connections on port 2222. Players connect with any SSH client, get dropped into a data-driven world loaded from TOML files at startup. Binary systems: SSH handling (russh), command parser, game state, multiplayer broadcast, ANSI terminal rendering. Data layer: world/ directory with regions, rooms, NPCs, and objects defined as individual TOML files — no recompile needed to modify. Commands: look, movement (n/s/e/w/u/d), say, who, help, quit. Made-with: Cursor --- .gitignore | 1 + Cargo.lock | 2159 ++++++++++++++++++++++++ Cargo.toml | 12 + src/ansi.rs | 54 + src/commands.rs | 416 +++++ src/game.rs | 70 + src/main.rs | 83 + src/ssh.rs | 311 ++++ src/world.rs | 281 +++ world/manifest.toml | 2 + world/town/npcs/barkeep.toml | 4 + world/town/npcs/guard.toml | 4 + world/town/objects/healing_potion.toml | 4 + world/town/objects/rusty_sword.toml | 4 + world/town/region.toml | 2 + world/town/rooms/cellar.toml | 8 + world/town/rooms/dark_alley.toml | 9 + world/town/rooms/forge.toml | 8 + world/town/rooms/gate.toml | 8 + world/town/rooms/market.toml | 9 + world/town/rooms/tavern.toml | 9 + world/town/rooms/temple.toml | 8 + world/town/rooms/town_square.toml | 11 + 23 files changed, 3477 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/ansi.rs create mode 100644 src/commands.rs create mode 100644 src/game.rs create mode 100644 src/main.rs create mode 100644 src/ssh.rs create mode 100644 src/world.rs create mode 100644 world/manifest.toml create mode 100644 world/town/npcs/barkeep.toml create mode 100644 world/town/npcs/guard.toml create mode 100644 world/town/objects/healing_potion.toml create mode 100644 world/town/objects/rusty_sword.toml create mode 100644 world/town/region.toml create mode 100644 world/town/rooms/cellar.toml create mode 100644 world/town/rooms/dark_alley.toml create mode 100644 world/town/rooms/forge.toml create mode 100644 world/town/rooms/gate.toml create mode 100644 world/town/rooms/market.toml create mode 100644 world/town/rooms/tavern.toml create mode 100644 world/town/rooms/temple.toml create mode 100644 world/town/rooms/town_square.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bdb8165 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2159 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bcrypt-pbkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" +dependencies = [ + "blowfish", + "pbkdf2", + "sha2", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "delegate" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "internal-russh-forked-ssh-key" +version = "0.6.11+upstream-0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a77eae781ed6a7709fb15b64862fcca13d886b07c7e2786f5ed34e5e2b9187" +dependencies = [ + "argon2", + "bcrypt-pbkdf", + "ecdsa", + "ed25519-dalek", + "hex", + "hmac", + "p256", + "p384", + "p521", + "rand_core", + "rsa", + "sec1", + "sha1", + "sha2", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mudserver" +version = "0.1.0" +dependencies = [ + "env_logger", + "log", + "russh", + "serde", + "tokio", + "toml", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "rand", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core", + "sha2", +] + +[[package]] +name = "pageant" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb28bd89a207e5cad59072ac4b364b08459d05f90ccfbcdaa920a95857d94430" +dependencies = [ + "byteorder", + "bytes", + "delegate", + "futures", + "log", + "rand", + "thiserror", + "tokio", + "windows", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "pkcs5", + "rand_core", + "spki", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "russh" +version = "0.54.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3ee9363fcf66d434d8015d9ae7d879681206981534c21bfdff8a7e34f52cca" +dependencies = [ + "aes", + "base64ct", + "bitflags", + "block-padding", + "byteorder", + "bytes", + "cbc", + "ctr", + "curve25519-dalek", + "data-encoding", + "delegate", + "der", + "digest", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "enum_dispatch", + "futures", + "generic-array", + "getrandom", + "hex-literal", + "hmac", + "home", + "inout", + "internal-russh-forked-ssh-key", + "log", + "md5", + "num-bigint", + "once_cell", + "p256", + "p384", + "p521", + "pageant", + "pbkdf2", + "pkcs5", + "pkcs8", + "rand", + "rand_core", + "ring", + "russh-cryptovec", + "russh-util", + "sec1", + "sha1", + "sha2", + "signature", + "spki", + "ssh-encoding", + "subtle", + "thiserror", + "tokio", + "typenum", + "zeroize", +] + +[[package]] +name = "russh-cryptovec" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb0ed583ff0f6b4aa44c7867dd7108df01b30571ee9423e250b4cc939f8c6cf" +dependencies = [ + "libc", + "log", + "nix", + "ssh-encoding", + "winapi", +] + +[[package]] +name = "russh-util" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668424a5dde0bcb45b55ba7de8476b93831b4aa2fa6947e145f3b053e22c60b6" +dependencies = [ + "chrono", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "ssh-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "aes", + "aes-gcm", + "cbc", + "chacha20", + "cipher", + "ctr", + "poly1305", + "ssh-encoding", + "subtle", +] + +[[package]] +name = "ssh-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +dependencies = [ + "base64ct", + "bytes", + "pem-rfc7468", + "sha2", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" +dependencies = [ + "windows-core 0.59.0", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-core" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" +dependencies = [ + "windows-implement 0.59.0", + "windows-interface", + "windows-result 0.3.4", + "windows-strings 0.3.1", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-implement" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8662b66 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "mudserver" +version = "0.1.0" +edition = "2021" + +[dependencies] +russh = { version = "0.54", default-features = false, features = ["ring"] } +tokio = { version = "1", features = ["full"] } +serde = { version = "1", features = ["derive"] } +toml = "0.8" +log = "0.4" +env_logger = "0.11" diff --git a/src/ansi.rs b/src/ansi.rs new file mode 100644 index 0000000..5466546 --- /dev/null +++ b/src/ansi.rs @@ -0,0 +1,54 @@ +pub const RESET: &str = "\x1b[0m"; +pub const BOLD: &str = "\x1b[1m"; +pub const DIM: &str = "\x1b[2m"; +pub const RED: &str = "\x1b[31m"; +pub const GREEN: &str = "\x1b[32m"; +pub const YELLOW: &str = "\x1b[33m"; +pub const BLUE: &str = "\x1b[34m"; +pub const MAGENTA: &str = "\x1b[35m"; +pub const CYAN: &str = "\x1b[36m"; +pub const WHITE: &str = "\x1b[37m"; +pub const CLEAR_SCREEN: &str = "\x1b[2J\x1b[H"; + +pub fn color(c: &str, text: &str) -> String { + format!("{c}{text}{RESET}") +} + +pub fn bold(text: &str) -> String { + format!("{BOLD}{text}{RESET}") +} + +pub fn room_name(name: &str) -> String { + format!("{BOLD}{CYAN}{name}{RESET}") +} + +pub fn player_name(name: &str) -> String { + format!("{BOLD}{GREEN}{name}{RESET}") +} + +pub fn direction(dir: &str) -> String { + format!("{YELLOW}{dir}{RESET}") +} + +pub fn system_msg(text: &str) -> String { + format!("{DIM}{text}{RESET}") +} + +pub fn error_msg(text: &str) -> String { + format!("{RED}{text}{RESET}") +} + +pub fn prompt() -> String { + format!("{BOLD}{MAGENTA}> {RESET}") +} + +pub fn welcome_banner() -> String { + format!( + "\r\n{BOLD}{CYAN}\ + ╔════════════════════════════════════════╗\r\n\ + ║ Welcome to the MUD Server ║\r\n\ + ║ A Text-Based Adventure World ║\r\n\ + ╚════════════════════════════════════════╝\r\n\ + {RESET}\r\n" + ) +} diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 0000000..bcb0e5d --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,416 @@ +use russh::server::Session; +use russh::{ChannelId, CryptoVec}; + +use crate::ansi; +use crate::game::SharedState; + +pub struct BroadcastMsg { + pub channel: ChannelId, + pub handle: russh::server::Handle, + pub data: CryptoVec, +} + +pub struct CommandResult { + pub output: String, + pub broadcasts: Vec, + pub quit: bool, +} + +const DIRECTION_ALIASES: &[(&str, &str)] = &[ + ("n", "north"), + ("s", "south"), + ("e", "east"), + ("w", "west"), + ("u", "up"), + ("d", "down"), +]; + +fn resolve_direction(input: &str) -> &str { + for &(alias, full) in DIRECTION_ALIASES { + if input == alias { + return full; + } + } + input +} + +pub async fn execute( + input: &str, + player_id: usize, + state: &SharedState, + session: &mut Session, + channel: ChannelId, +) -> Result { + let input = input.trim(); + if input.is_empty() { + send(session, channel, &ansi::prompt())?; + return Ok(true); + } + + let (cmd, args) = match input.split_once(' ') { + Some((c, a)) => (c.to_lowercase(), a.trim().to_string()), + None => (input.to_lowercase(), String::new()), + }; + + let result = match cmd.as_str() { + "look" | "l" => cmd_look(player_id, state).await, + "go" => cmd_go(player_id, &args, state).await, + "north" | "south" | "east" | "west" | "up" | "down" | "n" | "s" | "e" | "w" | "u" + | "d" => cmd_go(player_id, resolve_direction(&cmd), state).await, + "say" | "'" => cmd_say(player_id, &args, state).await, + "who" => cmd_who(player_id, state).await, + "help" | "h" | "?" => cmd_help(), + "quit" | "exit" => CommandResult { + output: format!("{}\r\n", ansi::system_msg("Farewell, adventurer...")), + broadcasts: Vec::new(), + quit: true, + }, + _ => CommandResult { + output: format!( + "{}\r\n", + ansi::error_msg(&format!("Unknown command: '{cmd}'. Type 'help' for commands.")) + ), + broadcasts: Vec::new(), + quit: false, + }, + }; + + send(session, channel, &result.output)?; + + for msg in result.broadcasts { + let _ = msg.handle.data(msg.channel, msg.data).await; + } + + if result.quit { + return Ok(false); + } + + send(session, channel, &ansi::prompt())?; + Ok(true) +} + +fn send(session: &mut Session, channel: ChannelId, text: &str) -> Result<(), russh::Error> { + session.data(channel, CryptoVec::from(text.as_bytes()))?; + Ok(()) +} + +fn render_room_view( + room_id: &str, + player_id: usize, + state: &tokio::sync::MutexGuard<'_, crate::game::GameState>, +) -> String { + let room = match state.world.get_room(room_id) { + Some(r) => r, + None => return format!("{}\r\n", ansi::error_msg("You are in the void.")), + }; + + let mut out = String::new(); + out.push_str(&format!( + "\r\n{} {}\r\n", + ansi::room_name(&room.name), + ansi::system_msg(&format!("[{}]", room.region)) + )); + out.push_str(&format!(" {}\r\n", room.description)); + + if !room.npcs.is_empty() { + let npc_names: Vec = room + .npcs + .iter() + .filter_map(|id| state.world.get_npc(id)) + .map(|n| ansi::color(ansi::YELLOW, &n.name)) + .collect(); + if !npc_names.is_empty() { + out.push_str(&format!( + "\r\n{}{}\r\n", + ansi::color(ansi::DIM, "Present: "), + npc_names.join(", ") + )); + } + } + + if !room.objects.is_empty() { + let obj_names: Vec = room + .objects + .iter() + .filter_map(|id| state.world.get_object(id)) + .map(|o| ansi::color(ansi::CYAN, &o.name)) + .collect(); + if !obj_names.is_empty() { + out.push_str(&format!( + "{}{}\r\n", + ansi::color(ansi::DIM, "You see: "), + obj_names.join(", ") + )); + } + } + + let others = state.players_in_room(room_id, player_id); + if !others.is_empty() { + let names: Vec = others + .iter() + .map(|c| ansi::player_name(&c.player.name)) + .collect(); + out.push_str(&format!( + "{}{}\r\n", + ansi::color(ansi::GREEN, "Players here: "), + names.join(", ") + )); + } + + if !room.exits.is_empty() { + let mut dirs: Vec<&String> = room.exits.keys().collect(); + dirs.sort(); + let dir_strs: Vec = dirs.iter().map(|d| ansi::direction(d)).collect(); + out.push_str(&format!( + "{} {}\r\n", + ansi::color(ansi::DIM, "Exits:"), + dir_strs.join(", ") + )); + } + + out +} + +async fn cmd_look(player_id: usize, state: &SharedState) -> CommandResult { + let state = state.lock().await; + let room_id = match state.players.get(&player_id) { + Some(c) => c.player.room_id.clone(), + None => { + return CommandResult { + output: format!("{}\r\n", ansi::error_msg("You don't seem to exist.")), + broadcasts: Vec::new(), + quit: false, + } + } + }; + + CommandResult { + output: render_room_view(&room_id, player_id, &state), + broadcasts: Vec::new(), + quit: false, + } +} + +async fn cmd_go(player_id: usize, direction: &str, state: &SharedState) -> CommandResult { + let direction_lower = direction.to_lowercase(); + let direction = resolve_direction(&direction_lower); + let mut state = state.lock().await; + + let (old_room_id, new_room_id, player_name) = { + let conn = match state.players.get(&player_id) { + Some(c) => c, + None => { + return CommandResult { + output: format!("{}\r\n", ansi::error_msg("You don't exist.")), + broadcasts: Vec::new(), + quit: false, + } + } + }; + let room = match state.world.get_room(&conn.player.room_id) { + Some(r) => r, + None => { + return CommandResult { + output: format!("{}\r\n", ansi::error_msg("You are in the void.")), + broadcasts: Vec::new(), + quit: false, + } + } + }; + let dest = match room.exits.get(direction) { + Some(id) => id.clone(), + None => { + return CommandResult { + output: format!( + "{}\r\n", + ansi::error_msg(&format!("You can't go {direction}.")) + ), + broadcasts: Vec::new(), + quit: false, + } + } + }; + ( + conn.player.room_id.clone(), + dest, + conn.player.name.clone(), + ) + }; + + let leave_msg = CryptoVec::from( + format!( + "{}\r\n{}", + ansi::system_msg(&format!("{player_name} heads {direction}.")), + ansi::prompt() + ) + .as_bytes(), + ); + let mut broadcasts = Vec::new(); + for conn in state.players_in_room(&old_room_id, player_id) { + broadcasts.push(BroadcastMsg { + channel: conn.channel, + handle: conn.handle.clone(), + data: leave_msg.clone(), + }); + } + + if let Some(conn) = state.players.get_mut(&player_id) { + conn.player.room_id = new_room_id.clone(); + } + + let arrive_msg = CryptoVec::from( + format!( + "\r\n{}\r\n{}", + ansi::system_msg(&format!("{player_name} arrives.")), + ansi::prompt() + ) + .as_bytes(), + ); + for conn in state.players_in_room(&new_room_id, player_id) { + broadcasts.push(BroadcastMsg { + channel: conn.channel, + handle: conn.handle.clone(), + data: arrive_msg.clone(), + }); + } + + let output = render_room_view(&new_room_id, player_id, &state); + + CommandResult { + output, + broadcasts, + quit: false, + } +} + +async fn cmd_say(player_id: usize, message: &str, state: &SharedState) -> CommandResult { + if message.is_empty() { + return CommandResult { + output: format!("{}\r\n", ansi::error_msg("Say what?")), + broadcasts: Vec::new(), + quit: false, + }; + } + + let state = state.lock().await; + let conn = match state.players.get(&player_id) { + Some(c) => c, + None => { + return CommandResult { + output: format!("{}\r\n", ansi::error_msg("You don't exist.")), + broadcasts: Vec::new(), + quit: false, + } + } + }; + + let name = &conn.player.name; + let room_id = conn.player.room_id.clone(); + + let self_msg = format!( + "{}You say: {}{}\r\n", + ansi::BOLD, + ansi::RESET, + ansi::color(ansi::WHITE, message) + ); + + let other_msg = CryptoVec::from( + format!( + "\r\n{} says: {}{}\r\n{}", + ansi::player_name(name), + ansi::RESET, + ansi::color(ansi::WHITE, message), + ansi::prompt() + ) + .as_bytes(), + ); + + let mut broadcasts = Vec::new(); + for other in state.players_in_room(&room_id, player_id) { + broadcasts.push(BroadcastMsg { + channel: other.channel, + handle: other.handle.clone(), + data: other_msg.clone(), + }); + } + + CommandResult { + output: self_msg, + broadcasts, + quit: false, + } +} + +async fn cmd_who(player_id: usize, state: &SharedState) -> CommandResult { + let state = state.lock().await; + let mut out = String::new(); + out.push_str(&format!( + "\r\n{}\r\n", + ansi::bold("=== Who's Online ===") + )); + + let self_name = state + .players + .get(&player_id) + .map(|c| c.player.name.as_str()) + .unwrap_or(""); + + for conn in state.players.values() { + let room_name = state + .world + .get_room(&conn.player.room_id) + .map(|r| r.name.as_str()) + .unwrap_or("???"); + let marker = if conn.player.name == self_name { + " (you)" + } else { + "" + }; + out.push_str(&format!( + " {} — {}{}\r\n", + ansi::player_name(&conn.player.name), + ansi::room_name(room_name), + ansi::system_msg(marker) + )); + } + + let count = state.players.len(); + out.push_str(&format!( + "{}\r\n", + ansi::system_msg(&format!("{count} player(s) online")) + )); + + CommandResult { + output: out, + broadcasts: Vec::new(), + quit: false, + } +} + +fn cmd_help() -> CommandResult { + let mut out = String::new(); + out.push_str(&format!("\r\n{}\r\n", ansi::bold("=== Commands ==="))); + let cmds = [ + ("look, l", "Look around the current room"), + ( + "go , north/n, south/s, east/e, west/w", + "Move in a direction", + ), + ("say , ' ", "Say something to players in the room"), + ("who", "See who's online"), + ("help, h, ?", "Show this help"), + ("quit, exit", "Leave the game"), + ]; + for (cmd, desc) in cmds { + out.push_str(&format!( + " {:<44} {}\r\n", + ansi::color(ansi::YELLOW, cmd), + ansi::color(ansi::DIM, desc) + )); + } + CommandResult { + output: out, + broadcasts: Vec::new(), + quit: false, + } +} diff --git a/src/game.rs b/src/game.rs new file mode 100644 index 0000000..bb1ef03 --- /dev/null +++ b/src/game.rs @@ -0,0 +1,70 @@ +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +use russh::server::Handle; +use russh::ChannelId; + +use crate::world::World; + +pub struct Player { + pub name: String, + pub room_id: String, +} + +pub struct PlayerConnection { + pub player: Player, + pub channel: ChannelId, + pub handle: Handle, +} + +pub struct GameState { + pub world: World, + pub players: HashMap, +} + +pub type SharedState = Arc>; + +impl GameState { + pub fn new(world: World) -> Self { + GameState { + world, + players: HashMap::new(), + } + } + + pub fn spawn_room(&self) -> &str { + &self.world.spawn_room + } + + pub fn add_player(&mut self, id: usize, name: String, channel: ChannelId, handle: Handle) { + let room_id = self.world.spawn_room.clone(); + self.players.insert( + id, + PlayerConnection { + player: Player { name, room_id }, + channel, + handle, + }, + ); + } + + pub fn remove_player(&mut self, id: usize) -> Option { + self.players.remove(&id) + } + + pub fn players_in_room(&self, room_id: &str, exclude_id: usize) -> Vec<&PlayerConnection> { + self.players + .iter() + .filter(|(&id, conn)| conn.player.room_id == room_id && id != exclude_id) + .map(|(_, conn)| conn) + .collect() + } + + pub fn all_player_names(&self) -> Vec<&str> { + self.players + .values() + .map(|c| c.player.name.as_str()) + .collect() + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..44d1a0f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,83 @@ +mod ansi; +mod commands; +mod game; +mod ssh; +mod world; + +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::Mutex; + +use russh::keys::ssh_key::rand_core::OsRng; +use russh::server::Server as _; +use tokio::net::TcpListener; + +const DEFAULT_PORT: u16 = 2222; +const DEFAULT_WORLD_DIR: &str = "./world"; + +#[tokio::main] +async fn main() { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + + let mut port = DEFAULT_PORT; + let mut world_dir = PathBuf::from(DEFAULT_WORLD_DIR); + + let args: Vec = std::env::args().collect(); + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--port" | "-p" => { + i += 1; + port = args + .get(i) + .and_then(|s| s.parse().ok()) + .expect("--port requires a number"); + } + "--world" | "-w" => { + i += 1; + world_dir = PathBuf::from( + args.get(i).expect("--world requires a path"), + ); + } + "--help" => { + eprintln!("Usage: mudserver [--port PORT] [--world PATH]"); + eprintln!(" --port, -p Listen port (default: {DEFAULT_PORT})"); + eprintln!(" --world, -w World directory (default: {DEFAULT_WORLD_DIR})"); + std::process::exit(0); + } + other => { + eprintln!("Unknown argument: {other}"); + eprintln!("Run with --help for usage."); + std::process::exit(1); + } + } + i += 1; + } + + log::info!("Loading world from: {}", world_dir.display()); + let loaded_world = world::World::load(&world_dir).unwrap_or_else(|e| { + eprintln!("Failed to load world: {e}"); + std::process::exit(1); + }); + + let key = + russh::keys::PrivateKey::random(&mut OsRng, russh::keys::Algorithm::Ed25519).unwrap(); + + let config = russh::server::Config { + inactivity_timeout: Some(std::time::Duration::from_secs(3600)), + auth_rejection_time: std::time::Duration::from_secs(1), + auth_rejection_time_initial: Some(std::time::Duration::from_secs(0)), + keys: vec![key], + ..Default::default() + }; + let config = Arc::new(config); + + let state = Arc::new(Mutex::new(game::GameState::new(loaded_world))); + let mut server = ssh::MudServer::new(state); + + let listener = TcpListener::bind(("0.0.0.0", port)).await.unwrap(); + log::info!("MUD server listening on 0.0.0.0:{port}"); + log::info!("Connect with: ssh @localhost -p {port}"); + + server.run_on_socket(config, &listener).await.unwrap(); +} diff --git a/src/ssh.rs b/src/ssh.rs new file mode 100644 index 0000000..1e29f2e --- /dev/null +++ b/src/ssh.rs @@ -0,0 +1,311 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; + +use russh::server::{Auth, Handle, Msg, Server, Session}; +use russh::{Channel, ChannelId, CryptoVec, Pty}; + +use crate::ansi; +use crate::commands; +use crate::game::SharedState; + +pub struct MudServer { + pub state: SharedState, + next_id: AtomicUsize, +} + +impl MudServer { + pub fn new(state: SharedState) -> Self { + MudServer { + state, + next_id: AtomicUsize::new(1), + } + } +} + +impl Server for MudServer { + type Handler = MudHandler; + + fn new_client(&mut self, addr: Option) -> MudHandler { + let id = self.next_id.fetch_add(1, Ordering::SeqCst); + log::info!("New connection (id={id}) from {addr:?}"); + MudHandler { + id, + username: String::new(), + channel: None, + handle: None, + line_buffer: String::new(), + state: self.state.clone(), + } + } + + fn handle_session_error(&mut self, error: ::Error) { + log::error!("Session error: {error:#?}"); + } +} + +pub struct MudHandler { + id: usize, + username: String, + channel: Option, + handle: Option, + line_buffer: String, + state: SharedState, +} + +impl MudHandler { + async fn register_player(&self, session: &mut Session, channel: ChannelId) { + let handle = session.handle(); + let mut state = self.state.lock().await; + state.add_player(self.id, self.username.clone(), channel, handle); + + let spawn_room = state.spawn_room().to_string(); + let arrival = CryptoVec::from( + format!( + "\r\n{}\r\n{}", + ansi::system_msg(&format!("{} has entered the world.", self.username)), + ansi::prompt() + ) + .as_bytes(), + ); + let others: Vec<_> = state + .players_in_room(&spawn_room, self.id) + .iter() + .map(|c| (c.channel, c.handle.clone())) + .collect(); + drop(state); + + for (ch, h) in others { + let _ = h.data(ch, arrival.clone()).await; + } + } + + async fn send_welcome(&self, session: &mut Session, channel: ChannelId) { + let state = self.state.lock().await; + let world_name = state.world.name.clone(); + drop(state); + + let welcome = format!( + "{}\r\n{}Welcome to {}, {}! Type {} to get started.\r\n\r\n", + ansi::CLEAR_SCREEN, + ansi::welcome_banner(), + ansi::bold(&world_name), + ansi::player_name(&self.username), + ansi::color(ansi::YELLOW, "'help'") + ); + let _ = session.data(channel, CryptoVec::from(welcome.as_bytes())); + } + + async fn show_room(&self, session: &mut Session, channel: ChannelId) { + let state = self.state.lock().await; + let room_id = match state.players.get(&self.id) { + Some(c) => c.player.room_id.clone(), + None => return, + }; + let room = match state.world.get_room(&room_id) { + Some(r) => r, + None => return, + }; + + let mut out = String::new(); + out.push_str(&format!( + "{} {}\r\n", + ansi::room_name(&room.name), + ansi::system_msg(&format!("[{}]", room.region)) + )); + out.push_str(&format!(" {}\r\n", room.description)); + + if !room.npcs.is_empty() { + let npc_names: Vec = room + .npcs + .iter() + .filter_map(|id| state.world.get_npc(id)) + .map(|n| ansi::color(ansi::YELLOW, &n.name)) + .collect(); + if !npc_names.is_empty() { + out.push_str(&format!( + "\r\n{}{}\r\n", + ansi::color(ansi::DIM, "Present: "), + npc_names.join(", ") + )); + } + } + + if !room.exits.is_empty() { + let mut dirs: Vec<&String> = room.exits.keys().collect(); + dirs.sort(); + let dir_strs: Vec = dirs.iter().map(|d| ansi::direction(d)).collect(); + out.push_str(&format!( + "{} {}\r\n", + ansi::color(ansi::DIM, "Exits:"), + dir_strs.join(", ") + )); + } + + out.push_str(&ansi::prompt()); + let _ = session.data(channel, CryptoVec::from(out.as_bytes())); + } + + async fn handle_disconnect(&self) { + let mut state = self.state.lock().await; + if let Some(conn) = state.remove_player(self.id) { + let departure = CryptoVec::from( + format!( + "\r\n{}\r\n{}", + ansi::system_msg(&format!("{} has left the world.", conn.player.name)), + ansi::prompt() + ) + .as_bytes(), + ); + let others: Vec<_> = state + .players_in_room(&conn.player.room_id, self.id) + .iter() + .map(|c| (c.channel, c.handle.clone())) + .collect(); + drop(state); + + for (ch, h) in others { + let _ = h.data(ch, departure.clone()).await; + } + + log::info!("{} disconnected (id={})", conn.player.name, self.id); + } + } +} + +impl russh::server::Handler for MudHandler { + type Error = russh::Error; + + async fn auth_password(&mut self, user: &str, _password: &str) -> Result { + self.username = user.to_string(); + log::info!("Auth accepted for '{}' (id={})", user, self.id); + Ok(Auth::Accept) + } + + async fn auth_publickey( + &mut self, + user: &str, + _key: &russh::keys::ssh_key::PublicKey, + ) -> Result { + self.username = user.to_string(); + log::info!("Pubkey auth accepted for '{}' (id={})", user, self.id); + Ok(Auth::Accept) + } + + async fn auth_none(&mut self, user: &str) -> Result { + self.username = user.to_string(); + Ok(Auth::Accept) + } + + async fn channel_open_session( + &mut self, + channel: Channel, + session: &mut Session, + ) -> Result { + self.channel = Some(channel.id()); + self.handle = Some(session.handle()); + Ok(true) + } + + async fn pty_request( + &mut self, + channel: ChannelId, + _term: &str, + _col_width: u32, + _row_height: u32, + _pix_width: u32, + _pix_height: u32, + _modes: &[(Pty, u32)], + session: &mut Session, + ) -> Result<(), Self::Error> { + session.channel_success(channel)?; + Ok(()) + } + + async fn shell_request( + &mut self, + channel: ChannelId, + session: &mut Session, + ) -> Result<(), Self::Error> { + session.channel_success(channel)?; + + self.send_welcome(session, channel).await; + self.register_player(session, channel).await; + self.show_room(session, channel).await; + + Ok(()) + } + + async fn data( + &mut self, + channel: ChannelId, + data: &[u8], + session: &mut Session, + ) -> Result<(), Self::Error> { + for &byte in data { + match byte { + 3 | 4 => { + self.handle_disconnect().await; + session.close(channel)?; + return Ok(()); + } + 8 | 127 => { + if !self.line_buffer.is_empty() { + self.line_buffer.pop(); + session.data(channel, CryptoVec::from(&b"\x08 \x08"[..]))?; + } + } + b'\r' | b'\n' => { + if byte == b'\n' && self.line_buffer.is_empty() { + continue; + } + session.data(channel, CryptoVec::from(&b"\r\n"[..]))?; + + let line = std::mem::take(&mut self.line_buffer); + let keep_going = + commands::execute(&line, self.id, &self.state, session, channel).await?; + + if !keep_going { + self.handle_disconnect().await; + session.close(channel)?; + return Ok(()); + } + } + 27 => {} + b if b < 32 => {} + _ => { + self.line_buffer.push(byte as char); + session.data(channel, CryptoVec::from(&[byte][..]))?; + } + } + } + Ok(()) + } + + async fn channel_eof( + &mut self, + _channel: ChannelId, + _session: &mut Session, + ) -> Result<(), Self::Error> { + self.handle_disconnect().await; + Ok(()) + } + + async fn channel_close( + &mut self, + _channel: ChannelId, + _session: &mut Session, + ) -> Result<(), Self::Error> { + self.handle_disconnect().await; + Ok(()) + } +} + +impl Drop for MudHandler { + fn drop(&mut self) { + let state = self.state.clone(); + let id = self.id; + tokio::spawn(async move { + let mut state = state.lock().await; + state.remove_player(id); + }); + } +} diff --git a/src/world.rs b/src/world.rs new file mode 100644 index 0000000..1b15a43 --- /dev/null +++ b/src/world.rs @@ -0,0 +1,281 @@ +use serde::Deserialize; +use std::collections::HashMap; +use std::path::Path; + +// --- On-disk TOML schemas --- + +#[derive(Deserialize)] +pub struct Manifest { + pub name: String, + pub spawn_room: String, +} + +#[derive(Deserialize)] +pub struct RegionFile { + pub name: String, + #[serde(default)] + pub description: String, +} + +#[derive(Deserialize)] +pub struct RoomFile { + pub name: String, + pub description: String, + #[serde(default)] + pub exits: HashMap, +} + +#[derive(Deserialize)] +pub struct NpcFile { + pub name: String, + pub description: String, + pub room: String, + #[serde(default)] + pub dialogue: Option, +} + +#[derive(Deserialize)] +pub struct ObjectFile { + pub name: String, + pub description: String, + #[serde(default)] + pub room: Option, + #[serde(default)] + pub kind: Option, +} + +// --- Runtime types --- + +pub struct Room { + pub id: String, + pub region: String, + pub name: String, + pub description: String, + pub exits: HashMap, + pub npcs: Vec, + pub objects: Vec, +} + +pub struct Npc { + pub id: String, + pub name: String, + pub description: String, + pub room: String, + pub dialogue: Option, +} + +pub struct Object { + pub id: String, + pub name: String, + pub description: String, + pub room: Option, + pub kind: Option, +} + +pub struct World { + pub name: String, + pub spawn_room: String, + pub rooms: HashMap, + pub npcs: HashMap, + pub objects: HashMap, +} + +impl World { + pub fn load(world_dir: &Path) -> Result { + let manifest_path = world_dir.join("manifest.toml"); + let manifest: Manifest = load_toml(&manifest_path)?; + + let mut rooms = HashMap::new(); + let mut npcs = HashMap::new(); + let mut objects = HashMap::new(); + + let entries = std::fs::read_dir(world_dir) + .map_err(|e| format!("Cannot read world dir {}: {e}", world_dir.display()))?; + + let mut region_dirs: Vec<_> = entries + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false)) + .collect(); + region_dirs.sort_by_key(|e| e.file_name()); + + for entry in region_dirs { + let region_name = entry.file_name().to_string_lossy().to_string(); + let region_path = entry.path(); + + let region_toml = region_path.join("region.toml"); + if !region_toml.exists() { + log::debug!("Skipping directory without region.toml: {}", region_path.display()); + continue; + } + + let _region_meta: RegionFile = load_toml(®ion_toml)?; + log::info!("Loading region: {region_name}"); + + load_entities_from_dir( + ®ion_path.join("rooms"), + ®ion_name, + &mut |id, content| { + let rf: RoomFile = toml::from_str(content) + .map_err(|e| format!("Bad room {id}: {e}"))?; + rooms.insert( + id.clone(), + Room { + id: id.clone(), + region: region_name.clone(), + name: rf.name, + description: rf.description, + exits: rf.exits, + npcs: Vec::new(), + objects: Vec::new(), + }, + ); + Ok(()) + }, + )?; + + load_entities_from_dir( + ®ion_path.join("npcs"), + ®ion_name, + &mut |id, content| { + let nf: NpcFile = toml::from_str(content) + .map_err(|e| format!("Bad npc {id}: {e}"))?; + npcs.insert( + id.clone(), + Npc { + id: id.clone(), + name: nf.name, + description: nf.description, + room: nf.room.clone(), + dialogue: nf.dialogue, + }, + ); + Ok(()) + }, + )?; + + load_entities_from_dir( + ®ion_path.join("objects"), + ®ion_name, + &mut |id, content| { + let of: ObjectFile = toml::from_str(content) + .map_err(|e| format!("Bad object {id}: {e}"))?; + objects.insert( + id.clone(), + Object { + id: id.clone(), + name: of.name, + description: of.description, + room: of.room, + kind: of.kind, + }, + ); + Ok(()) + }, + )?; + } + + // Place NPCs and objects into their rooms + for npc in npcs.values() { + if let Some(room) = rooms.get_mut(&npc.room) { + room.npcs.push(npc.id.clone()); + } + } + for obj in objects.values() { + if let Some(ref room_id) = obj.room { + if let Some(room) = rooms.get_mut(room_id) { + room.objects.push(obj.id.clone()); + } + } + } + + // Validate + if !rooms.contains_key(&manifest.spawn_room) { + return Err(format!( + "Spawn room '{}' not found in loaded rooms", + manifest.spawn_room + )); + } + + for room in rooms.values() { + for (dir, target) in &room.exits { + if !rooms.contains_key(target) { + return Err(format!( + "Room '{}' exit '{dir}' points to unknown room '{target}'", + room.id + )); + } + } + } + + log::info!( + "World '{}' loaded: {} rooms, {} npcs, {} objects", + manifest.name, + rooms.len(), + npcs.len(), + objects.len() + ); + + Ok(World { + name: manifest.name, + spawn_room: manifest.spawn_room, + rooms, + npcs, + objects, + }) + } + + pub fn get_room(&self, id: &str) -> Option<&Room> { + self.rooms.get(id) + } + + pub fn get_npc(&self, id: &str) -> Option<&Npc> { + self.npcs.get(id) + } + + pub fn get_object(&self, id: &str) -> Option<&Object> { + self.objects.get(id) + } +} + +fn load_toml(path: &Path) -> Result { + let content = std::fs::read_to_string(path) + .map_err(|e| format!("Cannot read {}: {e}", path.display()))?; + toml::from_str(&content).map_err(|e| format!("Bad TOML in {}: {e}", path.display())) +} + +fn load_entities_from_dir( + dir: &Path, + region: &str, + handler: &mut dyn FnMut(String, &str) -> Result<(), String>, +) -> Result<(), String> { + if !dir.exists() { + return Ok(()); + } + + let mut entries: Vec<_> = std::fs::read_dir(dir) + .map_err(|e| format!("Cannot read {}: {e}", dir.display()))? + .filter_map(|e| e.ok()) + .filter(|e| { + e.path() + .extension() + .map(|ext| ext == "toml") + .unwrap_or(false) + }) + .collect(); + entries.sort_by_key(|e| e.file_name()); + + for entry in entries { + let stem = entry + .path() + .file_stem() + .unwrap() + .to_string_lossy() + .to_string(); + let id = format!("{region}:{stem}"); + let content = std::fs::read_to_string(entry.path()) + .map_err(|e| format!("Cannot read {}: {e}", entry.path().display()))?; + handler(id, &content)?; + } + + Ok(()) +} diff --git a/world/manifest.toml b/world/manifest.toml new file mode 100644 index 0000000..0c97246 --- /dev/null +++ b/world/manifest.toml @@ -0,0 +1,2 @@ +name = "The Shattered Realm" +spawn_room = "town:town_square" diff --git a/world/town/npcs/barkeep.toml b/world/town/npcs/barkeep.toml new file mode 100644 index 0000000..2c83f92 --- /dev/null +++ b/world/town/npcs/barkeep.toml @@ -0,0 +1,4 @@ +name = "Grizzled Barkeep" +description = "A weathered man with thick forearms and a permanent scowl. He polishes the same mug endlessly." +room = "town:tavern" +dialogue = "Welcome to The Rusty Tankard. We've got ale, and we've got stronger ale. Pick one." diff --git a/world/town/npcs/guard.toml b/world/town/npcs/guard.toml new file mode 100644 index 0000000..aba196b --- /dev/null +++ b/world/town/npcs/guard.toml @@ -0,0 +1,4 @@ +name = "Town Guard" +description = "A bored-looking guard in dented chainmail. He leans on his spear and watches passersby." +room = "town:gate" +dialogue = "Move along. Nothing to see here." diff --git a/world/town/objects/healing_potion.toml b/world/town/objects/healing_potion.toml new file mode 100644 index 0000000..e7eb7a1 --- /dev/null +++ b/world/town/objects/healing_potion.toml @@ -0,0 +1,4 @@ +name = "Healing Potion" +description = "A small glass vial filled with a shimmering red liquid." +room = "town:temple" +kind = "consumable" diff --git a/world/town/objects/rusty_sword.toml b/world/town/objects/rusty_sword.toml new file mode 100644 index 0000000..7b236c9 --- /dev/null +++ b/world/town/objects/rusty_sword.toml @@ -0,0 +1,4 @@ +name = "Rusty Sword" +description = "A battered iron blade with a cracked leather grip. It's seen better days." +room = "town:cellar" +kind = "weapon" diff --git a/world/town/region.toml b/world/town/region.toml new file mode 100644 index 0000000..b65b458 --- /dev/null +++ b/world/town/region.toml @@ -0,0 +1,2 @@ +name = "Thornwall" +description = "A fortified trading town at the crossroads of the known world." diff --git a/world/town/rooms/cellar.toml b/world/town/rooms/cellar.toml new file mode 100644 index 0000000..0b0a899 --- /dev/null +++ b/world/town/rooms/cellar.toml @@ -0,0 +1,8 @@ +name = "Tavern Cellar" +description = """\ +A damp underground room lined with barrels and crates. Cobwebs drape \ +the ceiling. A single lantern sputters on a hook, casting long shadows. \ +Something skitters in the dark corners.""" + +[exits] +up = "town:tavern" diff --git a/world/town/rooms/dark_alley.toml b/world/town/rooms/dark_alley.toml new file mode 100644 index 0000000..c741c76 --- /dev/null +++ b/world/town/rooms/dark_alley.toml @@ -0,0 +1,9 @@ +name = "Dark Alley" +description = """\ +A narrow passage between crumbling stone walls. Puddles of dubious origin \ +dot the ground. Shadows pool in doorways, and you catch the faint sound \ +of whispered conversation from somewhere above.""" + +[exits] +north = "town:town_square" +south = "town:gate" diff --git a/world/town/rooms/forge.toml b/world/town/rooms/forge.toml new file mode 100644 index 0000000..9592e1b --- /dev/null +++ b/world/town/rooms/forge.toml @@ -0,0 +1,8 @@ +name = "Blacksmith's Forge" +description = """\ +Waves of heat roll from a roaring forge. A massive anvil stands in the \ +center, scarred by countless hammer strikes. Weapons and armor line the \ +walls, gleaming with fresh oil.""" + +[exits] +west = "town:market" diff --git a/world/town/rooms/gate.toml b/world/town/rooms/gate.toml new file mode 100644 index 0000000..88644a2 --- /dev/null +++ b/world/town/rooms/gate.toml @@ -0,0 +1,8 @@ +name = "City Gate" +description = """\ +Towering iron-reinforced wooden gates mark the southern edge of town. \ +Guards in dented armor lean on their spears, watching the dusty road \ +that stretches into the wilderness beyond.""" + +[exits] +north = "town:dark_alley" diff --git a/world/town/rooms/market.toml b/world/town/rooms/market.toml new file mode 100644 index 0000000..dd939f7 --- /dev/null +++ b/world/town/rooms/market.toml @@ -0,0 +1,9 @@ +name = "Market Row" +description = """\ +Colorful stalls line both sides of a narrow street. Merchants hawk their \ +wares — bolts of cloth, exotic spices, gleaming trinkets. The air is thick \ +with competing smells and the chatter of commerce.""" + +[exits] +west = "town:town_square" +east = "town:forge" diff --git a/world/town/rooms/tavern.toml b/world/town/rooms/tavern.toml new file mode 100644 index 0000000..3a2a869 --- /dev/null +++ b/world/town/rooms/tavern.toml @@ -0,0 +1,9 @@ +name = "The Rusty Tankard" +description = """\ +A warm tavern with low wooden beams and the smell of roasting meat. \ +A crackling fireplace casts dancing shadows across rough-hewn tables. \ +The barkeep polishes a mug behind the counter, eyeing newcomers.""" + +[exits] +south = "town:town_square" +down = "town:cellar" diff --git a/world/town/rooms/temple.toml b/world/town/rooms/temple.toml new file mode 100644 index 0000000..e842425 --- /dev/null +++ b/world/town/rooms/temple.toml @@ -0,0 +1,8 @@ +name = "Temple of the Dawn" +description = """\ +A serene stone temple bathed in golden light filtering through stained \ +glass windows. Rows of wooden pews face an altar adorned with candles. \ +The air hums with quiet reverence.""" + +[exits] +east = "town:town_square" diff --git a/world/town/rooms/town_square.toml b/world/town/rooms/town_square.toml new file mode 100644 index 0000000..440114f --- /dev/null +++ b/world/town/rooms/town_square.toml @@ -0,0 +1,11 @@ +name = "Town Square" +description = """\ +You stand in the heart of Thornwall. A worn stone fountain sits at the \ +center, water trickling quietly. Cobblestone paths branch in every \ +direction. The sounds of merchants and travelers fill the air.""" + +[exits] +north = "town:tavern" +east = "town:market" +west = "town:temple" +south = "town:dark_alley"