├── logs └── .keep ├── scripts └── .keep ├── data ├── world │ ├── .keep │ └── are_to_json.py ├── accounts │ └── .keep ├── characters │ └── .keep └── ataxia.toml ├── ataxia-core ├── LICENSE-MIT ├── LICENSE-APACHE ├── src │ ├── lib.rs │ └── config.rs ├── build.rs └── Cargo.toml ├── ataxia-engine ├── LICENSE-MIT ├── LICENSE-APACHE ├── src │ ├── lib.rs │ ├── engine.rs │ └── bin │ │ └── engine.rs ├── build.rs └── Cargo.toml ├── ataxia-portal ├── LICENSE-MIT ├── LICENSE-APACHE ├── src │ ├── portal │ │ └── handlers │ │ │ ├── mod.rs │ │ │ └── telnet.rs │ ├── lib.rs │ ├── bin │ │ └── portal.rs │ └── portal.rs ├── build.rs └── Cargo.toml ├── tools ├── generate-authors.sh └── release-edit.sh ├── .travis.yml ├── CREDITS ├── AUTHORS ├── .gitignore ├── bin └── startup.py ├── Cargo.toml ├── LICENSE-MIT ├── docs ├── enginedesign.md ├── randomideas.md └── gamedesign.md ├── Makefile ├── CONTRIBUTING ├── README.md ├── LICENSE-APACHE └── Cargo.lock /logs/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/world/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/accounts/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/characters/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ataxia-core/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /ataxia-core/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /ataxia-engine/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /ataxia-portal/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | ../LICENSE-MIT -------------------------------------------------------------------------------- /ataxia-engine/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /ataxia-portal/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | ../LICENSE-APACHE -------------------------------------------------------------------------------- /ataxia-portal/src/portal/handlers/mod.rs: -------------------------------------------------------------------------------- 1 | //! Submodules for various network I/O handlers 2 | pub mod telnet; 3 | //pub mod websockets; 4 | -------------------------------------------------------------------------------- /tools/generate-authors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Ataxia Authors:" > AUTHORS 4 | git log --pretty=format:"%an <%ae>" | sort -u >> AUTHORS 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - rust: nightly 9 | notifications: 10 | email: false 11 | -------------------------------------------------------------------------------- /CREDITS: -------------------------------------------------------------------------------- 1 | Code taken from: 2 | 3 | libtelnet - https://github.com/seanmiddleditch/libtelnet 4 | 5 | A lot of thanks to the forums at MudLab (http://www.mudlab.org/forum/) and Gammon Software Forums 6 | (http://www.gammon.com.au/forum/) 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Lead Developer 2 | -------------- 3 | 4 | Justin Seabrook-Rocha (Xenith) 5 | 6 | 7 | Some Guy who looks at things occasionally 8 | ----------------------------------------- 9 | 10 | Jason Anderson (Montrey) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | bin/engine 3 | bin/portal 4 | cmd/*/package.go 5 | *.pid 6 | *~ 7 | logs/ 8 | 9 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 10 | *.o 11 | *.a 12 | *.so 13 | 14 | *.test 15 | 16 | # Ignore the rust target directory 17 | target 18 | 19 | # Ignore the Go vendor directory 20 | vendor/ 21 | -------------------------------------------------------------------------------- /ataxia-engine/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Ataxia game engine library code 2 | #![deny( 3 | missing_debug_implementations, 4 | missing_copy_implementations, 5 | trivial_casts, 6 | trivial_numeric_casts, 7 | unsafe_code, 8 | unused_import_braces, 9 | unused_qualifications, 10 | clippy::all, 11 | clippy::pedantic 12 | )] 13 | #![forbid(unsafe_code)] 14 | #![warn(missing_docs)] 15 | 16 | pub mod engine; 17 | 18 | pub use crate::engine::Engine; 19 | -------------------------------------------------------------------------------- /ataxia-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Ataxia game engine library code 2 | #![deny( 3 | missing_debug_implementations, 4 | missing_copy_implementations, 5 | trivial_casts, 6 | trivial_numeric_casts, 7 | unsafe_code, 8 | unused_import_braces, 9 | unused_qualifications, 10 | clippy::all, 11 | clippy::pedantic, 12 | clippy::perf, 13 | clippy::style 14 | )] 15 | #![forbid(unsafe_code)] 16 | #![warn(missing_docs)] 17 | 18 | pub mod config; 19 | 20 | pub use crate::config::Config; 21 | -------------------------------------------------------------------------------- /bin/startup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import subprocess 3 | # Check the system to make sure all required directories and files are in the 4 | # right places 5 | # If game data doesn't exist, print a message to run the game data seed script 6 | 7 | # Check for exiting PID files to hint that a copy of the programs are already 8 | # running or possibly crashed 9 | 10 | portal = subprocess.Popen(["./bin/portal", "-d"]) 11 | engine = subprocess.Popen(["./bin/engine", "-d"]) 12 | engine.wait() 13 | portal.wait() 14 | -------------------------------------------------------------------------------- /ataxia-core/build.rs: -------------------------------------------------------------------------------- 1 | use time::OffsetDateTime; 2 | 3 | use std::env; 4 | use std::fs::File; 5 | use std::io::Write; 6 | use std::path::Path; 7 | 8 | fn main() { 9 | let out_dir = env::var("OUT_DIR").unwrap(); 10 | let dest_path = Path::new(&out_dir).join("version.rs"); 11 | let mut f = File::create(dest_path).unwrap(); 12 | 13 | let output: String = format!( 14 | "static ATAXIA_COMPILED: &str = \"{}\";", 15 | OffsetDateTime::now_utc() 16 | ); 17 | 18 | f.write_all(output.as_bytes()).unwrap(); 19 | } 20 | -------------------------------------------------------------------------------- /ataxia-engine/build.rs: -------------------------------------------------------------------------------- 1 | use time::OffsetDateTime; 2 | 3 | use std::env; 4 | use std::fs::File; 5 | use std::io::Write; 6 | use std::path::Path; 7 | 8 | fn main() { 9 | let out_dir = env::var("OUT_DIR").unwrap(); 10 | let dest_path = Path::new(&out_dir).join("version.rs"); 11 | let mut f = File::create(dest_path).unwrap(); 12 | 13 | let output: String = format!( 14 | "static ATAXIA_COMPILED: &str = \"{}\";", 15 | OffsetDateTime::now_utc() 16 | ); 17 | 18 | f.write_all(output.as_bytes()).unwrap(); 19 | } 20 | -------------------------------------------------------------------------------- /ataxia-portal/build.rs: -------------------------------------------------------------------------------- 1 | use time::OffsetDateTime; 2 | 3 | use std::env; 4 | use std::fs::File; 5 | use std::io::Write; 6 | use std::path::Path; 7 | 8 | fn main() { 9 | let out_dir = env::var("OUT_DIR").unwrap(); 10 | let dest_path = Path::new(&out_dir).join("version.rs"); 11 | let mut f = File::create(dest_path).unwrap(); 12 | 13 | let output: String = format!( 14 | "static ATAXIA_COMPILED: &str = \"{}\";", 15 | OffsetDateTime::now_utc() 16 | ); 17 | 18 | f.write_all(output.as_bytes()).unwrap(); 19 | } 20 | -------------------------------------------------------------------------------- /ataxia-portal/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Ataxia game engine library code 2 | #![deny( 3 | missing_debug_implementations, 4 | missing_copy_implementations, 5 | trivial_casts, 6 | trivial_numeric_casts, 7 | unsafe_code, 8 | unused_import_braces, 9 | unused_qualifications, 10 | clippy::all 11 | )] 12 | #![forbid(unsafe_code)] 13 | #![warn(missing_docs, clippy::pedantic)] 14 | // Disable this lint due to Tokio using the binding name _task_context 15 | #![recursion_limit = "256"] 16 | 17 | pub mod portal; 18 | 19 | pub use crate::portal::Portal; 20 | -------------------------------------------------------------------------------- /ataxia-engine/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ataxia-engine" 3 | build = "build.rs" 4 | version.workspace = true 5 | license.workspace = true 6 | authors.workspace = true 7 | description.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | readme.workspace = true 11 | edition.workspace = true 12 | 13 | [dependencies] 14 | ataxia-core = { path = "../ataxia-core" } 15 | log = "^0.4" 16 | simplelog = "^0.12" 17 | anyhow = "^1.0" 18 | #rand = "^0.8" 19 | 20 | [build-dependencies] 21 | time = "0.3" 22 | -------------------------------------------------------------------------------- /ataxia-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ataxia-core" 3 | build = "build.rs" 4 | version.workspace = true 5 | license.workspace = true 6 | authors.workspace = true 7 | description.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | readme.workspace = true 11 | edition.workspace = true 12 | 13 | [dependencies] 14 | toml = "^0.7" 15 | serde = { version = "^1.0", features = ["derive"] } 16 | serde_json = "^1.0" 17 | anyhow = "^1.0" 18 | clap = { version = "4.0", features = ["derive"] } 19 | 20 | [build-dependencies] 21 | time = "0.3" 22 | -------------------------------------------------------------------------------- /tools/release-edit.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | RELFILE="cmd/proxy/package.go" 4 | 5 | if [[ ! -d ".git" ]]; then 6 | echo "Must be run at the root of the git repository (.git directory not found)" 7 | exit 1 8 | fi 9 | 10 | DATE=`date` 11 | RELEASE=`git tag | egrep "^v([0-9]+\.?)+" | tail -n 1 | cut -d' ' -f 2` 12 | 13 | if [[ -z "$RELEASE" ]]; then 14 | RELEASE="development release" 15 | fi 16 | 17 | echo "Updating release constants:" 18 | echo " ataxiaVersion = '$RELEASE'" 19 | echo " ataxiaCompiled = '$DATE'" 20 | 21 | cat >"$RELFILE" <"] 13 | description = """ 14 | A modern MUD engine using Lua for scripting and game logic 15 | """ 16 | homepage = "https://github.com/xenith-studios/ataxia" 17 | repository = "https://github.com/xenith-studios/ataxia" 18 | readme = "README.md" 19 | edition = "2021" 20 | rust-version = "1.65" 21 | 22 | [profile.release] 23 | lto = true 24 | codegen-units = 1 25 | opt-level = "z" 26 | -------------------------------------------------------------------------------- /ataxia-portal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ataxia-portal" 3 | build = "build.rs" 4 | version.workspace = true 5 | license.workspace = true 6 | authors.workspace = true 7 | description.workspace = true 8 | homepage.workspace = true 9 | repository.workspace = true 10 | readme.workspace = true 11 | edition.workspace = true 12 | 13 | [dependencies] 14 | ataxia-core = { path = "../ataxia-core" } 15 | log = "^0.4" 16 | simplelog = "^0.12" 17 | anyhow = "^1.0" 18 | uuid = { version = "^1.3", features = ["v4"] } 19 | futures = "^0.3" 20 | tokio = { version = "^1", features = ["net", "rt", "rt-multi-thread", "io-util", "sync"] } 21 | tokio-util = { version = "^0.7", features = ["codec"] } 22 | #rand = "^0.8" 23 | 24 | [build-dependencies] 25 | time = "0.3" 26 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Ataxia Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /docs/enginedesign.md: -------------------------------------------------------------------------------- 1 | ### Networking notes 2 | 3 | How to listen for incoming connections and spawn them onto the executor as futures: 4 | 5 | ``` 6 | let server = TcpStream::new() 7 | .incoming() 8 | .map_err(|e| println!("error = {:?}", e)) 9 | .for_each(|s| { 10 | tokio::spawn(TelnetHandler::new(s)) 11 | }) 12 | ``` 13 | TelnetHandler (and WebSocketHandler) will be a future that handles a single connection: 14 | * Reading input 15 | * Parsing input 16 | * Send parsed/formatted input on channel to be sent to engine via mq 17 | * Receive output from engine via channel 18 | * Formatting output 19 | * Writing output 20 | 21 | There will be another future that handles shuffling messages back and forth via mq to the engine. 22 | It receives input from all client futures via mpsc channel, sends them out the mq to the engine. 23 | It receives output from the engine via mq, then writes it to the proper mpsc channel to the correct client future. 24 | 25 | * Task (main future) is the listener 26 | * tokio::run the task, blocks thread until listener shuts down 27 | * It spawns a task for the mq future 28 | * It spawns new tasks (futures) for each client connection 29 | * TelnetHandler/WebSocketHandler implements Future -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CARGO = cargo 2 | CARGO_OPTS = 3 | 4 | debug: 5 | $(CARGO) $(CARGO_OPTS) build 6 | cp -f target/debug/portal bin/portal 7 | cp -f target/debug/engine bin/engine 8 | 9 | release: 10 | $(CARGO) $(CARGO_OPTS) build --release 11 | cp -f target/release/portal bin/portal 12 | cp -f target/release/engine bin/engine 13 | 14 | full: lint debug doc 15 | 16 | portal: lint 17 | $(CARGO) $(CARGO_OPTS) build --bin portal 18 | cp -f target/debug/portal bin/portal 19 | 20 | engine: lint 21 | $(CARGO) $(CARGO_OPTS) build --bin engine 22 | cp -f target/debug/engine bin/engine 23 | 24 | lint: 25 | $(CARGO) $(CARGO_OPTS) fmt 26 | env CARGO_TARGET_DIR=./target/clippy $(CARGO) $(CARGO_OPTS) clippy --workspace --all-targets 27 | 28 | bootstrap: 29 | rustup component add --toolchain nightly rustfmt-preview 30 | rustup component add --toolchain nightly clippy-preview 31 | 32 | clean: 33 | $(CARGO) $(CARGO_OPTS) clean 34 | rm -f bin/{engine,portal} 35 | 36 | check: 37 | $(CARGO) $(CARGO_OPTS) check 38 | 39 | test: 40 | $(CARGO) $(CARGO_OPTS) test 41 | 42 | bench: 43 | $(CARGO) $(CARGO_OPTS) bench 44 | 45 | doc: 46 | $(CARGO) $(CARGO_OPTS) doc 47 | 48 | .PHONY: quick full portal engine lint bootstrap clean check test bench doc 49 | -------------------------------------------------------------------------------- /CONTRIBUTING: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 1 Letterman Drive 6 | Suite D4700 7 | San Francisco, CA, 94129 8 | 9 | Everyone is permitted to copy and distribute verbatim copies of this 10 | license document, but changing it is not allowed. 11 | 12 | 13 | Developer's Certificate of Origin 1.1 14 | 15 | By making a contribution to this project, I certify that: 16 | 17 | (a) The contribution was created in whole or in part by me and I 18 | have the right to submit it under the open source license 19 | indicated in the file; or 20 | 21 | (b) The contribution is based upon previous work that, to the best 22 | of my knowledge, is covered under an appropriate open source 23 | license and I have the right under that license to submit that 24 | work with modifications, whether created in whole or in part 25 | by me, under the same open source license (unless I am 26 | permitted to submit under a different license), as indicated 27 | in the file; or 28 | 29 | (c) The contribution was provided directly to me by some other 30 | person who certified (a), (b) or (c) and I have not modified 31 | it. 32 | 33 | (d) I understand and agree that this project and the contribution 34 | are public and that a record of the contribution (including all 35 | personal information I submit with it, including my sign-off) is 36 | maintained indefinitely and may be redistributed consistent with 37 | this project or the open source license(s) involved. 38 | -------------------------------------------------------------------------------- /data/ataxia.toml: -------------------------------------------------------------------------------- 1 | # Ataxia Configuration file 2 | 3 | # The portal process listens on this address for player telnet connections 4 | telnet_addr = "0.0.0.0:9000" 5 | 6 | # The portal process listens on this address for player websockets connections 7 | ws_addr = "0.0.0.0:9001" 8 | 9 | # Both the portal and engine processes connect to the message queue listening on this address 10 | mq_addr = "[::1]:9005" 11 | 12 | # Location of the PID file 13 | # pid_file = "data/ataxia.pid" 14 | 15 | # Logging facility to use 16 | # Options are: stdout, file, syslog 17 | # log_facility = "file" 18 | 19 | # Location of the log file 20 | # log_file = "logs/ataxia.log" 21 | 22 | # Email facility to use 23 | # Options are: none, smtp, sendmail 24 | # email_facility = "none" 25 | 26 | # Location of the sendmail binary 27 | # sendmail_path = "/usr/sbin/sendmail" 28 | 29 | # Administrator's email address for notifications 30 | # admin_email = "" 31 | 32 | # Email address to send abuse notifications 33 | # abuse_email = "" 34 | 35 | # Storage facility to use 36 | # Options are: file, database 37 | # storage_facility = "file" 38 | 39 | # Location of the data files on disk 40 | # data_path = "data/" 41 | 42 | # Max simultaenous connections total 43 | # max_clients = 1000 44 | 45 | # Max simultaneous connections per IP 46 | # max_clients_per_host = 25 47 | 48 | # Allow creation of new accounts 49 | # account_creation = true 50 | 51 | # Maximum number of characters per account 52 | # chars_per_account = 5 53 | 54 | # Maximum number of simultaneous characters online per account (multiplaying support) 55 | # active_per_account = 1 56 | 57 | # Autosave interval in minutes 58 | # autosave = 15 59 | -------------------------------------------------------------------------------- /ataxia-engine/src/engine.rs: -------------------------------------------------------------------------------- 1 | //! Engine module and related methods 2 | 3 | use ataxia_core::Config; 4 | 5 | /// Engine data structure contains all related low-level data for running the game 6 | /// TODO: This is a stub data structure for now 7 | #[derive(Debug)] 8 | pub struct Engine { 9 | config: Config, 10 | } 11 | 12 | impl Engine { 13 | #![allow(clippy::new_ret_no_self)] 14 | /// Returns a new fully initialized game `Engine` 15 | /// 16 | /// # Arguments 17 | /// 18 | /// * `config` - A Config structure, contains all necessary configuration 19 | /// 20 | /// # Errors 21 | /// 22 | /// * Does not currently return any errors 23 | /// 24 | pub fn new(config: Config) -> Result { 25 | // Initialize game 26 | // Set game start time 27 | // Initialize Lua 28 | // Load game data 29 | // Load commands 30 | // Load world data 31 | // Load all entities (populate world) 32 | Ok(Self { config }) 33 | } 34 | 35 | /// Run the big game loop 36 | /// 37 | /// # Errors 38 | /// 39 | /// * Does not currently return any errors 40 | pub fn run(self) -> Result<(), anyhow::Error> { 41 | let engine = self; 42 | // Main game loop 43 | /*loop { 44 | // Read network input channel and process all pending external events 45 | // Did we get a new player login? If so, create the entity and add them to the game 46 | // Did the player just quit? If so, remove them from the game and delete the entity 47 | // Process all player commands 48 | // Process all server events (weather, time, zone updates, NPC updates, etc) 49 | // Process all output events and write them to the network output channel 50 | // Something something timing (ticks/pulses, sleep(pulse_time - how_long_this_loop_took)) 51 | }*/ 52 | 53 | // Game loop ends 54 | // Clean up 55 | // Save the world 56 | // Save game data 57 | // Shutdown Lua 58 | Ok(()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ataxia-engine/src/bin/engine.rs: -------------------------------------------------------------------------------- 1 | //! Binary source for the game engine 2 | //! There should be minimal functionality in this file. It exists mainly to set up the engine and 3 | //! call out to the library code. 4 | #![deny( 5 | trivial_casts, 6 | trivial_numeric_casts, 7 | unsafe_code, 8 | unused_import_braces, 9 | unused_qualifications, 10 | clippy::all, 11 | clippy::pedantic, 12 | clippy::perf, 13 | clippy::style 14 | )] 15 | 16 | // Include this file to get access to the datetime of the last time we compiled 17 | include!(concat!(env!("OUT_DIR"), "/version.rs")); 18 | 19 | use std::fs::File; 20 | use std::io::Write; 21 | use std::path::PathBuf; 22 | use std::process; 23 | 24 | use log::{error, info}; 25 | use simplelog::{ 26 | ColorChoice, CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode, WriteLogger, 27 | }; 28 | 29 | fn main() -> Result<(), anyhow::Error> { 30 | // Load settings from config file while allowing command-line overrides 31 | let config = ataxia_core::Config::new().unwrap_or_else(|err| { 32 | eprintln!("Unable to load the configuration file: {err}"); 33 | std::process::exit(1); 34 | }); 35 | 36 | // Initialize logging subsystem 37 | CombinedLogger::init(vec![ 38 | TermLogger::new( 39 | if config.debug() { 40 | LevelFilter::Debug 41 | } else if config.verbose() { 42 | LevelFilter::Info 43 | } else { 44 | LevelFilter::Warn 45 | }, 46 | Config::default(), 47 | TerminalMode::Mixed, 48 | ColorChoice::Auto, 49 | ), 50 | WriteLogger::new( 51 | if config.debug() { 52 | LevelFilter::Debug 53 | } else { 54 | LevelFilter::Info 55 | }, 56 | Config::default(), 57 | File::create(config.log_file())?, 58 | ), 59 | ])?; 60 | info!("Loading Ataxia Engine, compiled on {}", ATAXIA_COMPILED); 61 | 62 | // TODO: Figure out a system for catching/handling signals (SIGINT, SIGQUIT, SIGHUP) 63 | 64 | // Clean up from previous unclean shutdown if necessary 65 | 66 | // Write PID to file 67 | // TODO: Acquire lock on PID file as additional method of insuring only a single instance is running? 68 | let pid_file = PathBuf::from(config.pid_file()); 69 | // FIXME: Remove once we have a startup/supervisor system in place to handle unclean shutdown 70 | if pid_file.exists() { 71 | std::fs::remove_file(&pid_file)?; 72 | } 73 | File::create(&pid_file)?.write_all(format!("{}", process::id()).as_ref())?; 74 | 75 | // Initialize support subsystems 76 | // Environment 77 | // Queues 78 | // Database 79 | 80 | // Initialize engine subsystem 81 | let server = ataxia_engine::Engine::new(config).unwrap_or_else(|err| { 82 | error!("Unable to initialize the engine: {}", err); 83 | std::process::exit(1); 84 | }); 85 | 86 | // Initialize async networking subsystem in a dedicated thread 87 | 88 | // Start main game loop 89 | if let Err(e) = server.run() { 90 | error!("Unresolved system error: {}", e); 91 | std::process::exit(1); 92 | } 93 | 94 | // If the game loop exited without an error, we have a clean shutdown 95 | // Flush pending database writes and close database connection 96 | // Remove the PID file 97 | if pid_file.exists() { 98 | std::fs::remove_file(&pid_file)?; 99 | } 100 | 101 | Ok(()) 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ataxia Game Engine 2 | 3 | ## About 4 | 5 | Ataxia is a modern MUD/MUSH engine written in [Rust](https://www.rust-lang.org/). It utilizes Lua for commands and game logic. It uses separate processes for the game engine and network portal. 6 | 7 | PLEASE NOTE THAT CURRENTLY THERE IS VERY LITTLE CODE/FEATURES WRITTEN. 8 | 9 | ### Planned Features 10 | 11 | The separate process model allows Ataxia to support the following features: 12 | 13 | - Reload the engine process without disconnecting players. Avoids common "copyover/hotboot" hacks. 14 | - Adds the ability to rollback players to a previous version of code if needed 15 | - The portal can support various different communication protocols such as: 16 | - telnet (with or without tls) 17 | - ssh 18 | - websockets (to enable an HTML client) 19 | - Allows the network portal to present a unified front-end to allow connecting to multiple backend game engines through a single connection/port: 20 | - Live game 21 | - Test game (for feature/bug testing) 22 | - Building interface 23 | - Admin interface 24 | - The network portal will manage the account and login/permissions system: 25 | - Allows granting permissions (building interface, test access) on a per-account basis 26 | - Allows tying multiple characters to a single account/login 27 | - If you really want to, you can run the network portal on a different server than the game engine 28 | 29 | ## Install 30 | 31 | ### Dependencies 32 | 33 | The only dependency at this time is the Rust toolchain, which can be installed from the official channels via [rustup](https://www.rust-lang.org/en-US/install.html). The game is written to work with the Rust 2021 edition. 34 | 35 | For best results, the minimum required version of Rust is 1.65.0. The code should compile with any stable or nightly version released after 2021-11-03. 36 | 37 | For further information: https://www.rust-lang.org/ 38 | 39 | ### Building from Source 40 | 41 | Compiling from source is straightforward: 42 | 43 | ```sh 44 | $ make 45 | ``` 46 | 47 | This will install all library dependencies and build ataxia. (Make will automatically call `cargo build` as needed.) 48 | 49 | Modify the configuration file in `data/ataxia.toml` 50 | 51 | Run Ataxia: 52 | 53 | ```sh 54 | $ ./bin/startup.py 55 | ``` 56 | 57 | ### Development 58 | 59 | If you would also like to develop Ataxia, you will need to install the following additional tools: 60 | 61 | - Rust 62 | - rustfmt 63 | - clippy 64 | 65 | You can install both tools via make: 66 | 67 | ```sh 68 | $ make bootstrap 69 | ``` 70 | 71 | To perform a full compile, including all lints: 72 | 73 | ```sh 74 | $ make full 75 | ``` 76 | 77 | To run all tests: 78 | 79 | ```sh 80 | $ make test 81 | ``` 82 | 83 | ## Directory Layout 84 | 85 | src/ 86 | Rust source code 87 | bin/ 88 | The location of compiled binary files and scripts for running the game 89 | docs/ 90 | User and developer documentation 91 | logs/ 92 | Log files 93 | tools/ 94 | Helper scripts and tools for developers and game admins 95 | data/ 96 | On-disk data files (ex. config files, world files) 97 | scripts/ 98 | Lua scripts 99 | scripts/interface 100 | Helper scripts that set up the data interface between Rust and Lua 101 | scripts/commands 102 | All in-game commands 103 | 104 | ## License 105 | 106 | Licensed under either of: 107 | 108 | - Apache License, Version 2.0, (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) 109 | - MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT) 110 | 111 | at your option. 112 | 113 | Previous versions of this code were licensed under the BSD three-clause license. 114 | 115 | ### Contribution 116 | 117 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 118 | -------------------------------------------------------------------------------- /docs/randomideas.md: -------------------------------------------------------------------------------- 1 | ## Random Ideas 2 | * Database for storage 3 | * SQLite (or PostgreSQL) 4 | * Maybe a K/V store of some kind? 5 | * Area files 6 | * Areas defined in YAML (or similar markup language) 7 | * NPC templates defined in YAML 8 | * Lua for scripting (or maybe [dyon](https://github.com/pistondevelopers/dyon) or [gluon](https://github.com/gluon-lang/gluon)?) 9 | * One Lua_State per thread 10 | * Lua bridges 11 | * [rlua](https://github.com/kyren/rlua) 12 | * Multithreaded 13 | * Main thread for engine (for handling parsed game input, game state, npc updates, etc) and time (pulses/ticks) 14 | * Network thread for handing player communication and [gossip](https://gossip.haus) (Tokio event loop), might be implemented as a threadpool? 15 | * "Game" thread for non-time sensitive background events (weather, in-game time, etc) 16 | * Where to put slow, blocking actions (DNS calls, etc)? 17 | * Variables spaces 18 | * Game data in Rust data structures 19 | * Player and world data (rooms, npcs) in Lua data structures? 20 | * Rust handles saving/loading data and converts into lua data structures 21 | * Proxy process 22 | * All players connect to proxy, proxy manages player communications. 23 | * Proxy communicates to engine process via some data protocol (gRPC/protobuf, capnproto, thrift, etc) 24 | * This allows engine to only need to implement a single advanced protocol (https://github.com/mudcoders/ump can be an example) 25 | * Proxy can implement telnet, web sockets, etc. for different types of clients 26 | * This allows us to greatly simplify hotboot/copyover process. Engine doesn't need to worry about maintaining sockets anymore, the engine just reconnects to the proxy after it restarts. Don't even necessarily need the engine to exec itself, can just let process shutdown and reboot (let the startup/supervisor script handle that!) 27 | * Examples: https://github.com/evennia/evennia/wiki/Portal-And-Server and http://evennia.blogspot.com/2018/01/kicking-into-gear-from-distance.html 28 | 29 | ## Object hierarchy notes 30 | * Entity-component system (ECS) 31 | * Template objects/archetypes 32 | * Objects all defined in scripts? or YAML? 33 | 34 | ## NPC notes 35 | * Template-based system 36 | * Forms (animal, humanoid, plant, skeleton, etc)? 37 | * Example: npc object contains pointer to species object which defines species data (name, immunities, etc) 38 | * NPC taxonomy 39 | 40 | ## Area/room notes 41 | * Still many questions to be answered here: 42 | * Regions are top-level 43 | * Regions can have subregions (mountain range region encased in continental region encased in planetary region) 44 | * Zones belong to regions 45 | * "Traditional area" 46 | * City, dungeon, mountain path 47 | * Generally used for a higher-level of detail than region 48 | * Ex: Mountain region has a path zone through it (if path is important) 49 | * Rooms are container objects 50 | * Generally placed as objects on the coordinate grid inside of a zone 51 | * Ex: building inside a city 52 | * Mainly coordinate-based roomless system 53 | * Rooms can be placed in sequence inside zone for dungeon effect, if necessary 54 | * Room taxonomy? ("Terrain type") 55 | 56 | ## General functionality 57 | * System of event hooks (before_event, on_event, after_event) 58 | * Sent to every object when events happen 59 | * Objects knows how to handle own events 60 | * Objects ignore events they can't handle 61 | * Event-driven system? 62 | 63 | ## Commands 64 | * Every command is defined as a separate lua script 65 | * Some core commands are built-in (like shutdown, reboot, load script) 66 | * Command prefixes 67 | * Admin commands prefixed with / 68 | 69 | ## Settings/lore notes 70 | * Modern fantasy 71 | * Low magic (start off as no magic, must rediscover magic?) 72 | * Magic is not flashy, very subtle, but can be powerful (like LOTR or Dresden) 73 | * Ages 74 | * Age of Creation 75 | * Age of Strife(?) 76 | * Age of Legends 77 | * Age of Legacy <-- game starts here? 78 | * Big cities are bastions of civilization (safe zones), the farther you get from the cities the more dangerous the world gets. 79 | -------------------------------------------------------------------------------- /ataxia-portal/src/bin/portal.rs: -------------------------------------------------------------------------------- 1 | //! Binary source for the network portal 2 | //! There should be minimal functionality in this file. It exists mainly to set up the portal and 3 | //! call out to the library code. 4 | #![warn( 5 | trivial_casts, 6 | trivial_numeric_casts, 7 | unsafe_code, 8 | unused_import_braces, 9 | unused_qualifications, 10 | clippy::all, 11 | clippy::pedantic, 12 | clippy::perf, 13 | clippy::style 14 | )] 15 | #![forbid(unsafe_code)] 16 | 17 | // Include this file to get access to the timestamp of the compilation 18 | include!(concat!(env!("OUT_DIR"), "/version.rs")); 19 | 20 | use std::fs::File; 21 | use std::io::Write; 22 | use std::path::PathBuf; 23 | use std::process; 24 | 25 | use log::{error, info}; 26 | use simplelog::{ 27 | ColorChoice, CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode, WriteLogger, 28 | }; 29 | 30 | #[allow(clippy::too_many_lines)] 31 | fn main() -> Result<(), anyhow::Error> { 32 | // Load settings from config file while allowing command-line overrides 33 | let config = ataxia_core::Config::new().unwrap_or_else(|err| { 34 | eprintln!("Unable to load the configuration file: {err}"); 35 | std::process::exit(1); 36 | }); 37 | 38 | // Initialize logging subsystem 39 | CombinedLogger::init(vec![ 40 | TermLogger::new( 41 | if config.debug() { 42 | LevelFilter::Debug 43 | } else if config.verbose() { 44 | LevelFilter::Info 45 | } else { 46 | LevelFilter::Warn 47 | }, 48 | Config::default(), 49 | TerminalMode::Mixed, 50 | ColorChoice::Auto, 51 | ), 52 | WriteLogger::new( 53 | if config.debug() { 54 | LevelFilter::Debug 55 | } else { 56 | LevelFilter::Info 57 | }, 58 | Config::default(), 59 | File::create(config.log_file())?, 60 | ), 61 | ])?; 62 | info!( 63 | "Loading Ataxia Network Portal, compiled on {}", 64 | ATAXIA_COMPILED 65 | ); 66 | 67 | // TODO: Set up signal handlers (SIGINT, SIGQUIT, SIGHUP) 68 | 69 | // Clean up from previous unclean shutdown if necessary 70 | 71 | // Write PID to file 72 | // TODO: Acquire lock on PID file as additional method of insuring only a single instance is running? 73 | let pid_file = PathBuf::from(config.pid_file()); 74 | // FIXME: Remove this block once we have a supervisor system in place to handle unclean shutdown 75 | if pid_file.exists() { 76 | std::fs::remove_file(&pid_file)?; 77 | } 78 | File::create(&pid_file)?.write_all(format!("{}", process::id()).as_ref())?; 79 | 80 | // Initialize support subsystems 81 | // Environment 82 | // Queues 83 | // Database 84 | 85 | // Initialize Tokio async runtime and spin up the worker threadpool 86 | let runtime = tokio::runtime::Runtime::new().expect("Unable to initialize the Tokio Runtime"); 87 | 88 | // Initialize portal and networking subsystems 89 | let server = runtime 90 | .block_on(ataxia_portal::Portal::new(config)) 91 | .unwrap_or_else(|err| { 92 | error!("Unable to initialize the portal: {}", err); 93 | std::process::exit(1); 94 | }); 95 | 96 | // Start main loop 97 | if let Err(e) = runtime.block_on(server.run()) { 98 | // If we enter this block, the system has crashed and we don't know why 99 | // TODO: Do some cleanup before exiting, but leave the PID file in place to signal an 100 | // unclean shutdown 101 | error!("Unresolved system error: {}", e); 102 | std::process::exit(1); 103 | } 104 | 105 | // If the loop exited without an error, we have a clean shutdown 106 | // TODO: Flush pending database writes and close database connection 107 | // Remove the PID file 108 | if pid_file.exists() { 109 | std::fs::remove_file(&pid_file)?; 110 | } 111 | 112 | info!("Clean shutdown"); 113 | Ok(()) 114 | } 115 | -------------------------------------------------------------------------------- /ataxia-portal/src/portal.rs: -------------------------------------------------------------------------------- 1 | //! Portal module and related methods 2 | 3 | pub mod handlers; 4 | 5 | use std::collections::BTreeMap; 6 | use std::sync::atomic::AtomicUsize; 7 | use std::sync::Arc; 8 | 9 | //use self::handlers::{telnet, websockets}; 10 | use self::handlers::telnet; 11 | use ataxia_core::Config; 12 | use log::info; 13 | use tokio::sync::mpsc; 14 | 15 | /// Shorthand for the transmit half of the message channel. 16 | type Tx = mpsc::UnboundedSender; 17 | 18 | /// Shorthand for the receive half of the message channel. 19 | type Rx = mpsc::UnboundedReceiver; 20 | 21 | /// Message is a wrapper for data passed between the main thread and the tasks 22 | #[derive(Debug)] 23 | pub enum Message { 24 | /// A message that should be broadcasted to others. 25 | Data(usize, String), 26 | 27 | /// A new connection 28 | NewConnection(usize, Tx, String), 29 | 30 | /// Remove an existing connection 31 | CloseConnection(usize), 32 | } 33 | 34 | /// portal data structure contains all related low-level data for running the network portal 35 | #[derive(Debug)] 36 | pub struct Portal { 37 | config: Config, 38 | client_list: BTreeMap, 39 | telnet_server: telnet::Server, 40 | //ws_server: websockets::Server, 41 | rx: Rx, 42 | } 43 | 44 | impl Portal { 45 | #![allow(clippy::new_ret_no_self, clippy::mixed_read_write_in_expression)] 46 | /// Returns a new fully initialized `portal` server 47 | /// 48 | /// # Arguments 49 | /// 50 | /// * `config` - A Config structure, contains all necessary configuration 51 | /// * 'rt' - The `tokio::runtime::Runtime` used to run the async I/O 52 | /// 53 | /// # Errors 54 | /// 55 | /// * Returns an error if the `Server` fails to initialize 56 | pub async fn new(config: Config) -> Result { 57 | // Initialize the portal 58 | let id_counter = Arc::new(AtomicUsize::new(1)); 59 | let client_list = BTreeMap::new(); 60 | let telnet_addr = config.telnet_addr().to_string(); 61 | //let ws_addr = config.ws_addr().to_string(); 62 | let (tx, rx) = mpsc::unbounded_channel(); 63 | //TODO: Set portal start time 64 | 65 | Ok(Self { 66 | config, 67 | client_list, 68 | telnet_server: telnet::Server::new(&telnet_addr, id_counter.clone(), tx.clone()) 69 | .await?, 70 | //ws_server: websockets::Server::new(&ws_addr, id_counter, tx).await?, 71 | rx, 72 | }) 73 | } 74 | 75 | /// Run the main loop 76 | /// 77 | /// # Errors 78 | /// 79 | /// * Does not currently return any errors 80 | /// 81 | /// # Panics 82 | /// 83 | /// TODO: add possible panics 84 | pub async fn run(mut self) -> Result<(), anyhow::Error> { 85 | // Start the network I/O servers 86 | tokio::spawn(self.telnet_server.run()); 87 | //tokio::spawn(self.ws_server.run()); 88 | 89 | // Main loop 90 | while let Some(message) = self.rx.recv().await { 91 | // Process all input events 92 | // Send all processed events over MQ to engine process 93 | // Process all output events 94 | // Send all processed output events to connections 95 | // Something something timing 96 | match message { 97 | Message::NewConnection(id, rx, name) => { 98 | info!("Player {} has connected on socket {}", name, id); 99 | self.client_list.values().for_each(|(_, tx)| { 100 | tx.send(Message::Data(id, format!("{name} has joined the chat."))) 101 | .unwrap(); 102 | }); 103 | self.client_list.insert(id, (name, rx)); 104 | } 105 | Message::CloseConnection(id) => { 106 | if let Some((name, _)) = self.client_list.remove(&id) { 107 | info!("Player {} has disconnected on socket {}", name, id); 108 | self.client_list.values().for_each(|(_, tx)| { 109 | tx.send(Message::Data(id, format!("{name} has left the chat."))) 110 | .unwrap(); 111 | }); 112 | } 113 | } 114 | Message::Data(id, message) => { 115 | if let Some((name, _)) = self.client_list.get(&id) { 116 | info!("Received message from {}: {}", name, message); 117 | self.client_list.values().for_each(|(tx_name, tx)| { 118 | if tx_name != name { 119 | tx.send(Message::Data(id, format!("{name}: {message}"))) 120 | .unwrap(); 121 | } 122 | }); 123 | } 124 | } 125 | } 126 | } 127 | // Main loop ends 128 | 129 | // Clean up 130 | Ok(()) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /ataxia-core/src/config.rs: -------------------------------------------------------------------------------- 1 | //! Configuration module for Ataxia 2 | use clap::Parser; 3 | use serde::Deserialize; 4 | use std::fs::File; 5 | use std::io::Read; 6 | use std::path::PathBuf; 7 | 8 | /// Config structure for holding internal and external configuration data 9 | #[derive(Debug, Deserialize, Parser)] 10 | #[command(name = "Ataxia MUD", 11 | about = env!("CARGO_PKG_DESCRIPTION"), 12 | version = env!("CARGO_PKG_VERSION"), 13 | )] 14 | pub struct Config { 15 | #[arg(short = 'c', long = "config", default_value = "data/ataxia.toml")] 16 | #[serde(default)] 17 | config_file: PathBuf, 18 | #[arg(short = 'p', long = "pid")] 19 | pid_file: Option, 20 | #[arg(short = 'T', long)] 21 | telnet_addr: Option, 22 | #[arg(short = 'W', long)] 23 | ws_addr: Option, 24 | #[arg(short = 'M', long)] 25 | mq_addr: Option, 26 | #[arg(short = 'L', long)] 27 | log_file: Option, 28 | #[arg(short, long)] 29 | #[serde(default)] 30 | debug: bool, 31 | #[arg(short, long)] 32 | #[serde(default)] 33 | verbose: bool, 34 | } 35 | impl Config { 36 | /// Returns a new Config 37 | /// Read configuration from the file path specified in the Clap arguments struct. 38 | /// 39 | /// # Errors 40 | /// 41 | /// * Returns `std::io::Error` if the config file can't be opened or read 42 | /// * Returns `toml::de::Error` if TOML parsing fails 43 | /// 44 | /// # Panics 45 | /// 46 | /// TODO: add possible panics here 47 | pub fn new() -> Result { 48 | let cli = Config::parse(); 49 | 50 | // TODO: This is a very simplistic method that should be improved/strengthened 51 | let process_name = std::env::args() 52 | .next() 53 | .unwrap() 54 | .split('/') 55 | .last() 56 | .unwrap() 57 | .to_string(); 58 | 59 | let mut input = String::new(); 60 | File::open(&cli.config_file)?.read_to_string(&mut input)?; 61 | let mut config = toml::from_str::(&input)?; 62 | 63 | if let Some(pid_file) = cli.pid_file { 64 | config.pid_file = Some(pid_file); 65 | } else if config.pid_file.is_none() { 66 | // The PID file wasn't specified. Default to process name 67 | config.pid_file = Some(format!("data/{process_name}.pid")); 68 | } 69 | 70 | if let Some(log_file) = cli.log_file { 71 | config.log_file = Some(log_file); 72 | } else if config.log_file.is_none() { 73 | // The log file wasn't specified. Default to process name 74 | config.log_file = Some(format!("logs/{process_name}.log")); 75 | } 76 | 77 | if let Some(ws_addr) = cli.ws_addr { 78 | config.ws_addr = Some(ws_addr); 79 | } 80 | 81 | if let Some(telnet_addr) = cli.telnet_addr { 82 | config.telnet_addr = Some(telnet_addr); 83 | } 84 | 85 | if let Some(mq_addr) = cli.mq_addr { 86 | config.mq_addr = Some(mq_addr); 87 | } 88 | 89 | config.debug = cli.debug; 90 | 91 | config.verbose = cli.verbose; 92 | 93 | Ok(config) 94 | } 95 | 96 | /// Returns the listen address for player telnet connections 97 | #[must_use] 98 | pub fn telnet_addr(&self) -> &str { 99 | self.telnet_addr 100 | .as_ref() 101 | .map_or("", |telnet_addr| telnet_addr) 102 | } 103 | /// Set the listen address for player telnet connections 104 | pub fn set_telnet_addr(&mut self, addr: String) { 105 | self.telnet_addr = Some(addr); 106 | } 107 | 108 | /// Returns the listen address for player websocket connections 109 | #[must_use] 110 | pub fn ws_addr(&self) -> &str { 111 | self.ws_addr.as_ref().map_or("", |ws_addr| ws_addr) 112 | } 113 | /// Set the listen address for player websocket connections 114 | pub fn set_ws_addr(&mut self, addr: String) { 115 | self.ws_addr = Some(addr); 116 | } 117 | 118 | /// Returns the listen address for the message queue 119 | #[must_use] 120 | pub fn mq_addr(&self) -> &str { 121 | self.mq_addr.as_ref().map_or("", |mq_addr| mq_addr) 122 | } 123 | /// Set the listen address for the message queue 124 | pub fn set_mq_addr(&mut self, addr: String) { 125 | self.mq_addr = Some(addr); 126 | } 127 | 128 | /// Returns the file path to the pid file 129 | #[must_use] 130 | pub fn pid_file(&self) -> &str { 131 | self.pid_file.as_ref().map_or("", |pid_file| pid_file) 132 | } 133 | /// Set the file path to the pid file 134 | pub fn set_pid_file(&mut self, file: String) { 135 | self.pid_file = Some(file); 136 | } 137 | 138 | /// Returns the file path to the log file 139 | #[must_use] 140 | pub fn log_file(&self) -> &str { 141 | self.log_file.as_ref().map_or("", |log_file| log_file) 142 | } 143 | /// Set the file path to the log file 144 | pub fn set_log_file(&mut self, file: String) { 145 | self.log_file = Some(file); 146 | } 147 | 148 | /// Returns true if the debug CLI flag was specified 149 | #[must_use] 150 | pub fn debug(&self) -> bool { 151 | self.debug 152 | } 153 | 154 | /// Returns true if the verbose CLI flag was specified 155 | #[must_use] 156 | pub fn verbose(&self) -> bool { 157 | self.verbose 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /ataxia-portal/src/portal/handlers/telnet.rs: -------------------------------------------------------------------------------- 1 | //! Telnet contains code specifically to handle network I/O for a telnet connection 2 | //! 3 | use crate::portal::{Message, Rx, Tx}; 4 | use anyhow::anyhow; 5 | use futures::prelude::*; 6 | use log::{error, info}; 7 | use std::sync::atomic::{AtomicUsize, Ordering}; 8 | use std::sync::Arc; 9 | use tokio::net::TcpListener; 10 | use tokio::sync::mpsc; 11 | use tokio_util::codec::{Framed, LinesCodec}; 12 | use uuid::Uuid; 13 | 14 | /// A player connection 15 | #[derive(Debug)] 16 | pub struct Socket { 17 | uuid: Uuid, 18 | id: usize, 19 | stream: Framed, 20 | rx: Rx, 21 | main_tx: Tx, 22 | addr: String, 23 | } 24 | 25 | impl Socket { 26 | /// Returns a new Socket 27 | /// 28 | /// # Arguments 29 | /// 30 | /// * `stream` - A `TcpStream` from Tokio 31 | /// * `id` - A unique incrementing connection identifier 32 | /// * `main_tx` - Transmit channel to communicate with the main task 33 | /// 34 | /// # Errors 35 | /// 36 | /// * This function will return an error if any send/recv operations fail on any channel or 37 | /// stream, which would signal a connection error and disconnect the client. 38 | pub async fn new( 39 | stream: tokio::net::TcpStream, 40 | id: usize, 41 | main_tx: Tx, 42 | ) -> Result { 43 | let addr = match stream.peer_addr() { 44 | Ok(addr) => addr.to_string(), 45 | Err(_) => "Unknown".to_string(), 46 | }; 47 | info!("Telnet client connected: ID: {id}, remote_addr: {addr}"); 48 | let mut stream = Framed::new(stream, LinesCodec::new()); 49 | let (tx, rx) = mpsc::unbounded_channel(); 50 | stream.send("Welcome to the Ataxia Portal.").await?; 51 | stream.send("Please enter your username:").await?; 52 | let username = match stream.next().await { 53 | Some(Ok(line)) => line, 54 | Some(Err(_)) | None => { 55 | return Err(anyhow!("Failed to get username from {addr}.")); 56 | } 57 | }; 58 | stream.send(format!("Welcome, {username}!")).await?; 59 | main_tx.send(Message::NewConnection(id, tx, username))?; 60 | Ok(Self { 61 | uuid: Uuid::new_v4(), 62 | id, 63 | stream, 64 | rx, 65 | main_tx, 66 | addr, 67 | }) 68 | } 69 | 70 | /// Handle a connection 71 | /// 72 | /// # Errors 73 | /// 74 | /// * This function will return an error and disconnect the client if any send/recv fails. 75 | /// 76 | /// # Panics 77 | /// 78 | /// TODO: add possible panics 79 | #[allow(clippy::mut_mut)] 80 | pub async fn handle(mut self) -> Result<(), anyhow::Error> { 81 | loop { 82 | futures::select! { 83 | data = self.rx.recv().fuse() => { 84 | if let Some(message) = data { 85 | match message { 86 | Message::Data(_, message) => self.stream.send(message).await?, 87 | _ => error!("Oops"), 88 | } 89 | } else { 90 | // The main loop closed our channel to signal system shutdown 91 | self.stream.send("The game is shutting down, goodbye!").await?; 92 | break; 93 | } 94 | }, 95 | data = self.stream.next().fuse() => { 96 | if let Some(message) = data { 97 | if let Err(e) = self.main_tx.send(Message::Data(self.id, message?)) { 98 | // The main loop channel was closed, most likely this signals a crash 99 | self.stream.send("The game has crashed, sorry! Please try again.").await?; 100 | error!("Connection error due to game crash? {}", e); 101 | break; 102 | } 103 | } else { 104 | // The other end closed the connection. Nothing left to do. 105 | break; 106 | } 107 | }, 108 | }; 109 | } 110 | self.main_tx.send(Message::CloseConnection(self.id))?; 111 | Ok(()) 112 | } 113 | } 114 | 115 | /// Server data structure holding all the server state 116 | #[derive(Debug)] 117 | pub struct Server { 118 | listener: TcpListener, 119 | id_counter: Arc, 120 | main_tx: Tx, 121 | } 122 | 123 | impl Server { 124 | /// Returns a new Server 125 | /// 126 | /// # Arguments 127 | /// 128 | /// * `address` - A String containing the listen addr:port 129 | /// * `clients` - A shared binding to the client list 130 | /// * `id_counter` - A shared binding to a global connection counter 131 | /// 132 | /// # Errors 133 | /// 134 | /// * Returns `tokio::io::Error` if the server can't bind to the listen port 135 | /// 136 | pub async fn new( 137 | address: &str, 138 | id_counter: Arc, 139 | tx: Tx, 140 | ) -> Result { 141 | let listener = TcpListener::bind(address).await?; 142 | info!("Listening for telnet clients on {:?}", address); 143 | Ok(Self { 144 | listener, 145 | id_counter, 146 | main_tx: tx, 147 | }) 148 | } 149 | /// Start the listener loop, which will spawn individual connections into the runtime 150 | #[allow(clippy::needless_return)] 151 | pub async fn run(self) { 152 | loop { 153 | let connection = self.listener.accept().await; 154 | match connection { 155 | Err(e) => error!("Accept failed: {:?}", e), 156 | Ok(stream) => { 157 | let client_id = self.id_counter.fetch_add(1, Ordering::Relaxed); 158 | let main_tx = self.main_tx.clone(); 159 | tokio::spawn(async move { 160 | let socket = match Socket::new(stream.0, client_id, main_tx).await { 161 | Ok(socket) => socket, 162 | Err(e) => { 163 | error!("Client disconnected: {}", e); 164 | return; 165 | } 166 | }; 167 | if let Err(e) = socket.handle().await { 168 | error!("Client error: {}", e); 169 | return; 170 | }; 171 | }); 172 | } 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Ataxia Developers 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /data/world/are_to_json.py: -------------------------------------------------------------------------------- 1 | import json 2 | import shlex 3 | 4 | text = '' 5 | last_str = '' 6 | 7 | def read_char(): 8 | global text 9 | global last_str 10 | text = text.lstrip() 11 | c = text[0] 12 | text = text[1:] 13 | last_str = c 14 | return c 15 | 16 | def read_word(): 17 | global text 18 | global last_str 19 | text = text.lstrip() 20 | 21 | if text[0] == "'": 22 | last_str, text = text[1:].split("'", 1) 23 | return "'" + last_str + "'" 24 | if text[0] == '"': 25 | last_str, text = text[1:].split('"', 1) 26 | return "'" + last_str + "'" 27 | 28 | buf = '' 29 | c = text[0] 30 | while c != ' ' and c != '\t' and c != '\n': 31 | buf += c 32 | text = text[1:] 33 | c = text[0] 34 | last_str = buf 35 | return buf 36 | # last_str, text = text.split(None, 1) 37 | # print last_str 38 | # return last_str.lstrip() 39 | 40 | def read_string(): 41 | global text 42 | global last_str 43 | last_str, text = text.split('~', 1) 44 | if last_str[0] == '\n': 45 | last_str = last_str[1:] 46 | return last_str 47 | 48 | def read_int(): 49 | global text 50 | global last_str 51 | buf = '' 52 | text = text.lstrip() 53 | if text[0] == '-' or text[0] == '+': 54 | buf = text[0] 55 | text = text[1:].lstrip() 56 | 57 | while text[0].isdigit(): 58 | buf += text[0] 59 | text = text[1:] 60 | last_str = buf 61 | return int(buf) 62 | 63 | def read_dice(): 64 | global last_str 65 | buf = '' 66 | buf += '%d'%read_int() 67 | buf += read_char() 68 | buf += '%d'%read_int() 69 | buf += read_char() 70 | buf += '%d'%read_int() 71 | last_str = buf 72 | return buf 73 | 74 | def read_eol(): 75 | global text 76 | 77 | c = text[0] 78 | while c != '\n': 79 | text = text[1:] 80 | c = text[0] 81 | # text = text.split('\n', 1)[-1] 82 | 83 | def read_area(): 84 | area = {} 85 | area['filename'] = read_string() 86 | area['name'] = read_string() 87 | area['credits'] = read_string() 88 | area['min_vnum'] = read_int() 89 | area['max_vnum'] = read_int() 90 | return area 91 | 92 | def read_mobile(): 93 | global text 94 | text = text.lstrip() 95 | if text[0] != '#': 96 | print 'load_mobiles: # not found' 97 | exit() 98 | 99 | text = text[1:] 100 | vnum = read_int() 101 | 102 | if vnum == 0: 103 | return None 104 | 105 | mobile = {} 106 | mobile['vnum'] = vnum 107 | mobile['keywords'] = read_string() 108 | mobile['short_descr'] = read_string() 109 | mobile['long_descr'] = read_string() 110 | mobile['description'] = read_string() 111 | mobile['race'] = read_string() 112 | mobile['act_flags'] = read_word() 113 | mobile['aff_flags'] = read_word() 114 | mobile['alignment'] = read_int() 115 | mobile['group'] = read_word() 116 | mobile['level'] = read_int() 117 | mobile['hitroll'] = read_int() 118 | mobile['hp_dice'] = read_dice() 119 | mobile['mana_dice'] = read_dice() 120 | mobile['damage_dice'] = read_dice() 121 | mobile['damage_type'] = read_word() 122 | mobile['ac_pierce'] = read_int() 123 | mobile['ac_bash'] = read_int() 124 | mobile['ac_slash'] = read_int() 125 | mobile['ac_exotic'] = read_int() 126 | mobile['off_flags'] = read_word() 127 | mobile['imm_flags'] = read_word() 128 | mobile['res_flags'] = read_word() 129 | mobile['vuln_flags'] = read_word() 130 | mobile['start_pos'] = read_word() 131 | mobile['default_pos'] = read_word() 132 | mobile['sex'] = read_word() 133 | mobile['wealth'] = read_int() 134 | mobile['form_flags'] = read_word() 135 | mobile['part_flags'] = read_word() 136 | mobile['size'] = read_word() 137 | mobile['material'] = read_word() 138 | 139 | while True: 140 | text = text.lstrip() 141 | if text[0] == 'F': 142 | text = text[1:] 143 | if 'remove_flags' not in mobile: 144 | mobile['remove_flags'] = [] 145 | flag_mod = {} 146 | flag_mod['type'] = read_word() 147 | flag_mod['bit'] = read_word() 148 | mobile['remove_flags'].append(flag_mod) 149 | elif text[0] == '>': 150 | text = text[1:] 151 | if 'mobprogs' not in mobile: 152 | mobile['mobprogs'] = [] 153 | mobprog = {} 154 | parts = read_string().split(None, 1) 155 | mobprog['type'] = parts[0] 156 | if len(parts) > 1: 157 | mobprog['args'] = parts[1] 158 | mobprog['commands'] = read_string() 159 | mobile['mobprogs'].append(mobprog) 160 | elif text[0] == '|': 161 | text = text[1:] 162 | break 163 | elif text[0] == '#': 164 | break 165 | else: 166 | print 'weird mob', vnum 167 | exit() 168 | 169 | return mobile 170 | 171 | def read_mobiles(): 172 | mobiles = {} 173 | 174 | while True: 175 | mobile = read_mobile() 176 | if mobile == None: 177 | break 178 | mobiles[mobile['vnum']] = mobile 179 | 180 | return mobiles 181 | 182 | def read_object(): 183 | global text 184 | text = text.lstrip() 185 | if text[0] != '#': 186 | print 'load_object: # not found' 187 | exit() 188 | 189 | text = text[1:] 190 | vnum = read_int() 191 | 192 | if vnum == 0: 193 | return None 194 | 195 | obj = {} 196 | obj['vnum'] = vnum 197 | obj['keywords'] = read_string() 198 | obj['short_descr'] = read_string() 199 | obj['description'] = read_string() 200 | obj['material'] = read_string() 201 | obj['item_type'] = read_word() 202 | obj['extra_flags'] = read_word() 203 | obj['wear_flags'] = read_word() 204 | obj['value0'] = read_word() 205 | obj['value1'] = read_word() 206 | obj['value2'] = read_word() 207 | obj['value3'] = read_word() 208 | obj['value4'] = read_word() 209 | obj['level'] = read_int() 210 | obj['weight'] = read_int() 211 | obj['cost'] = read_int() 212 | obj['condition'] = read_word() 213 | 214 | while True: 215 | text = text.lstrip() 216 | if text[0] == 'E': 217 | text = text[1:] 218 | if 'extra_descr' not in obj: 219 | obj['extra_descr'] = {} 220 | obj['extra_descr'][read_string()] = read_string() 221 | elif text[0] == 'A': 222 | text = text[1:] 223 | if 'added_affects' not in obj: 224 | obj['added_affects'] = [] 225 | affect = {} 226 | affect['location'] = read_int() 227 | affect['modifier'] = read_int() 228 | obj['added_affects'].append(affect) 229 | elif text[0] == 'F': 230 | text = text[1:] 231 | if 'added_flags' not in obj: 232 | obj['added_flags'] = [] 233 | add = {} 234 | text = text.lstrip() 235 | add['type'] = text[0] 236 | text = text[1:] 237 | add['location'] = read_int() 238 | add['modifier'] = read_int() 239 | add['bitvector'] = read_word() 240 | obj['added_flags'].append(add) 241 | elif text[0] == '#': 242 | break 243 | else: 244 | print 'weird obj', vnum 245 | exit() 246 | 247 | return obj 248 | 249 | def read_objects(): 250 | objects = {} 251 | 252 | while True: 253 | obj = read_object() 254 | if obj == None: 255 | break 256 | objects[obj['vnum']] = obj 257 | 258 | return objects 259 | 260 | def read_room(): 261 | global text 262 | text = text.lstrip() 263 | if text[0] != '#': 264 | print 'load_room: # not found' 265 | exit() 266 | 267 | text = text[1:] 268 | vnum = read_int() 269 | 270 | if vnum == 0: 271 | return None 272 | 273 | room = {} 274 | room['vnum'] = vnum 275 | room['name'] = read_string() 276 | room['description'] = read_string() 277 | room['tele_dest'] = read_int() 278 | room['room_flags'] = read_word() 279 | room['sector_type'] = read_int() 280 | 281 | while True: 282 | c = read_char() 283 | if c == 'E': 284 | if 'extra_descr' not in room: 285 | room['extra_descr'] = {} 286 | room['extra_descr'][read_string()] = read_string() 287 | elif c == 'H': 288 | room['heal_rate'] = read_int() 289 | elif c == 'M': 290 | room['mana_rate'] = read_int() 291 | elif c == 'C': 292 | room['clan'] = read_word() 293 | elif c == 'G': 294 | room['guild'] = read_word() 295 | elif c == 'O': 296 | room['owner'] = read_string() 297 | elif c == 'D': 298 | if 'exits' not in room: 299 | room['exits'] = {} 300 | exit = {} 301 | direction = read_int() 302 | exit['description'] = read_string() 303 | exit['keywords'] = read_string() 304 | exit['locks'] = read_int() 305 | exit['key'] = read_int() 306 | exit['vnum'] = read_int() 307 | room['exits'][direction] = exit 308 | elif c == 'S': 309 | break 310 | else: 311 | print 'weird room', vnum 312 | exit() 313 | return room 314 | 315 | def read_rooms(): 316 | rooms = {} 317 | 318 | while True: 319 | room = read_room() 320 | if room == None: 321 | break 322 | rooms[room['vnum']] = room 323 | 324 | return rooms 325 | 326 | def read_resets(): 327 | global text 328 | resets = [] 329 | 330 | while True: 331 | letter = read_char() 332 | 333 | if letter == '*': 334 | read_eol() 335 | continue 336 | elif letter == 'S': 337 | break 338 | 339 | reset = {} 340 | read_int() 341 | reset['type'] = letter 342 | reset['arg1'] = read_int() 343 | reset['arg2'] = read_int() 344 | if letter == 'G' or letter == 'R': 345 | reset['arg3'] = 0 346 | else: 347 | reset['arg3'] = read_int() 348 | if letter == 'P' or letter == 'M': 349 | reset['arg4'] = read_int() 350 | else: 351 | reset['arg4'] = 0 352 | 353 | read_eol() 354 | resets.append(reset) 355 | 356 | return resets 357 | 358 | def read_shops(): 359 | global text 360 | shops = [] 361 | 362 | while True: 363 | vnum = read_int() 364 | 365 | if vnum == 0: 366 | break 367 | 368 | shop = {} 369 | shop['keeper'] = vnum 370 | shop['buy_type'] = read_int() 371 | shop['profit_buy'] = read_int() 372 | shop['profit_sell'] = read_int() 373 | shop['open_hour'] = read_int() 374 | shop['close_hour'] = read_int() 375 | 376 | read_eol() 377 | shops.append(shop) 378 | 379 | return shops 380 | 381 | def read_specials(): 382 | global text 383 | specials = [] 384 | 385 | while True: 386 | letter = read_char() 387 | 388 | if letter == '*': 389 | read_eol() 390 | continue 391 | elif letter == 'S': 392 | break 393 | 394 | special = {} 395 | special['type'] = letter 396 | special['vnum'] = read_int() 397 | special['spec'] = read_word() 398 | 399 | read_eol() 400 | # print 'read special, remaining is', text 401 | specials.append(special) 402 | 403 | # print 'finished specials, remaining is', text 404 | return specials 405 | 406 | def read_tourstarts(): 407 | tourstarts = {} 408 | return tourstarts 409 | 410 | def read_tourroutes(): 411 | tourroutes = {} 412 | return tourroutes 413 | 414 | def to_json(f): 415 | global text 416 | sections = {} 417 | 418 | for line in f: 419 | text += line 420 | 421 | text.replace(r'\r','') 422 | 423 | try: 424 | while True: 425 | text = text.lstrip() 426 | if (len(text) == 0): 427 | break; 428 | 429 | parts = text.split(None, 1) 430 | if len(parts) == 1: 431 | # print parts[0] 432 | break 433 | word = parts[0] 434 | text = parts[1] 435 | # print word 436 | 437 | if word == '#AREA': 438 | sections['AREA'] = read_area() 439 | for k in sections['AREA']: 440 | sections[k] = sections['AREA'][k] 441 | elif word == '#MOBILES': 442 | sections['MOBILES'] = read_mobiles() 443 | elif word == '#OBJECTS': 444 | sections['OBJECTS'] = read_objects() 445 | elif word == '#RESETS': 446 | sections['RESETS'] = read_resets() 447 | elif word == '#ROOMS': 448 | sections['ROOMS'] = read_rooms() 449 | elif word == '#SHOPS': 450 | sections['SHOPS'] = read_shops() 451 | elif word == '#SPECIALS': 452 | sections['SPECIALS'] = read_specials() 453 | # elif word == '#TOURSTARTS': 454 | # sections['TOURSTARTS'] = read_tourstarts() 455 | # elif word == '#TOURROUTES': 456 | # sections['TOURROUTES'] = read_tourroutes() 457 | elif word.startswith('#$'): 458 | break 459 | else: 460 | pass 461 | except: 462 | print 'Last string:', last_str 463 | print 'Remaining:', text 464 | raise 465 | 466 | # return sections 467 | return json.dumps(sections, indent=4) 468 | 469 | if __name__ == "__main__": 470 | import codecs 471 | import sys 472 | # area = open(sys.argv[1]) 473 | area = codecs.open(sys.argv[1], 'r', 'utf-8', 'ignore') 474 | 475 | try: 476 | j = to_json(area) 477 | except: 478 | print sys.argv[1] 479 | raise 480 | finally: 481 | area.close() 482 | 483 | fname = sys.argv[1].rsplit('.', 1)[0] 484 | f = open(fname + '.json', 'w') 485 | f.write(j) 486 | f.close() 487 | # import pprint 488 | # pp = pprint.PrettyPrinter(depth=4) 489 | # pp.pprint(j) 490 | # print j 491 | 492 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.5.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "utf8parse", 32 | ] 33 | 34 | [[package]] 35 | name = "anstyle" 36 | version = "1.0.2" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" 39 | 40 | [[package]] 41 | name = "anstyle-parse" 42 | version = "0.2.1" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" 45 | dependencies = [ 46 | "utf8parse", 47 | ] 48 | 49 | [[package]] 50 | name = "anstyle-query" 51 | version = "1.0.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 54 | dependencies = [ 55 | "windows-sys", 56 | ] 57 | 58 | [[package]] 59 | name = "anstyle-wincon" 60 | version = "2.1.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" 63 | dependencies = [ 64 | "anstyle", 65 | "windows-sys", 66 | ] 67 | 68 | [[package]] 69 | name = "anyhow" 70 | version = "1.0.75" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" 73 | 74 | [[package]] 75 | name = "ataxia-core" 76 | version = "0.0.1-dev" 77 | dependencies = [ 78 | "anyhow", 79 | "clap", 80 | "serde", 81 | "serde_json", 82 | "time", 83 | "toml", 84 | ] 85 | 86 | [[package]] 87 | name = "ataxia-engine" 88 | version = "0.0.1-dev" 89 | dependencies = [ 90 | "anyhow", 91 | "ataxia-core", 92 | "log", 93 | "simplelog", 94 | "time", 95 | ] 96 | 97 | [[package]] 98 | name = "ataxia-portal" 99 | version = "0.0.1-dev" 100 | dependencies = [ 101 | "anyhow", 102 | "ataxia-core", 103 | "futures", 104 | "log", 105 | "simplelog", 106 | "time", 107 | "tokio", 108 | "tokio-util", 109 | "uuid", 110 | ] 111 | 112 | [[package]] 113 | name = "autocfg" 114 | version = "1.1.0" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 117 | 118 | [[package]] 119 | name = "backtrace" 120 | version = "0.3.69" 121 | source = "registry+https://github.com/rust-lang/crates.io-index" 122 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 123 | dependencies = [ 124 | "addr2line", 125 | "cc", 126 | "cfg-if", 127 | "libc", 128 | "miniz_oxide", 129 | "object", 130 | "rustc-demangle", 131 | ] 132 | 133 | [[package]] 134 | name = "bytes" 135 | version = "1.4.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" 138 | 139 | [[package]] 140 | name = "cc" 141 | version = "1.0.83" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 144 | dependencies = [ 145 | "libc", 146 | ] 147 | 148 | [[package]] 149 | name = "cfg-if" 150 | version = "1.0.0" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 153 | 154 | [[package]] 155 | name = "clap" 156 | version = "4.4.1" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "7c8d502cbaec4595d2e7d5f61e318f05417bd2b66fdc3809498f0d3fdf0bea27" 159 | dependencies = [ 160 | "clap_builder", 161 | "clap_derive", 162 | "once_cell", 163 | ] 164 | 165 | [[package]] 166 | name = "clap_builder" 167 | version = "4.4.1" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "5891c7bc0edb3e1c2204fc5e94009affabeb1821c9e5fdc3959536c5c0bb984d" 170 | dependencies = [ 171 | "anstream", 172 | "anstyle", 173 | "clap_lex", 174 | "strsim", 175 | ] 176 | 177 | [[package]] 178 | name = "clap_derive" 179 | version = "4.4.0" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "c9fd1a5729c4548118d7d70ff234a44868d00489a4b6597b0b020918a0e91a1a" 182 | dependencies = [ 183 | "heck", 184 | "proc-macro2", 185 | "quote", 186 | "syn", 187 | ] 188 | 189 | [[package]] 190 | name = "clap_lex" 191 | version = "0.5.1" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" 194 | 195 | [[package]] 196 | name = "colorchoice" 197 | version = "1.0.0" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 200 | 201 | [[package]] 202 | name = "deranged" 203 | version = "0.3.8" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" 206 | 207 | [[package]] 208 | name = "equivalent" 209 | version = "1.0.1" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 212 | 213 | [[package]] 214 | name = "futures" 215 | version = "0.3.28" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" 218 | dependencies = [ 219 | "futures-channel", 220 | "futures-core", 221 | "futures-executor", 222 | "futures-io", 223 | "futures-sink", 224 | "futures-task", 225 | "futures-util", 226 | ] 227 | 228 | [[package]] 229 | name = "futures-channel" 230 | version = "0.3.28" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" 233 | dependencies = [ 234 | "futures-core", 235 | "futures-sink", 236 | ] 237 | 238 | [[package]] 239 | name = "futures-core" 240 | version = "0.3.28" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" 243 | 244 | [[package]] 245 | name = "futures-executor" 246 | version = "0.3.28" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" 249 | dependencies = [ 250 | "futures-core", 251 | "futures-task", 252 | "futures-util", 253 | ] 254 | 255 | [[package]] 256 | name = "futures-io" 257 | version = "0.3.28" 258 | source = "registry+https://github.com/rust-lang/crates.io-index" 259 | checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" 260 | 261 | [[package]] 262 | name = "futures-macro" 263 | version = "0.3.28" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" 266 | dependencies = [ 267 | "proc-macro2", 268 | "quote", 269 | "syn", 270 | ] 271 | 272 | [[package]] 273 | name = "futures-sink" 274 | version = "0.3.28" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" 277 | 278 | [[package]] 279 | name = "futures-task" 280 | version = "0.3.28" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" 283 | 284 | [[package]] 285 | name = "futures-util" 286 | version = "0.3.28" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" 289 | dependencies = [ 290 | "futures-channel", 291 | "futures-core", 292 | "futures-io", 293 | "futures-macro", 294 | "futures-sink", 295 | "futures-task", 296 | "memchr", 297 | "pin-project-lite", 298 | "pin-utils", 299 | "slab", 300 | ] 301 | 302 | [[package]] 303 | name = "getrandom" 304 | version = "0.2.10" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 307 | dependencies = [ 308 | "cfg-if", 309 | "libc", 310 | "wasi", 311 | ] 312 | 313 | [[package]] 314 | name = "gimli" 315 | version = "0.28.0" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" 318 | 319 | [[package]] 320 | name = "hashbrown" 321 | version = "0.14.0" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" 324 | 325 | [[package]] 326 | name = "heck" 327 | version = "0.4.1" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 330 | 331 | [[package]] 332 | name = "hermit-abi" 333 | version = "0.3.2" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" 336 | 337 | [[package]] 338 | name = "indexmap" 339 | version = "2.0.0" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" 342 | dependencies = [ 343 | "equivalent", 344 | "hashbrown", 345 | ] 346 | 347 | [[package]] 348 | name = "itoa" 349 | version = "1.0.9" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" 352 | 353 | [[package]] 354 | name = "libc" 355 | version = "0.2.147" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 358 | 359 | [[package]] 360 | name = "log" 361 | version = "0.4.20" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 364 | 365 | [[package]] 366 | name = "memchr" 367 | version = "2.6.0" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "76fc44e2588d5b436dbc3c6cf62aef290f90dab6235744a93dfe1cc18f451e2c" 370 | 371 | [[package]] 372 | name = "miniz_oxide" 373 | version = "0.7.1" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 376 | dependencies = [ 377 | "adler", 378 | ] 379 | 380 | [[package]] 381 | name = "mio" 382 | version = "0.8.8" 383 | source = "registry+https://github.com/rust-lang/crates.io-index" 384 | checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" 385 | dependencies = [ 386 | "libc", 387 | "wasi", 388 | "windows-sys", 389 | ] 390 | 391 | [[package]] 392 | name = "num_cpus" 393 | version = "1.16.0" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 396 | dependencies = [ 397 | "hermit-abi", 398 | "libc", 399 | ] 400 | 401 | [[package]] 402 | name = "num_threads" 403 | version = "0.1.6" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" 406 | dependencies = [ 407 | "libc", 408 | ] 409 | 410 | [[package]] 411 | name = "object" 412 | version = "0.32.0" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" 415 | dependencies = [ 416 | "memchr", 417 | ] 418 | 419 | [[package]] 420 | name = "once_cell" 421 | version = "1.18.0" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 424 | 425 | [[package]] 426 | name = "pin-project-lite" 427 | version = "0.2.13" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 430 | 431 | [[package]] 432 | name = "pin-utils" 433 | version = "0.1.0" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 436 | 437 | [[package]] 438 | name = "proc-macro2" 439 | version = "1.0.66" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" 442 | dependencies = [ 443 | "unicode-ident", 444 | ] 445 | 446 | [[package]] 447 | name = "quote" 448 | version = "1.0.33" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 451 | dependencies = [ 452 | "proc-macro2", 453 | ] 454 | 455 | [[package]] 456 | name = "rustc-demangle" 457 | version = "0.1.23" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 460 | 461 | [[package]] 462 | name = "ryu" 463 | version = "1.0.15" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" 466 | 467 | [[package]] 468 | name = "serde" 469 | version = "1.0.188" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" 472 | dependencies = [ 473 | "serde_derive", 474 | ] 475 | 476 | [[package]] 477 | name = "serde_derive" 478 | version = "1.0.188" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" 481 | dependencies = [ 482 | "proc-macro2", 483 | "quote", 484 | "syn", 485 | ] 486 | 487 | [[package]] 488 | name = "serde_json" 489 | version = "1.0.105" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" 492 | dependencies = [ 493 | "itoa", 494 | "ryu", 495 | "serde", 496 | ] 497 | 498 | [[package]] 499 | name = "serde_spanned" 500 | version = "0.6.3" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" 503 | dependencies = [ 504 | "serde", 505 | ] 506 | 507 | [[package]] 508 | name = "simplelog" 509 | version = "0.12.1" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "acee08041c5de3d5048c8b3f6f13fafb3026b24ba43c6a695a0c76179b844369" 512 | dependencies = [ 513 | "log", 514 | "termcolor", 515 | "time", 516 | ] 517 | 518 | [[package]] 519 | name = "slab" 520 | version = "0.4.9" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 523 | dependencies = [ 524 | "autocfg", 525 | ] 526 | 527 | [[package]] 528 | name = "socket2" 529 | version = "0.5.3" 530 | source = "registry+https://github.com/rust-lang/crates.io-index" 531 | checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" 532 | dependencies = [ 533 | "libc", 534 | "windows-sys", 535 | ] 536 | 537 | [[package]] 538 | name = "strsim" 539 | version = "0.10.0" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 542 | 543 | [[package]] 544 | name = "syn" 545 | version = "2.0.29" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" 548 | dependencies = [ 549 | "proc-macro2", 550 | "quote", 551 | "unicode-ident", 552 | ] 553 | 554 | [[package]] 555 | name = "termcolor" 556 | version = "1.1.3" 557 | source = "registry+https://github.com/rust-lang/crates.io-index" 558 | checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" 559 | dependencies = [ 560 | "winapi-util", 561 | ] 562 | 563 | [[package]] 564 | name = "time" 565 | version = "0.3.28" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" 568 | dependencies = [ 569 | "deranged", 570 | "itoa", 571 | "libc", 572 | "num_threads", 573 | "serde", 574 | "time-core", 575 | "time-macros", 576 | ] 577 | 578 | [[package]] 579 | name = "time-core" 580 | version = "0.1.1" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" 583 | 584 | [[package]] 585 | name = "time-macros" 586 | version = "0.2.14" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" 589 | dependencies = [ 590 | "time-core", 591 | ] 592 | 593 | [[package]] 594 | name = "tokio" 595 | version = "1.32.0" 596 | source = "registry+https://github.com/rust-lang/crates.io-index" 597 | checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" 598 | dependencies = [ 599 | "backtrace", 600 | "bytes", 601 | "libc", 602 | "mio", 603 | "num_cpus", 604 | "pin-project-lite", 605 | "socket2", 606 | "windows-sys", 607 | ] 608 | 609 | [[package]] 610 | name = "tokio-util" 611 | version = "0.7.8" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" 614 | dependencies = [ 615 | "bytes", 616 | "futures-core", 617 | "futures-sink", 618 | "pin-project-lite", 619 | "tokio", 620 | "tracing", 621 | ] 622 | 623 | [[package]] 624 | name = "toml" 625 | version = "0.7.6" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" 628 | dependencies = [ 629 | "serde", 630 | "serde_spanned", 631 | "toml_datetime", 632 | "toml_edit", 633 | ] 634 | 635 | [[package]] 636 | name = "toml_datetime" 637 | version = "0.6.3" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" 640 | dependencies = [ 641 | "serde", 642 | ] 643 | 644 | [[package]] 645 | name = "toml_edit" 646 | version = "0.19.14" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" 649 | dependencies = [ 650 | "indexmap", 651 | "serde", 652 | "serde_spanned", 653 | "toml_datetime", 654 | "winnow", 655 | ] 656 | 657 | [[package]] 658 | name = "tracing" 659 | version = "0.1.37" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 662 | dependencies = [ 663 | "cfg-if", 664 | "pin-project-lite", 665 | "tracing-core", 666 | ] 667 | 668 | [[package]] 669 | name = "tracing-core" 670 | version = "0.1.31" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" 673 | dependencies = [ 674 | "once_cell", 675 | ] 676 | 677 | [[package]] 678 | name = "unicode-ident" 679 | version = "1.0.11" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" 682 | 683 | [[package]] 684 | name = "utf8parse" 685 | version = "0.2.1" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 688 | 689 | [[package]] 690 | name = "uuid" 691 | version = "1.4.1" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" 694 | dependencies = [ 695 | "getrandom", 696 | ] 697 | 698 | [[package]] 699 | name = "wasi" 700 | version = "0.11.0+wasi-snapshot-preview1" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 703 | 704 | [[package]] 705 | name = "winapi" 706 | version = "0.3.9" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 709 | dependencies = [ 710 | "winapi-i686-pc-windows-gnu", 711 | "winapi-x86_64-pc-windows-gnu", 712 | ] 713 | 714 | [[package]] 715 | name = "winapi-i686-pc-windows-gnu" 716 | version = "0.4.0" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 719 | 720 | [[package]] 721 | name = "winapi-util" 722 | version = "0.1.5" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 725 | dependencies = [ 726 | "winapi", 727 | ] 728 | 729 | [[package]] 730 | name = "winapi-x86_64-pc-windows-gnu" 731 | version = "0.4.0" 732 | source = "registry+https://github.com/rust-lang/crates.io-index" 733 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 734 | 735 | [[package]] 736 | name = "windows-sys" 737 | version = "0.48.0" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 740 | dependencies = [ 741 | "windows-targets", 742 | ] 743 | 744 | [[package]] 745 | name = "windows-targets" 746 | version = "0.48.5" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 749 | dependencies = [ 750 | "windows_aarch64_gnullvm", 751 | "windows_aarch64_msvc", 752 | "windows_i686_gnu", 753 | "windows_i686_msvc", 754 | "windows_x86_64_gnu", 755 | "windows_x86_64_gnullvm", 756 | "windows_x86_64_msvc", 757 | ] 758 | 759 | [[package]] 760 | name = "windows_aarch64_gnullvm" 761 | version = "0.48.5" 762 | source = "registry+https://github.com/rust-lang/crates.io-index" 763 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 764 | 765 | [[package]] 766 | name = "windows_aarch64_msvc" 767 | version = "0.48.5" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 770 | 771 | [[package]] 772 | name = "windows_i686_gnu" 773 | version = "0.48.5" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 776 | 777 | [[package]] 778 | name = "windows_i686_msvc" 779 | version = "0.48.5" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 782 | 783 | [[package]] 784 | name = "windows_x86_64_gnu" 785 | version = "0.48.5" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 788 | 789 | [[package]] 790 | name = "windows_x86_64_gnullvm" 791 | version = "0.48.5" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 794 | 795 | [[package]] 796 | name = "windows_x86_64_msvc" 797 | version = "0.48.5" 798 | source = "registry+https://github.com/rust-lang/crates.io-index" 799 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 800 | 801 | [[package]] 802 | name = "winnow" 803 | version = "0.5.15" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" 806 | dependencies = [ 807 | "memchr", 808 | ] 809 | -------------------------------------------------------------------------------- /docs/gamedesign.md: -------------------------------------------------------------------------------- 1 | Most of these questions are from Nick Gammon, posted on his [forums](http://www.gammon.com.au/forum/bbshowpost.php?bbsubject_id=5959) 2 | 3 | # Terminology 4 | 5 | * Entity: An entity is some object that exists within the engine. Some entities are visible in the game world, such as objects, agents, rooms, players, etc. Some entities are not visible in the game world and only have representation within the engine, such as timers and threads. 6 | * EID: Entity ID. Every entity has a unique identifier attached to it. This is an internal identifier, so it is usually fairly cryptic. 7 | * Object: An object is a physical entity that exists in the game world. This can be anything from a sword or bag up to a full-sized room. 8 | * OID: Object ID. Every object has a unique identifier. This is an external identifier, designed to be fairly human-readable. 9 | * Agent: An agent is some "being" that interacts with the world. Specific examples include players and npcs. 10 | * Region: The top-level hierarchical organizational construct for the game world. A region is a collection of coordinates on the game grid. Regions can contain other sub-regions. 11 | * Zone: The mid-level organizational structure for the game world. A zone is a collection of rooms. Analogous to a classic mud "area". A zone can contain other sub-zones. 12 | * Room: The lowest-level organizational structure for the game world. A container object. 13 | * Reset: A timer that creates or destroys objects or agents at a specific interval or on a specific trigger. 14 | * Trigger: A timer that triggers on a specific event that causes a specific action to occur. 15 | * Timer: A timer emits an event when activated (for instance, when the countdown reaches zero). This event will generally activate a trigger. 16 | * Event: Events are emitted by timers, triggers, player actions, etc. Many things can listen for emitted events. For example, a trap can listen for a "player enters the room" event to trigger it's effect. 17 | 18 | # General Design 19 | 20 | ## The World 21 | 22 | The game world will be separated into three main types of "areas": Regions, zones, and rooms. Regions will be built on a coordinate grid. Zones will be collections of rooms. Rooms will be container objects. 23 | 24 | #### Region 25 | 26 | A region is the top-level organizational structure, used to group areas of the world together into one logical unit. A region can contain other regions (sub-regions). A region can contain multiple zones. As an example, we can have a planet region which contains a continent region which contains a mountain range region. Regions are generally used to divide areas based on geography, culture, politics, etc. Most terrain locations will belong to multiple regions. For example, a specific location (coordinate) can belong to a terrain region (plains), a country region (Estonia), and a cultural region (German). 27 | 28 | Regions represent stretches of terrain. Thus they represent geographical locations, as opposed to specific locations. In general, you will have large expanses of "region terrain" in between zones. 29 | 30 | The vast majority of the time, regions will be invisible to players. They are used mainly as a building tool. If you want to do something specific, you should use a zone. 31 | 32 | #### Zone 33 | 34 | A zone is akin to a traditional mud area. A zone represents a specific location, such as a city, mountain path, dungeon, etc. A zone should represent an area of more detail than a region. Example zones: The Swamp Of Sorrows, The City of Barrowhaven, Shrek's Pass 35 | 36 | Zones are collections of rooms laid out upon a grid. 37 | 38 | #### Room 39 | 40 | At the core, a room is basically a specialized container object. Each room can have a number of exits that can link either between rooms or between a room and a point on the main game grid (called the attachment point). 41 | 42 | A room can be used for things such as buildings, caves, etc. A room, unlike a coordinate on the grid, can have a custom description written for it (within reason). 43 | 44 | #### Resets / repops 45 | 46 | The reset system will be a combination approach. There are three different types of resets: 47 | 48 | * Static resets 49 | ** These specify agents that should always load in a specific location and will (usually) stay in that location. 50 | ** Static reset agents are loaded when the area loads and will stay around until killed or the area is unloaded. 51 | ** Static resets specify a respawn interval. After dying, the agent will respawn in the location after the interval. 52 | ** Ex: NPC shopkeeper, lair boss and minions. Can also be used to specify agents such as city guards that will walk a specific path or wander in a certain area 53 | * Dynamic resets 54 | ** Think random encounter 55 | ** Dynamic resets generally take a list of possible agents and a percent chance to spawn for each 56 | ** Dynamic resets can be attached to a specific location, such as a room or grid location 57 | ** Dynamic resets can also be attached to an area, i.e. region, zone, or group of rooms (specified by room taxonomy system) 58 | ** Ex: Wandering monsters 59 | * Object resets 60 | ** Spawn any game object, such as resources or items. 61 | ** Each object will have a respawn timer and respawn after the given interval. 62 | ** ==(Should this be a separate type of reset? Should we just roll objects into static and dynamic resets?)== 63 | 64 | #### Geography 65 | 66 | * Terrain taxonomy/terrain type 67 | 68 | Ataxia will be able to support multiple planets, multiple continents, etc. Anywhere from a small 1000 room world up to 100k worlds with 10 different planets. 69 | 70 | There will be a terrain taxonomy system. 71 | 72 | * Weather 73 | 74 | There will be an extensible weather system. It will be able to range anywhere from a basic rain/sunny to a large world-wide weather pattern generator that can generate multiple types of storms/weather patterns. 75 | 76 | * Time 77 | 78 | There will be a fairly simple time system. You will generally have simple day and night. The time speed can generally be specified per-game, even per-planet (for multi-planet games). There will, of course, be a light/dark system, where you would need lanterns/torches/magic/etc to see in the dark. 79 | 80 | #### Scripting 81 | 82 | We will use Lua for scripting. In general, most functionality will probably be in Lua space. The vast majority of commands will be scripts, with a few basic built-in commands. Keeping most functionality in Lua will enable us to attach specific functionality wherever we want. IE, triggered event attached to a room when someone walks in. 83 | 84 | Lua scripts are all interpreted, but it is likely that most scripts will be loaded at boot-time, meaning they will already be in-memory. To avoid runtime errors crashing the mud, the main lua interpeter will be inside its own thread. This way we can catch run-away scripts (scripts that never return) and runtime errors. 85 | 86 | #### Combat 87 | 88 | Do you want fighting? If so, what are the rules? Do attacks take time (eg. a slow sword swing, or a fast dagger strike)? Can strikes be resisted, dodged, parried, evaded, and so on? 89 | 90 | Do you have player-killing? (player vs. player). This can make your MUD more exciting, however you need to design in the rules. Can anyone attack anyone else? It is consensual? Can you only attack an opposing faction? What about NPCs (mobs)? What is to stop level 100 players wandering around killing your newbies 5 minutes after they connect to the game? 91 | 92 | Do you have magic spells? If so, how do they work? Do they take time to cast? Can they be interrupted, resisted, repelled, reflected? What do they consume (eg. mana). 93 | 94 | What "cooldown" do spells or abilities have? For example, a powerful spell might only be useable once every 10 minutes, thus you could say it has 10 minute cooldown. 95 | 96 | Can you run from a battle? With what degree of success? Can you be chased? If so, how far? 97 | 98 | Can combat occur in towns or "safe places"? 99 | 100 | Do you have "area of effect" spells? (ie. a spell that hits all mobs, not just one) 101 | 102 | Can characters freeze others in place (eg. to escape)? 103 | 104 | Can characters hide (eg. to sneak past something)? 105 | 106 | What happens if a player loses his/her connection in the middle of combat, intentionally or otherwise? Does their character keep fighting (or being attacked) until it is dead? It is removed from the game after 30 seconds? 107 | 108 | If multiple characters are attacking one mob, which one does the mob fight back? The weakest? The first to attack it? The last to attack it? The one causing the most damage? The one who is nearest to death? The one who is healing others? 109 | 110 | #### Races / classes / factions 111 | 112 | Do you have different races (like orcs and goblins)? If so, what makes one different from another? What are you doing to make them balanced, so that one race is not all-powerful, or too weak? 113 | 114 | Do you have different classes (like mages and warriors)? If so, what reason would players have for choosing one over the other? What are you doing to balance them? You probably need a paper-rock-scissors model, so that no one class (or race) can defeat, or be defeated by, all other ones. 115 | 116 | Do you want different factions? (eg. good/evil, humans/martians, alliance/rebels). Factions can add interest to a game because they give a natural "side" that players can join. However if you have different factions then you have extra complexity (for example, deciding who fights who), and you would need cities/towns for each faction. 117 | 118 | #### Character attributes 119 | 120 | Most MUDs assign some sort of attributes to the characters (eg. strength, dexterity, wisdom, luck). What ones will you have? What does each one do? Try to be specific, not just "wisdom makes you smarter". For example, "each point of wisdom gives you 10 mana", or "each point of strength adds 1 extra damage to a sword-wielder". 121 | 122 | How are the initial attributes assigned? Randomly? A table per class / race? 123 | 124 | How do players increase their attributes? Wearing equipment? Going up levels? Spells? Do they choose themselves what attribute is increased? (eg. each level they may be assigned 5 new attributes to distribute as they see fit). 125 | 126 | Formulae 127 | 128 | Work out in advance what the rules are for applying each attribute, not just vague talk about how "dexterity will be useful". For example: 129 | 130 | 131 | Each 10 points in dexterity increases your chance to dodge a melee attack by 1% 132 | 133 | Hit points lost in a melee attack = (attacker_attack_rating - defender_defense_rating) * weapon_damage_amount + dice_roll (2, 3) 134 | 135 | 136 | 137 | Doing this will help focus on what attributes you want in your MUD, and what they will be used for. 138 | 139 | #### Movement/Transport 140 | 141 | Do you have transport systems (cars, boats, trains, aircraft, portals, teleports, horses)? If so, how do they work? What do they cost? Can anyone use them? What happens if a player logs out while on a boat? 142 | 143 | #### Death 144 | 145 | What happens when a character dies? Does it lose money, experience, equipment, or suffer damage? How long before it can be played again? Any penalties? 146 | 147 | Can players resurrect other players? What are the requirements? How long does it take? Does it consume a reagent? Have a cooldown time? 148 | 149 | #### Groups 150 | 151 | Can players form groups? Is there a limit to a group's size? What happens if the group leader leaves? Is there a limit to level differences? (eg, no grouping level 1 players with level 100 players)? 152 | 153 | Can players automatically follow other players? 154 | 155 | What are the rules for looting mobs once killed (inside a group)? 156 | 157 | What are the rules for gaining experience when a group kills a mob, or completes a quest? Do these vary if some group members are not in the same room? Or a long way away? 158 | 159 | If a group kills a quest mob (eg. a boss) does the entire group get credit? 160 | 161 | If a quest mob has a quest item (eg. a pendant) does the entire group get it, or only the player who looted the mob? 162 | 163 | If a group kills a mob, but a character in the group dies half-way through the fight, does it get credit for the kill? If there is a quest item does s/he get that too? 164 | 165 | #### Guilds 166 | 167 | Can players form guilds (groups with some common purpose)? How? At what expense? Are there membership limits (eg. only orcs). 168 | 169 | What requirements are there to form a guild? Disband it? How do players get added to a guild? Removed from it? How do they remove themselves? Remove someone else? Do they have a guild meeting place? 170 | 171 | #### Economy 172 | 173 | Think about your game economy. MUD games tend to suffer from inflation because there is an unlimited amount of resources that can be obtained (you kill a mob, it comes back 5 minutes later, and mobs generally carry cash or valuable items). You need to design in "cash sinks" - ways that force players to spend their cash so there isn't too much of it around. For example, charge for transport, repairs, training, food, etc. 174 | 175 | Can players trade with each other? (eg. sell a sword for 5 gold pieces). If so, how do they do that? Do they have to be in the same room or close by? If not, how would they do it? Do you have a mechanism to avoid cheating? (eg. a player pays his 5 gold but is not given his sword). 176 | 177 | Do you want some sort of auction system? This could let players put goods up for auction, and let them sell to the highest bidder. You need to design the rules for such a thing. 178 | 179 | How do players buy/sell goods? From shops? Wandering salesmen? Can they sell anything to anyone, or does it have to be a dealer in that product? What is the buy/sell markup? A fixed percentage or set for every object in the game? Are shops always open? Do they run out of goods? If a player sells something to a shopkeeper, is that then available for someone else to buy? 180 | 181 | #### Quests 182 | 183 | Do you have quests? (eg. "your task is to kill 5 kobolds", or "please take this letter to the mayor"). How are they defined (eg. in a script)? What are their rewards? Does one quest depend on another? 184 | 185 | If you choose to have quests you will probably need to spend hours (if not weeks) designing a suitable set of quests, for each level that players will be in your game. 186 | 187 | Can quests be shared with other players? 188 | 189 | Who gives out quests? 190 | 191 | Who accepts completed quests? 192 | 193 | How do you abandon a quest? Re-gain it? 194 | 195 | Is there a limit to the number of quests you can do? 196 | 197 | #### Training 198 | 199 | Do players need to train to learn things? From where? At what cost? What are the prerequisites? Can they learn everything, or do you limit them to choices (eg. you can learn 5 out of the available 10 skills)? 200 | 201 | How are skills developed? Through practice? Trainers? Purchasing things? 202 | 203 | #### Skills 204 | 205 | Can players learn things (eg. metalwork)? Where do they learn? What materials are required? What cost? What can they make? What use are the made products (eg. swords could be sold). You may need to define, for each skill that players can learn, something like this: 206 | 207 | 208 | Skill level required to make it. 209 | 210 | Do they know how to make it? (eg. have the recipe) 211 | 212 | What reagents are required? That is, the things that are consumed in the process (eg. leather to make shoes) 213 | 214 | What tools are required? That is, things that are needed but are not consumed (eg. a leather-worker's needle). 215 | 216 | Do they need to be at a certain place? Eg. a forge for metalwork, a fire for cooking. 217 | 218 | 219 | #### Objects 220 | 221 | Most MUDs have objects - that is, things that can be picked up, carried around and used. What are you planning to have? What will they do? Where do players get them? What use are they? Can they be bought or sold? Can they be made? 222 | 223 | Does equipment wear out? To what rules? How is it repaired? At what cost? Where? 224 | 225 | Can objects be discarded (left lying on the ground), or destroyed? 226 | 227 | How many objects can a player carry at one time? Can they purchase or obtain extra capacity? Is capacity based on weight, number, volume? 228 | 229 | What attributes do objects have? An object might have: 230 | 231 | 232 | Armour class (wearing it increases armour) 233 | 234 | Attack rating (wielding it gives X amount of attack, eg. a sword) 235 | 236 | Attribute modification (eg. wearing it increases wisdom) 237 | 238 | A use - eg. using a key unlocks a door, eating food increases health 239 | 240 | Value - how much you can sell it for 241 | 242 | Wear and tear - how much damage it has suffered 243 | 244 | Minimum level number to use it 245 | 246 | Restrictions on use - eg. only mages might use wands 247 | 248 | Can a player carry more than one of it? If so, is there a limit? 249 | 250 | Is it a quest item? If so, for which quest(s)? 251 | 252 | Does it disappear if the player disconnects? 253 | 254 | Can it be sold? Traded? 255 | 256 | 257 | #### Mail 258 | 259 | Do you have an in-game mail system? If so, what can be sent? Messages? Objects? Money? 260 | 261 | Where are mailboxes? Does it cost to use them? 262 | 263 | How long does mail take to be delivered? 264 | 265 | #### Bank 266 | 267 | Do you have a "bank" or similar storage place where players can deposit things that they cannot fit into their inventory? 268 | 269 | How much can they store in the bank? Where is it/them? How much does it cost to use it? 270 | 271 | #### Logging 272 | 273 | Do you log everything that happens? Eg. what players say, who they kill, what trades are occurring? 274 | 275 | In the event of a dispute, can the log be easily perused? 276 | 277 | #### Levels 278 | 279 | Do players have levels? (eg. you start at level 1, and hope to reach level 100). How many? How do you advance levels? What advantages are there to being a higher level? 280 | 281 | What do players do when they reach the highest level? 282 | 283 | #### Making money 284 | 285 | Do you - the developers of the game - plan to make (real) money from it? Are you going to: 286 | 287 | 288 | Charge by the hour to play? 289 | 290 | Have a monthly subscription? 291 | 292 | Have a free trial period? 293 | 294 | Sell in-game goods or services for real money? 295 | 296 | Sell the game itself (to other aspiring MUD admins)? 297 | 298 | 299 | Who collects the money, and how? 300 | 301 | #### Ownership 302 | 303 | Who "owns" the game? If you are developing it from scratch, using a team of people, what happens if one leaves? Who (if anyone) owns: 304 | 305 | 306 | The name of the game? 307 | 308 | The computer program? 309 | 310 | Any scripts used in a scripting language? 311 | 312 | The room design (eg. room descriptions) 313 | 314 | Other designs (eg. quests, object design) 315 | 316 | 317 | Can someone leave your development team, take a copy of everything, and start a competing game? 318 | 319 | #### Running the game 320 | 321 | What server are you going to run it on? A commercial server or the private one belonging to one of the developers? 322 | 323 | How are you going to administer it 24 hours a day / 7 days a week / 365 days a year? What happens if the chief (or only) programmer goes on holiday for a month and it crashes while s/he is gone? 324 | 325 | Are you planning to have customer service people online all the time to help players? Will they be paid? How much? 326 | 327 | What will you do to help players that are stuck (eg. in a room with no exits), or have lost all their goods through some bug (or their own fault)? 328 | 329 | What method do players have to report bugs (eg. misspellings, or broken quests)? 330 | 331 | #### Handling problem players 332 | 333 | Eventually you will have a "problem" player. How do you deal with him/her? They may do things like: 334 | 335 | 336 | Have an unsuitable name, like a swear-word, or a name that implies they are an admin. 337 | 338 | Use abusive language. 339 | 340 | Harrass or irritate other players. 341 | 342 | Make life hard for other players by killing mobs that they are trying to kill or otherwise disrupting their quests. 343 | 344 | Cheat, eg. by promising to pay for something and then not pay. 345 | 346 | Exploit game mechanics or bugs to become super-powerful. 347 | 348 | Camp out valuable resources or important mobs, getting to them before anyone else can. 349 | 350 | 351 | 352 | You probably need some "self-help" mechanism for other players (eg. being able to ignore an annoying player), with a fall-back of being able to get help from a customer service person in extreme caes. 353 | 354 | #### Accounts 355 | 356 | Do players create "accounts"? That is, a central log-in point for all of their characters? Or is each character treated on its own? Is there a limit to how many characters one player can have? 357 | 358 | Do you allow multi-playing - that is, one person controlling multiple characters simultaneously? It may be hard to stop simply based on IP address these days, as many IP addresses are shared by routers using Network Address Translation (NAT). 359 | 360 | Do you "lock out" accounts for some reason (eg. non-payment, misbehaviour)? 361 | 362 | #### Newbies 363 | 364 | Every game will start of with "newbies" - even if they are experienced MUD players they will be new to your game. What do you do to help them? You might have help files, easy quests, a more experienced player assigned to help them, "newbie" chat channels, and so on. 365 | 366 | What do new players get to choose when they start playing? Character name / gender / race / class / description? 367 | 368 | 369 | #### Chatting 370 | 371 | Players expect to talk to each other - do you have multiple chat channels? You probably don't want level 100 players having to put up with newbie questions in the middle of a difficult quest. However newbies will need help too. Do you "zone" your chats, so the chats are relevant to where people are (eg. zoned to the current town)? 372 | 373 | #### Mob / NPC artificial intelligence 374 | 375 | AI (artificial intelligence) is the programming that controls your mobs / NPCs (Non Player Characters) so they behave in a reasonably interesting way. How do you plan to have this work? Do all mobs behave the same, or do humanoids behave distinctly differently to (say) spiders? 376 | 377 | If NPCs are spell-casters, what spells do they choose to cast? 378 | 379 | Do NPCs run away if facing an overwhelming foe? If so, how far? 380 | 381 | Do NPCs go and get help? 382 | 383 | Inside what range do aggressive NPCs attack? 384 | 385 | If the player runs away, does the NPC chase it, and if so, for how long? 386 | 387 | #### Instanced dungeons 388 | 389 | Do you plan to have "instanced dungeons"? These are areas of the game where players (or groups) get their own copy of a particular area. The point of this is to allow a group to play on its own, and not find that someone else has just killed the boss, or is harrassing the group. If you do plan on this, then it impacts on game design, because you may need to record an instance number alongside a player's location (eg. player Nick is in room 1002, instance 3). 390 | 391 | #### Pets 392 | 393 | Can players have pets? (eg. a pet dog) Is it for combat or local flavour? If for combat, how is it controlled? What happens when it dies? How is it resurrected / replaced? Does owning a pet cost anything (to purchase, or for upkeep)? 394 | 395 | #### Game balance 396 | 397 | What can you do to stop high-level players helping their friends with inappropriate equipment? (eg. level restrictions on good gear). 398 | 399 | Is each class / race / faction balanced? You can be sure that if players work out that "orc warriors" are much more powerful than everything else, then soon the game will be filled with orc warriors. 400 | 401 | Are attributes balanced? If one attribute (eg. strength) has a disproportional affect on combat, then every player will put all their points in strength (if they can). 402 | 403 | #### Making characters different 404 | 405 | You will probably want some sort of system that makes players choose between mutually exclusive features for their characters, so that you don't have, after 3 months, a MUD filled with dozens of characters who are all the same because they have all "maxed out" on every possible attribute. 406 | 407 | Examples are: 408 | 409 | 410 | Choosing different classes / races, where each one offers something the others don't 411 | 412 | Choosing different professions, which offer different things in the game (eg. make armor OR make potions, but not both) 413 | 414 | Wear different clothing, where a particular piece of armour can add protection or offensive power, but not both 415 | 416 | Give players optional points to choose from, where they have to choose 20 out of a possible 50, so it is unlikely that every player will choose the same 20. Points might be spent in offense, defense, stealth, long life, short battles, and so on. 417 | 418 | 419 | 420 | #### Saving things 421 | 422 | Players expect their characters to be there next time they connect. When is game data saved? Every minute? Every time a player changes rooms? Do you save to lots of small files (eg. one per player, like SMAUG does), or one big file (like PennMUSH does). 423 | 424 | Do you save to flat files or an SQL (or similar) database? 425 | 426 | What is saved? If a player moves an object from one room to another does the game remember that? 427 | 428 | If the server crashes and is restarted, what state is everything in? Are all mobs repopulated into their default positions? Or, does the game remember where they all were 5 minutes ago? 429 | 430 | #### Building 431 | 432 | How do builders extend the game? Dynamically while players are online? Offline by editing area files? 433 | 434 | Do you have some sort of "builder interface" used only by builders to extend the game while it is running? 435 | 436 | Do you provide some sort of web-page interface for builders? 437 | 438 | Does the game need to be restarted to incorporate additional content? 439 | 440 | # Specific Design 441 | 442 | ## The World 443 | 444 | ### Geography --------------------------------------------------------------------------------