aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorFuwn <[email protected]>2021-05-20 17:05:59 +0000
committerFuwn <[email protected]>2021-05-20 17:05:59 +0000
commit9dbc613765de8ab7dfa8e1374cf6661dcfd56bc8 (patch)
tree8cfff6a23bb72db2660e68c63a8cf9d0539a061f /crates
parentfeat(readme): add sqlfluff as a dev dep (diff)
downloadarchived-whirl-9dbc613765de8ab7dfa8e1374cf6661dcfd56bc8.tar.xz
archived-whirl-9dbc613765de8ab7dfa8e1374cf6661dcfd56bc8.zip
refactor(global): move crates around, stricter module isolation
Diffstat (limited to 'crates')
-rw-r--r--crates/whirl/.license_template2
-rw-r--r--crates/whirl/Cargo.toml69
-rw-r--r--crates/whirl/src/cli.rs93
-rw-r--r--crates/whirl/src/lib.rs29
-rw-r--r--crates/whirl/src/main.rs9
-rw-r--r--crates/whirl/src/subs.rs49
-rw-r--r--crates/whirl/src/whirl.rs38
-rw-r--r--crates/whirl_api/Cargo.toml30
-rw-r--r--crates/whirl_api/src/lib.rs46
-rw-r--r--crates/whirl_api/src/routes/mod.rs4
-rw-r--r--crates/whirl_api/src/routes/stats/mod.rs33
-rw-r--r--crates/whirl_api/src/routes/stats/structures.rs21
-rw-r--r--crates/whirl_common/Cargo.toml18
-rw-r--r--crates/whirl_common/src/lib.rs16
-rw-r--r--crates/whirl_common/src/log.rs19
-rw-r--r--crates/whirl_common/src/sort.rs6
-rw-r--r--crates/whirl_common/src/system.rs38
-rw-r--r--crates/whirl_config/.license_template2
-rw-r--r--crates/whirl_config/Cargo.toml25
-rw-r--r--crates/whirl_config/Whirl.default.toml24
-rw-r--r--crates/whirl_config/src/lib.rs90
-rw-r--r--crates/whirl_config/src/structures.rs37
-rw-r--r--crates/whirl_db/Cargo.toml22
-rw-r--r--crates/whirl_db/src/lib.rs48
-rw-r--r--crates/whirl_db/src/models.rs50
-rw-r--r--crates/whirl_db/src/schema.rs35
-rw-r--r--crates/whirl_prompt/.license_template2
-rw-r--r--crates/whirl_prompt/Cargo.toml25
-rw-r--r--crates/whirl_prompt/src/builtins/mod.rs108
-rw-r--r--crates/whirl_prompt/src/builtins/structures.rs34
-rw-r--r--crates/whirl_prompt/src/constants.rs19
-rw-r--r--crates/whirl_prompt/src/lib.rs150
-rw-r--r--crates/whirl_prompt/src/structure.rs10
-rw-r--r--crates/whirl_server/.license_template2
-rw-r--r--crates/whirl_server/Cargo.toml37
-rw-r--r--crates/whirl_server/src/cmd/commands/action.rs26
-rw-r--r--crates/whirl_server/src/cmd/commands/buddy_list.rs48
-rw-r--r--crates/whirl_server/src/cmd/commands/mod.rs12
-rw-r--r--crates/whirl_server/src/cmd/commands/property/create.rs168
-rw-r--r--crates/whirl_server/src/cmd/commands/property/mod.rs5
-rw-r--r--crates/whirl_server/src/cmd/commands/property/parse.rs14
-rw-r--r--crates/whirl_server/src/cmd/commands/redirect_id.rs42
-rw-r--r--crates/whirl_server/src/cmd/commands/room_id_request.rs18
-rw-r--r--crates/whirl_server/src/cmd/commands/subscribe_distance.rs24
-rw-r--r--crates/whirl_server/src/cmd/commands/subscribe_room.rs30
-rw-r--r--crates/whirl_server/src/cmd/commands/teleport.rs34
-rw-r--r--crates/whirl_server/src/cmd/commands/text.rs67
-rw-r--r--crates/whirl_server/src/cmd/constants.rs32
-rw-r--r--crates/whirl_server/src/cmd/extendable.rs18
-rw-r--r--crates/whirl_server/src/cmd/mod.rs9
-rw-r--r--crates/whirl_server/src/cmd/set_parser.rs39
-rw-r--r--crates/whirl_server/src/cmd/structure.rs22
-rw-r--r--crates/whirl_server/src/distributor.rs148
-rw-r--r--crates/whirl_server/src/hub.rs161
-rw-r--r--crates/whirl_server/src/interaction/mod.rs5
-rw-r--r--crates/whirl_server/src/interaction/peer.rs49
-rw-r--r--crates/whirl_server/src/interaction/shared.rs28
-rw-r--r--crates/whirl_server/src/lib.rs90
-rw-r--r--crates/whirl_server/src/net/constants.rs93
-rw-r--r--crates/whirl_server/src/net/converter.rs57
-rw-r--r--crates/whirl_server/src/net/mod.rs7
-rw-r--r--crates/whirl_server/src/net/property_parser.rs38
-rw-r--r--crates/whirl_server/src/net/structure.rs18
-rw-r--r--crates/whirl_server/src/packet_parser.rs38
-rw-r--r--crates/whirl_server/src/types.rs8
65 files changed, 2588 insertions, 0 deletions
diff --git a/crates/whirl/.license_template b/crates/whirl/.license_template
new file mode 100644
index 0000000..1fda769
--- /dev/null
+++ b/crates/whirl/.license_template
@@ -0,0 +1,2 @@
+// Copyleft (ɔ) {20\d{2}(-20\d{2})?} The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
diff --git a/crates/whirl/Cargo.toml b/crates/whirl/Cargo.toml
new file mode 100644
index 0000000..730309b
--- /dev/null
+++ b/crates/whirl/Cargo.toml
@@ -0,0 +1,69 @@
+[package]
+name = "whirl"
+version = "0.1.0"
+authors = ["Fuwn <[email protected]>"]
+edition = "2018"
+description = "Whirl, an open-source WorldServer implementation in Rust."
+documentation = "https://whirlsplash.org/docs/"
+readme = "../../README.rst"
+homepage = "https://whirlsplash.org"
+repository = "https://github.com/Whirlsplash/whirl"
+license = "GPL-3.0-only"
+# license-file = "LICENSE"
+keywords = ["rust", "worldserver", "whirl", "whirlsplash"]
+publish = false
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+# Environment
+dotenv = "0.15.0"
+
+# Logging
+log = "0.4.14"
+flexi_logger = "0.17.1"
+human-panic = "1.0.3"
+# simple-error = "0.2.3"
+
+# Utility
+rand = "0.8.3"
+async-trait = "0.1.50"
+sysinfo = "0.17.5"
+whirl_common = { path = "../whirl_common" }
+
+# Byte Manipulation
+bytes = "1.0.1"
+byteorder = "1.4.3"
+
+# Serialization
+serde = "1.0.126"
+serde_derive = "1.0.126"
+
+# CLI
+structopt = "0.3.21"
+
+# Config
+whirl_config = { path = "../whirl_config" }
+
+# TCP
+tokio = { version = "1.6.0", features = ["full"] }
+tokio-util = { version = "0.6.7", features = ["codec"] }
+tokio-stream = "0.1.6"
+
+# Web-server
+actix-web = { version = "3.3.2", features = ["rustls"] }
+actix-cors = "0.5.4"
+whirl_api = { path = "../whirl_api" }
+
+# Prompt
+whirl_prompt = { path = "../whirl_prompt" }
+
+# Server
+whirl_server = { path = "../whirl_server" }
+
+# Allocator
+[target.'cfg(windows)'.dependencies]
+mimalloc = { version = "0.1.25", default-features = false }
+
+[target.'cfg(unix)'.dependencies]
+jemallocator = "0.3.2"
diff --git a/crates/whirl/src/cli.rs b/crates/whirl/src/cli.rs
new file mode 100644
index 0000000..82d18c1
--- /dev/null
+++ b/crates/whirl/src/cli.rs
@@ -0,0 +1,93 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use structopt::clap::{App, AppSettings, Arg, ArgMatches, Shell, SubCommand};
+use whirl_config::Config;
+
+use crate::subs::run;
+
+pub struct Cli;
+impl Cli {
+ pub fn setup() -> ArgMatches<'static> {
+ let matches = Self::cli().get_matches();
+
+ std::env::set_var("DATABASE_URL", "whirl.sqlite3");
+
+ matches
+ }
+
+ pub async fn execute(matches: ArgMatches<'_>) -> Result<(), Box<dyn std::error::Error>> {
+ if Config::get().whirlsplash.log.test {
+ error!("error");
+ warn!("warn");
+ info!("info");
+ debug!("debug");
+ trace!("trace");
+ }
+
+ if matches.is_present("run") {
+ run().await;
+ } else if let Some(cmd) = matches.subcommand_matches("config") {
+ if cmd.is_present("show") {
+ println!("{:#?}", Config::get());
+ }
+ } else if let Some(shell) = matches.subcommand_matches("completions") {
+ if shell.is_present("powershell") {
+ Self::cli().gen_completions(env!("CARGO_PKG_NAME"), Shell::PowerShell, ".");
+ } else if shell.is_present("bash") {
+ Self::cli().gen_completions(env!("CARGO_PKG_NAME"), Shell::Bash, ".");
+ } else if shell.is_present("elvish") {
+ Self::cli().gen_completions(env!("CARGO_PKG_NAME"), Shell::Elvish, ".");
+ } else if shell.is_present("zsh") {
+ Self::cli().gen_completions(env!("CARGO_PKG_NAME"), Shell::Zsh, ".");
+ } else if shell.is_present("fish") {
+ Self::cli().gen_completions(env!("CARGO_PKG_NAME"), Shell::Fish, ".");
+ }
+ debug!("generated shell completions");
+ } else if matches.is_present("clean") {
+ let cleanable_directories = vec!["./log/"];
+ for dir in cleanable_directories {
+ let mut file_type = "directory";
+ if !dir.ends_with('/') {
+ file_type = "file";
+ }
+ println!("cleaning {}: {}", file_type, dir);
+ if let Err(e) = std::fs::remove_dir_all(dir) {
+ warn!("cannot delete {}: {}: {}", file_type, dir, e);
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ fn cli() -> App<'static, 'static> {
+ App::new(env!("CARGO_PKG_NAME"))
+ .about(env!("CARGO_PKG_DESCRIPTION"))
+ .version(env!("CARGO_PKG_VERSION"))
+ .author(env!("CARGO_PKG_AUTHORS"))
+ .settings(&[AppSettings::SubcommandRequiredElseHelp])
+ .subcommands(vec![
+ SubCommand::with_name("run").about("Start the WorldServer"),
+ SubCommand::with_name("config")
+ .setting(AppSettings::SubcommandRequiredElseHelp)
+ .subcommands(vec![SubCommand::with_name("show")]),
+ SubCommand::with_name("completions")
+ .setting(AppSettings::SubcommandRequiredElseHelp)
+ .about("Generate shell completions")
+ .subcommands(vec![
+ SubCommand::with_name("powershell"),
+ SubCommand::with_name("bash"),
+ SubCommand::with_name("elvish"),
+ SubCommand::with_name("zsh"),
+ SubCommand::with_name("fish"),
+ ]),
+ SubCommand::with_name("clean")
+ .about("Delete Whirl generated files/ directories which are NOT critical. E.g., logs/"),
+ ])
+ .args(&[
+ Arg::with_name("debug").short("d").long("debug"),
+ Arg::with_name("trace").short("t").long("trace"),
+ ])
+ }
+}
diff --git a/crates/whirl/src/lib.rs b/crates/whirl/src/lib.rs
new file mode 100644
index 0000000..9ef177c
--- /dev/null
+++ b/crates/whirl/src/lib.rs
@@ -0,0 +1,29 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+#![feature(
+ type_ascription,
+ hash_set_entry,
+ type_name_of_val,
+ decl_macro,
+ proc_macro_hygiene
+)]
+#![warn(rust_2018_idioms)]
+#![recursion_limit = "128"]
+
+#[macro_use]
+extern crate log;
+// #[macro_use]
+// extern crate simple_error;
+
+#[cfg(windows)]
+#[global_allocator]
+static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
+
+#[cfg(unix)]
+#[global_allocator]
+static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
+
+pub mod cli;
+pub mod subs;
+pub mod whirl;
diff --git a/crates/whirl/src/main.rs b/crates/whirl/src/main.rs
new file mode 100644
index 0000000..074ca69
--- /dev/null
+++ b/crates/whirl/src/main.rs
@@ -0,0 +1,9 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::error::Error;
+
+use whirl::whirl::Whirl;
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn Error>> { Whirl::splash().await }
diff --git a/crates/whirl/src/subs.rs b/crates/whirl/src/subs.rs
new file mode 100644
index 0000000..08da4d6
--- /dev/null
+++ b/crates/whirl/src/subs.rs
@@ -0,0 +1,49 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use whirl_api::Api;
+use whirl_config::Config;
+use whirl_prompt::Prompt;
+use whirl_server::{
+ distributor::Distributor,
+ hub::Hub,
+ Server,
+ ServerType::{AutoServer, RoomServer},
+};
+
+pub async fn run() {
+ let (tx, _rx) = std::sync::mpsc::channel();
+
+ let _threads = vec![
+ tokio::spawn(async move {
+ let _ = Distributor::listen(
+ &*format!("0.0.0.0:{}", Config::get().distributor.port),
+ AutoServer,
+ )
+ .await;
+ }),
+ tokio::spawn(async move {
+ let _ = Hub::listen(&*format!("0.0.0.0:{}", Config::get().hub.port), RoomServer).await;
+ }),
+ tokio::spawn(async move {
+ let _ = Api::listen(
+ tx,
+ &*format!("0.0.0.0:{}", Config::get().whirlsplash.api.port),
+ )
+ .await;
+ }),
+ ];
+
+ if std::env::var("DISABLE_PROMPT").unwrap_or_else(|_| "false".to_string()) == "true"
+ || !Config::get().whirlsplash.prompt.enable
+ {
+ info!("starting with prompt disabled");
+ loop {
+ std::thread::sleep(std::time::Duration::default());
+ }
+ } else {
+ Prompt::handle().await;
+ }
+
+ // actix_web::rt::System::new("").block_on(rx.recv().unwrap().stop(true));
+}
diff --git a/crates/whirl/src/whirl.rs b/crates/whirl/src/whirl.rs
new file mode 100644
index 0000000..451d79f
--- /dev/null
+++ b/crates/whirl/src/whirl.rs
@@ -0,0 +1,38 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::error::Error;
+
+use whirl_common::log::calculate_log_level;
+
+use crate::cli::Cli;
+
+pub struct Whirl;
+impl Whirl {
+ pub async fn splash() -> Result<(), Box<dyn Error>> {
+ // Environment and CLI
+ let matches = Cli::setup();
+
+ // Logging
+ dotenv::dotenv().ok();
+ human_panic::setup_panic!();
+ let logger = flexi_logger::Logger::with_str(calculate_log_level());
+ if std::env::var("LOG_FILE").unwrap_or_else(|_| "true".to_string()) == "false"
+ || !whirl_config::Config::get().whirlsplash.log.file
+ || std::env::args().collect::<Vec<_>>()[1] == "clean"
+ // Cheeky as all hell.
+ {
+ logger.start()?;
+ } else {
+ logger
+ .print_message()
+ .log_to_file()
+ .directory("log")
+ .start()?;
+ }
+
+ Cli::execute(matches).await.unwrap();
+
+ Ok(())
+ }
+}
diff --git a/crates/whirl_api/Cargo.toml b/crates/whirl_api/Cargo.toml
new file mode 100644
index 0000000..7687b42
--- /dev/null
+++ b/crates/whirl_api/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "whirl_api"
+version = "0.1.0"
+authors = ["Fuwn <[email protected]>"]
+edition = "2018"
+description = "Whirl, an open-source WorldServer implementation in Rust."
+documentation = "https://whirlsplash.org/docs/"
+readme = "../../README.rst"
+homepage = "https://whirlsplash.org"
+repository = "https://github.com/Whirlsplash/whirl"
+license = "GPL-3.0-only"
+# license-file = "LICENSE"
+keywords = ["rust", "worldserver", "whirl", "whirlsplash"]
+publish = false
+
+[dependencies]
+# Web-server
+actix-web = { version = "3.3.2", features = ["rustls"] }
+actix-cors = "0.5.4"
+
+# Utility
+sysinfo = "0.17.5"
+whirl_common = { path = "../whirl_common" }
+
+# Serialization
+serde = "1.0.126"
+serde_derive = "1.0.126"
+
+# Logging
+log = "0.4.14"
diff --git a/crates/whirl_api/src/lib.rs b/crates/whirl_api/src/lib.rs
new file mode 100644
index 0000000..e77e7d3
--- /dev/null
+++ b/crates/whirl_api/src/lib.rs
@@ -0,0 +1,46 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+#![feature(
+ type_ascription,
+ hash_set_entry,
+ type_name_of_val,
+ decl_macro,
+ proc_macro_hygiene
+)]
+#![warn(rust_2018_idioms)]
+#![recursion_limit = "128"]
+
+#[macro_use]
+extern crate log;
+#[macro_use]
+extern crate serde_derive;
+
+use actix_web::web::resource;
+
+mod routes;
+
+pub struct Api;
+impl Api {
+ pub async fn listen(
+ tx: std::sync::mpsc::Sender<actix_web::dev::Server>,
+ address: &str,
+ ) -> std::io::Result<()> {
+ let mut sys = actix_web::rt::System::new("api");
+
+ let server = actix_web::HttpServer::new(|| {
+ actix_web::App::new()
+ .wrap(actix_cors::Cors::default().allow_any_origin())
+ .service(resource("/").to(|| async { "Whirlsplash" }))
+ .service(resource("/api/v1/statistics").to(routes::stats::statistics))
+ })
+ .bind(address)?
+ .run();
+
+ info!("http api now listening at {}", address);
+
+ let _ = tx.send(server.clone());
+
+ sys.block_on(server)
+ }
+}
diff --git a/crates/whirl_api/src/routes/mod.rs b/crates/whirl_api/src/routes/mod.rs
new file mode 100644
index 0000000..f5a2ff4
--- /dev/null
+++ b/crates/whirl_api/src/routes/mod.rs
@@ -0,0 +1,4 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub mod stats;
diff --git a/crates/whirl_api/src/routes/stats/mod.rs b/crates/whirl_api/src/routes/stats/mod.rs
new file mode 100644
index 0000000..d5d0937
--- /dev/null
+++ b/crates/whirl_api/src/routes/stats/mod.rs
@@ -0,0 +1,33 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub mod structures;
+
+use actix_web::HttpResponse;
+use sysinfo::{get_current_pid, ProcessExt, System, SystemExt};
+use whirl_common::system::seconds_to_hrtime;
+
+use crate::routes::stats::structures::{Statistics, StatisticsProcess, StatisticsSystem};
+
+// This is mostly for developmental testing, it consumes more CPU than it's
+// worth.
+pub fn statistics() -> HttpResponse {
+ let mut sys = System::new_all();
+ sys.refresh_all();
+
+ let process = sys.get_process(get_current_pid().unwrap()).unwrap();
+
+ HttpResponse::Ok().json(Statistics {
+ system: StatisticsSystem {
+ os_type: sys.get_name().unwrap(),
+ release: sys.get_kernel_version().unwrap(),
+ uptime: seconds_to_hrtime(sysinfo::System::new().get_uptime() as usize),
+ },
+ process: StatisticsProcess {
+ // (process.cpu_usage() * 100.0).round() / 100.0
+ memory_usage: (process.memory() / 1000).to_string(),
+ cpu_usage: (process.cpu_usage() / sys.get_processors().len() as f32).to_string(),
+ // uptime: seconds_to_hrtime((sys.get_uptime() - process.start_time()) as usize),
+ },
+ })
+}
diff --git a/crates/whirl_api/src/routes/stats/structures.rs b/crates/whirl_api/src/routes/stats/structures.rs
new file mode 100644
index 0000000..88fc852
--- /dev/null
+++ b/crates/whirl_api/src/routes/stats/structures.rs
@@ -0,0 +1,21 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+#[derive(Serialize)]
+pub struct Statistics {
+ pub system: StatisticsSystem,
+ pub process: StatisticsProcess,
+}
+#[derive(Serialize)]
+pub struct StatisticsSystem {
+ #[serde(rename = "type")]
+ pub os_type: String,
+ pub release: String,
+ pub uptime: String,
+}
+#[derive(Serialize)]
+pub struct StatisticsProcess {
+ pub memory_usage: String,
+ pub cpu_usage: String,
+ // pub uptime: String,
+}
diff --git a/crates/whirl_common/Cargo.toml b/crates/whirl_common/Cargo.toml
new file mode 100644
index 0000000..aea1c8f
--- /dev/null
+++ b/crates/whirl_common/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "whirl_common"
+version = "0.1.0"
+authors = ["Fuwn <[email protected]>"]
+edition = "2018"
+description = "Whirl, an open-source WorldServer implementation in Rust."
+documentation = "https://whirlsplash.org/docs/"
+readme = "../../README.rst"
+homepage = "https://whirlsplash.org"
+repository = "https://github.com/Whirlsplash/whirl"
+license = "GPL-3.0-only"
+# license-file = "LICENSE"
+keywords = ["rust", "worldserver", "whirl", "whirlsplash"]
+publish = false
+
+[dependencies]
+# Config
+whirl_config = { path = "../whirl_config" }
diff --git a/crates/whirl_common/src/lib.rs b/crates/whirl_common/src/lib.rs
new file mode 100644
index 0000000..05a0e0c
--- /dev/null
+++ b/crates/whirl_common/src/lib.rs
@@ -0,0 +1,16 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+#![feature(
+ type_ascription,
+ hash_set_entry,
+ type_name_of_val,
+ decl_macro,
+ proc_macro_hygiene
+)]
+#![warn(rust_2018_idioms)]
+#![recursion_limit = "128"]
+
+pub mod log;
+pub mod sort;
+pub mod system;
diff --git a/crates/whirl_common/src/log.rs b/crates/whirl_common/src/log.rs
new file mode 100644
index 0000000..39ceca0
--- /dev/null
+++ b/crates/whirl_common/src/log.rs
@@ -0,0 +1,19 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use whirl_config::Config;
+
+pub fn calculate_log_level() -> String {
+ let mut level;
+
+ level = match Config::get().whirlsplash.log.level {
+ 2 => "debug".to_string(),
+ 3 => "trace".to_string(),
+ _ => "info".to_string(),
+ };
+ if !Config::get().whirlsplash.log.everything {
+ level = format!("whirl={}", level);
+ }
+
+ level
+}
diff --git a/crates/whirl_common/src/sort.rs b/crates/whirl_common/src/sort.rs
new file mode 100644
index 0000000..131fa55
--- /dev/null
+++ b/crates/whirl_common/src/sort.rs
@@ -0,0 +1,6 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub fn sort_vec_alphabetically(vec: &mut Vec<&str>) {
+ vec.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
+}
diff --git a/crates/whirl_common/src/system.rs b/crates/whirl_common/src/system.rs
new file mode 100644
index 0000000..7a823a0
--- /dev/null
+++ b/crates/whirl_common/src/system.rs
@@ -0,0 +1,38 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+const WEEK: usize = 60 * 60 * 60 * 60;
+const DAY: usize = 60 * 60 * 60;
+const HOUR: usize = 60 * 60;
+const MIN: usize = 60;
+
+fn make_parts(t: usize, steps: &[usize], mut accum: Vec<usize>) -> Vec<usize> {
+ match steps.split_first() {
+ None => accum,
+ Some((s, steps)) => {
+ accum.push(t / *s);
+ make_parts(t % *s, steps, accum)
+ }
+ }
+}
+
+pub fn seconds_to_hrtime(seconds: usize) -> String {
+ let word = ["week", "day", "hour", "min", "sec"];
+
+ make_parts(seconds, &[WEEK, DAY, HOUR, MIN, 1], Vec::new())
+ .iter()
+ .enumerate()
+ .filter_map(|(i, s)| {
+ if s > &0 {
+ if s > &1 {
+ Some(format!("{} {}s", s, word[i]))
+ } else {
+ Some(format!("{} {}", s, word[i]))
+ }
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<String>>()
+ .join(", ")
+}
diff --git a/crates/whirl_config/.license_template b/crates/whirl_config/.license_template
new file mode 100644
index 0000000..1fda769
--- /dev/null
+++ b/crates/whirl_config/.license_template
@@ -0,0 +1,2 @@
+// Copyleft (ɔ) {20\d{2}(-20\d{2})?} The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
diff --git a/crates/whirl_config/Cargo.toml b/crates/whirl_config/Cargo.toml
new file mode 100644
index 0000000..64100d1
--- /dev/null
+++ b/crates/whirl_config/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "whirl_config"
+version = "0.1.0"
+authors = ["Fuwn <[email protected]>"]
+edition = "2018"
+description = "Whirl, an open-source WorldServer implementation in Rust."
+documentation = "https://whirlsplash.org/docs/"
+readme = "../../README.rst"
+homepage = "https://whirlsplash.org"
+repository = "https://github.com/Whirlsplash/whirl"
+license = "GPL-3.0-only"
+# license-file = "LICENSE"
+keywords = ["rust", "worldserver", "whirl", "whirlsplash"]
+publish = false
+
+[dependencies]
+# Config
+config = "0.11.0"
+
+# Serialization
+serde = "1.0.126"
+serde_derive = "1.0.126"
+
+# Logging
+log = "0.4.14"
diff --git a/crates/whirl_config/Whirl.default.toml b/crates/whirl_config/Whirl.default.toml
new file mode 100644
index 0000000..aa2d684
--- /dev/null
+++ b/crates/whirl_config/Whirl.default.toml
@@ -0,0 +1,24 @@
+# See more keys and their definitions at https://whirlsplash.org/docs/whirl/configuration
+
+[whirlsplash]
+worldsmaster_username = "WORLDSMASTER"
+ip = "0.0.0.0"
+api.port = 8080
+
+[whirlsplash.prompt]
+enable = false
+ps1 = "[WORLDSMASTER@Whirlsplash ~]$"
+
+[whirlsplash.log]
+enable = true
+level = 1
+everything = false
+test = false
+file = true
+
+[distributor]
+worldsmaster_greeting = "Welcome to Whirlsplash!"
+port = 6650
+
+[hub]
+port = 5673
diff --git a/crates/whirl_config/src/lib.rs b/crates/whirl_config/src/lib.rs
new file mode 100644
index 0000000..8f312cd
--- /dev/null
+++ b/crates/whirl_config/src/lib.rs
@@ -0,0 +1,90 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+#![feature(
+ type_ascription,
+ hash_set_entry,
+ type_name_of_val,
+ decl_macro,
+ proc_macro_hygiene
+)]
+#![warn(rust_2018_idioms)]
+#![recursion_limit = "128"]
+
+#[macro_use]
+extern crate serde_derive;
+#[macro_use]
+extern crate log;
+
+mod structures;
+
+use config::{ConfigError, File};
+
+use crate::structures::{
+ DistributorConfig,
+ HubConfig,
+ WhirlsplashApiConfig,
+ WhirlsplashConfig,
+ WhirlsplashLogConfig,
+ WhirlsplashPromptConfig,
+};
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct Config {
+ pub whirlsplash: WhirlsplashConfig,
+ pub distributor: DistributorConfig,
+ pub hub: HubConfig,
+}
+impl Config {
+ pub fn refresh() { let _ = config::Config::new().refresh(); }
+
+ fn load() -> Result<Self, ConfigError> {
+ let mut s = config::Config::new();
+
+ s.merge(File::with_name("./Whirl.toml").required(false))?;
+ s.try_into()
+ }
+
+ pub fn get() -> Config {
+ return if let Err(why) = Self::load() {
+ error!(
+ "unable to load configuration file, reverting to default value: {}",
+ why
+ );
+ Self::default()
+ } else {
+ Self::load().unwrap()
+ };
+ }
+}
+impl Default for Config {
+ fn default() -> Self {
+ Config {
+ whirlsplash: WhirlsplashConfig {
+ worldsmaster_username: "WORLDSMASTER".to_string(),
+ ip: "0.0.0.0".to_string(),
+ api: WhirlsplashApiConfig {
+ port: 80
+ },
+ prompt: WhirlsplashPromptConfig {
+ enable: false,
+ ps1: "[WORLDSMASTER@Whirlsplash ~]$".to_string(),
+ },
+ log: WhirlsplashLogConfig {
+ enable: true,
+ level: 1,
+ everything: false,
+ test: false,
+ file: true,
+ },
+ },
+ distributor: DistributorConfig {
+ worldsmaster_greeting: "Welcome to Whirlsplash!".to_string(),
+ port: 6650,
+ },
+ hub: HubConfig {
+ port: 5673
+ },
+ }
+ }
+}
diff --git a/crates/whirl_config/src/structures.rs b/crates/whirl_config/src/structures.rs
new file mode 100644
index 0000000..4f1d62e
--- /dev/null
+++ b/crates/whirl_config/src/structures.rs
@@ -0,0 +1,37 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct WhirlsplashConfig {
+ pub worldsmaster_username: String,
+ pub ip: String,
+ pub api: WhirlsplashApiConfig,
+ pub prompt: WhirlsplashPromptConfig,
+ pub log: WhirlsplashLogConfig,
+}
+#[derive(Serialize, Deserialize, Debug)]
+pub struct WhirlsplashApiConfig {
+ pub port: i64,
+}
+#[derive(Serialize, Deserialize, Debug)]
+pub struct WhirlsplashPromptConfig {
+ pub enable: bool,
+ pub ps1: String,
+}
+#[derive(Serialize, Deserialize, Debug)]
+pub struct WhirlsplashLogConfig {
+ pub enable: bool,
+ pub level: i64,
+ pub everything: bool,
+ pub test: bool,
+ pub file: bool,
+}
+#[derive(Serialize, Deserialize, Debug)]
+pub struct DistributorConfig {
+ pub worldsmaster_greeting: String,
+ pub port: i64,
+}
+#[derive(Serialize, Deserialize, Debug)]
+pub struct HubConfig {
+ pub port: i64,
+}
diff --git a/crates/whirl_db/Cargo.toml b/crates/whirl_db/Cargo.toml
new file mode 100644
index 0000000..0e9bdf0
--- /dev/null
+++ b/crates/whirl_db/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "whirl_db"
+version = "0.1.0"
+authors = ["Fuwn <[email protected]>"]
+edition = "2018"
+description = "Whirl, an open-source WorldServer implementation in Rust."
+documentation = "https://whirlsplash.org/docs/"
+readme = "../../README.rst"
+homepage = "https://whirlsplash.org"
+repository = "https://github.com/Whirlsplash/whirl"
+license = "GPL-3.0-only"
+# license-file = "LICENSE"
+keywords = ["rust", "worldserver", "whirl", "whirlsplash"]
+publish = false
+
+[dependencies]
+# Database
+libsqlite3-sys = { version = "0.9.1", features = ["bundled"] }
+diesel = { version = "1.4.6", features = ["sqlite"] }
+
+# Environment
+dotenv = "0.15.0"
diff --git a/crates/whirl_db/src/lib.rs b/crates/whirl_db/src/lib.rs
new file mode 100644
index 0000000..35f05ae
--- /dev/null
+++ b/crates/whirl_db/src/lib.rs
@@ -0,0 +1,48 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+#![feature(
+ type_ascription,
+ hash_set_entry,
+ type_name_of_val,
+ decl_macro,
+ proc_macro_hygiene
+)]
+#![warn(rust_2018_idioms)]
+#![recursion_limit = "128"]
+
+#[macro_use]
+extern crate diesel;
+
+pub mod models;
+mod schema;
+
+use diesel::prelude::*;
+
+// use crate::db::models::*;
+
+pub fn establish_connection() -> SqliteConnection {
+ let database_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| "whirl.sqlite3".to_string());
+ SqliteConnection::establish(&database_url)
+ .unwrap_or_else(|_| panic!("error connecting to {}", database_url))
+}
+
+/// Only works if you have a valid database already setup!
+#[cfg(test)]
+#[test]
+#[ignore]
+pub fn show_serials() {
+ use crate::{models::SerialNumber, schema::serial_numbers::dsl::*};
+
+ dotenv::dotenv().ok();
+
+ let results = serial_numbers
+ .limit(5)
+ .load::<SerialNumber>(&establish_connection())
+ .expect("error loading serial numbers table");
+
+ println!("found {} results", results.len());
+ for result in results {
+ println!("{}", result.user_name);
+ }
+}
diff --git a/crates/whirl_db/src/models.rs b/crates/whirl_db/src/models.rs
new file mode 100644
index 0000000..52304b6
--- /dev/null
+++ b/crates/whirl_db/src/models.rs
@@ -0,0 +1,50 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+//! Much of the documentation that you will see within this module is quoted
+//! from http://dev.worlds.net/private/GammaDocs/WorldServer.html#RoomServer.
+
+// use crate::db::schema::*;
+
+// --------------
+// | Queryables |
+// --------------
+
+#[derive(Queryable, Debug)]
+pub struct SerialNumber {
+ pub serial_number: String,
+ pub user_name: String,
+ pub serial_status: i32,
+}
+
+#[derive(Queryable, Debug)]
+pub struct UserRegistration {
+ pub user_name_lower: String,
+ pub user_name: String,
+ pub serial_number: String,
+ pub password: String,
+ pub client_version: String,
+ pub account_status: i32,
+ pub registration_date: String,
+ pub times_on: i32,
+ pub total_minutes: i32,
+ pub user_privileges: i32,
+}
+
+#[derive(Queryable, Debug)]
+pub struct UserProperty {
+ pub user_name: String,
+ pub property_id: i32,
+ pub property_flags: i32,
+ pub property_access: i32,
+ pub property_string_value: String,
+ pub property_binary_value: String,
+}
+
+// ---------------
+// | Insertables |
+// ---------------
+
+// --------------
+// | Updatables |
+// --------------
diff --git a/crates/whirl_db/src/schema.rs b/crates/whirl_db/src/schema.rs
new file mode 100644
index 0000000..6d58598
--- /dev/null
+++ b/crates/whirl_db/src/schema.rs
@@ -0,0 +1,35 @@
+table! {
+ serial_numbers (user_name) {
+ serial_number -> Text,
+ user_name -> Text,
+ serial_status -> Integer,
+ }
+}
+
+table! {
+ user_properties (user_name) {
+ user_name -> Text,
+ property_id -> Integer,
+ property_flags -> Integer,
+ property_access -> Integer,
+ property_string_value -> Integer,
+ property_binary_value -> Nullable<Text>,
+ }
+}
+
+table! {
+ user_registration (user_name) {
+ user_name_lower -> Text,
+ user_name -> Text,
+ serial_number -> Text,
+ password -> Text,
+ client_version -> Text,
+ account_status -> Integer,
+ registration_date -> Text,
+ times_on -> Integer,
+ total_minutes -> Integer,
+ user_privileges -> Integer,
+ }
+}
+
+allow_tables_to_appear_in_same_query!(serial_numbers, user_properties, user_registration,);
diff --git a/crates/whirl_prompt/.license_template b/crates/whirl_prompt/.license_template
new file mode 100644
index 0000000..1fda769
--- /dev/null
+++ b/crates/whirl_prompt/.license_template
@@ -0,0 +1,2 @@
+// Copyleft (ɔ) {20\d{2}(-20\d{2})?} The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
diff --git a/crates/whirl_prompt/Cargo.toml b/crates/whirl_prompt/Cargo.toml
new file mode 100644
index 0000000..00abbcf
--- /dev/null
+++ b/crates/whirl_prompt/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "whirl_prompt"
+version = "0.1.0"
+authors = ["Fuwn <[email protected]>"]
+edition = "2018"
+description = "Whirl, an open-source WorldServer implementation in Rust."
+documentation = "https://whirlsplash.org/docs/"
+readme = "../../README.rst"
+homepage = "https://whirlsplash.org"
+repository = "https://github.com/Whirlsplash/whirl"
+license = "GPL-3.0-only"
+# license-file = "LICENSE"
+keywords = ["rust", "worldserver", "whirl", "whirlsplash"]
+publish = false
+
+[dependencies]
+# Utility
+sysinfo = "0.17.5"
+colour = "0.6.0"
+
+# Config
+whirl_config = { path = "../whirl_config" }
+
+# Web
+curl = "0.4.37"
diff --git a/crates/whirl_prompt/src/builtins/mod.rs b/crates/whirl_prompt/src/builtins/mod.rs
new file mode 100644
index 0000000..0359443
--- /dev/null
+++ b/crates/whirl_prompt/src/builtins/mod.rs
@@ -0,0 +1,108 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub mod structures;
+
+use std::io::Write;
+
+use sysinfo::SystemExt;
+use whirl_config::Config;
+
+use crate::constants::{FILES, HELPABLES_BUILTINS, HELPABLES_BUILTIN_CONFIG};
+
+pub fn builtin_echo(args: &[String]) -> i32 {
+ println!("{}", args.join(" "));
+ 0
+}
+
+pub fn builtin_history(history: &[String]) -> i32 {
+ for (index, cmd) in history.iter().enumerate() {
+ println!("{} {}", index, cmd.trim());
+ }
+ 0
+}
+
+pub fn builtin_help() -> i32 {
+ for help in HELPABLES_BUILTINS.iter() {
+ println!("{}", help);
+ }
+
+ 0
+}
+
+pub fn builtin_ls() -> i32 {
+ for file in &FILES {
+ print!("{} ", file);
+ }
+ println!();
+
+ 0
+}
+
+pub async fn builtin_cat(args: &[String]) -> i32 {
+ let file;
+ if let Some(file_name) = args.get(0) {
+ file = file_name.to_string();
+ } else {
+ return 0;
+ };
+
+ match file.as_str() {
+ "README.rst" => {
+ let mut easy = curl::easy::Easy::new();
+
+ easy
+ .url("https://raw.githubusercontent.com/Whirlsplash/whirl/develop/README.rst")
+ .unwrap();
+
+ let mut transfer = easy.transfer();
+ transfer
+ .write_function(|data| {
+ std::io::stdout().write_all(data).unwrap();
+ Ok(data.len())
+ })
+ .unwrap();
+ transfer.perform().unwrap();
+ }
+ "Whirl.toml" => {
+ colour::red_ln!("NOTE: This is just a wrapper for `config show`.");
+ println!("{:#?}", Config::get());
+ }
+ _ => println!("/cat: {}: no such file or directory", file),
+ }
+
+ 0
+}
+
+pub fn builtin_config(args: &[String]) -> i32 {
+ match args.get(0) {
+ Some(sub) =>
+ match sub.as_str() {
+ "show" => println!("{:#?}", Config::get()),
+ "help" | "--help" | "-h" =>
+ for help in HELPABLES_BUILTIN_CONFIG.iter() {
+ println!("{}", help);
+ },
+ "refresh" => Config::refresh(),
+ _ => println!("invalid arguments provided"),
+ },
+ None => println!("invalid amount arguments provided"),
+ }
+ 0
+}
+
+pub fn builtin_fetch() -> i32 {
+ // rfetch: https://github.com/Mangeshrex/rfetch
+
+ let mut sys = sysinfo::System::new();
+ sys.refresh_processes();
+
+ println!(" ");
+ println!(" .-. os {}", env!("CARGO_PKG_NAME"));
+ println!(" oo| ker {}", env!("CARGO_PKG_VERSION"));
+ println!(" / '\\ sh /wsh");
+ println!(" (\\_;/) up null");
+ println!(" ");
+
+ 0
+}
diff --git a/crates/whirl_prompt/src/builtins/structures.rs b/crates/whirl_prompt/src/builtins/structures.rs
new file mode 100644
index 0000000..4217a38
--- /dev/null
+++ b/crates/whirl_prompt/src/builtins/structures.rs
@@ -0,0 +1,34 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::str::FromStr;
+
+pub enum BuiltIn {
+ Echo,
+ History,
+ Exit,
+ Null,
+ Help,
+ Ls,
+ Cat,
+ Config,
+ Fetch,
+}
+impl FromStr for BuiltIn {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "echo" => Ok(BuiltIn::Echo),
+ "history" => Ok(BuiltIn::History),
+ "exit" => Ok(BuiltIn::Exit),
+ "null" => Ok(BuiltIn::Null),
+ "help" => Ok(BuiltIn::Help),
+ "ls" => Ok(BuiltIn::Ls),
+ "cat" => Ok(BuiltIn::Cat),
+ "config" => Ok(BuiltIn::Config),
+ "fetch" => Ok(BuiltIn::Fetch),
+ _ => Err(()),
+ }
+ }
+}
diff --git a/crates/whirl_prompt/src/constants.rs b/crates/whirl_prompt/src/constants.rs
new file mode 100644
index 0000000..c173a57
--- /dev/null
+++ b/crates/whirl_prompt/src/constants.rs
@@ -0,0 +1,19 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub const FILES: [&str; 2] = ["README.rst", "Whirl.toml"];
+pub const HELPABLES_BUILTINS: [&str; 8] = [
+ "cat - display the contents of a present file",
+ "config - manipulate the configuration",
+ "echo - display a line of predefined text",
+ "exit - end the process",
+ "fetch - a neofetch like utility loosely based on rfetch",
+ "help - you are here",
+ "history - display the command history",
+ "ls - display the present files",
+];
+pub const HELPABLES_BUILTIN_CONFIG: [&str; 3] = [
+ "help - you are here",
+ "refresh - reload the configuration file",
+ "show - display the current configuration",
+];
diff --git a/crates/whirl_prompt/src/lib.rs b/crates/whirl_prompt/src/lib.rs
new file mode 100644
index 0000000..32b247f
--- /dev/null
+++ b/crates/whirl_prompt/src/lib.rs
@@ -0,0 +1,150 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+#![feature(
+ type_ascription,
+ hash_set_entry,
+ type_name_of_val,
+ decl_macro,
+ proc_macro_hygiene
+)]
+#![warn(rust_2018_idioms)]
+#![recursion_limit = "128"]
+
+mod builtins;
+mod constants;
+mod structure;
+
+use std::{io, io::Write, str::FromStr};
+
+use whirl_config::Config;
+
+use crate::{
+ builtins::{
+ builtin_cat,
+ builtin_config,
+ builtin_echo,
+ builtin_fetch,
+ builtin_help,
+ builtin_history,
+ builtin_ls,
+ structures::BuiltIn,
+ },
+ structure::Command,
+};
+
+pub struct Prompt {
+ history: Vec<String>,
+}
+impl Prompt {
+ pub async fn handle() -> ! {
+ let mut prompt = Prompt {
+ history: vec![]
+ };
+
+ loop {
+ Prompt::write_prompt();
+ let command = prompt.read_command();
+ prompt
+ .process_command(Prompt::tokenize_command(command))
+ .await;
+ }
+ }
+
+ fn write_prompt() {
+ print!("{} ", Config::get().whirlsplash.prompt.ps1);
+ io::stdout().flush().unwrap();
+ }
+
+ fn read_command(&mut self) -> String {
+ let mut input = String::new();
+ io::stdin()
+ .read_line(&mut input)
+ .expect("failed to read command from stdin");
+
+ if input.len() <= 2 {
+ input = "null".to_string();
+ }
+
+ input
+ }
+
+ fn tokenize_command(c: String) -> Command {
+ let mut command_split: Vec<String> = c.split_whitespace().map(|s| s.to_string()).collect();
+
+ Command {
+ keyword: command_split.remove(0),
+ args: command_split,
+ }
+ }
+
+ // TODO: Find a way to make this access itself `history` doesn't have to be
+ // passed everytime.
+ async fn process_command(&mut self, c: Command) -> i32 {
+ let exit_code = match BuiltIn::from_str(&c.keyword) {
+ Ok(BuiltIn::Echo) => builtin_echo(&c.args),
+ Ok(BuiltIn::Exit) => std::process::exit(0),
+ Ok(BuiltIn::History) => builtin_history(&self.history),
+ Ok(BuiltIn::Null) => 0,
+ Ok(BuiltIn::Help) => builtin_help(),
+ Ok(BuiltIn::Ls) => builtin_ls(),
+ Ok(BuiltIn::Cat) => builtin_cat(&c.args).await,
+ Ok(BuiltIn::Config) => builtin_config(&c.args),
+ Ok(BuiltIn::Fetch) => builtin_fetch(),
+ _ => {
+ println!("wsh: command not found: {}", &c.keyword);
+ 1
+ }
+ };
+
+ if c.keyword != "null" {
+ self.history.push(c.to_line());
+ }
+
+ exit_code
+ }
+}
+
+#[cfg(test)]
+mod tokenize_command {
+ use crate::Prompt;
+
+ #[test]
+ #[ignore]
+ fn empty_command() { assert_eq!("", Prompt::tokenize_command("".to_string()).keyword) }
+
+ #[test]
+ fn test_keyword() { assert_eq!("test", Prompt::tokenize_command("test".to_string()).keyword) }
+
+ #[test]
+ fn no_arg() { assert_eq!(0, Prompt::tokenize_command("test".to_string()).args.len()) }
+
+ #[test]
+ fn one_arg() {
+ assert_eq!(
+ 1,
+ Prompt::tokenize_command("test one".to_string()).args.len()
+ )
+ }
+
+ #[test]
+ fn multi_arg() {
+ assert_eq!(
+ 3,
+ Prompt::tokenize_command("test one two three".to_string())
+ .args
+ .len()
+ )
+ }
+
+ #[test]
+ #[ignore]
+ fn quotes() {
+ assert_eq!(
+ 2,
+ Prompt::tokenize_command("test \"one two\" three".to_string())
+ .args
+ .len()
+ )
+ }
+}
diff --git a/crates/whirl_prompt/src/structure.rs b/crates/whirl_prompt/src/structure.rs
new file mode 100644
index 0000000..4603d37
--- /dev/null
+++ b/crates/whirl_prompt/src/structure.rs
@@ -0,0 +1,10 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub struct Command {
+ pub keyword: String,
+ pub args: Vec<String>,
+}
+impl Command {
+ pub fn to_line(&self) -> String { format!("{} {}", self.keyword, self.args.join(" ")) }
+}
diff --git a/crates/whirl_server/.license_template b/crates/whirl_server/.license_template
new file mode 100644
index 0000000..1fda769
--- /dev/null
+++ b/crates/whirl_server/.license_template
@@ -0,0 +1,2 @@
+// Copyleft (ɔ) {20\d{2}(-20\d{2})?} The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
diff --git a/crates/whirl_server/Cargo.toml b/crates/whirl_server/Cargo.toml
new file mode 100644
index 0000000..be88f2f
--- /dev/null
+++ b/crates/whirl_server/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "whirl_server"
+version = "0.1.0"
+authors = ["Fuwn <[email protected]>"]
+edition = "2018"
+description = "Whirl, an open-source WorldServer implementation in Rust."
+documentation = "https://whirlsplash.org/docs/"
+readme = "../../README.rst"
+homepage = "https://whirlsplash.org"
+repository = "https://github.com/Whirlsplash/whirl"
+license = "GPL-3.0-only"
+# license-file = "LICENSE"
+keywords = ["rust", "worldserver", "whirl", "whirlsplash"]
+publish = false
+
+[dependencies]
+# Logging
+log = "0.4.14"
+
+# Utility
+async-trait = "0.1.50"
+
+# Byte Manipulation
+bytes = "1.0.1"
+byteorder = "1.4.3"
+
+# Serialization
+serde = "1.0.126"
+serde_derive = "1.0.126"
+
+# TCP
+tokio = { version = "1.6.0", features = ["full"] }
+tokio-util = { version = "0.6.7", features = ["codec"] }
+tokio-stream = "0.1.6"
+
+# Config
+whirl_config = { path = "../whirl_config" }
diff --git a/crates/whirl_server/src/cmd/commands/action.rs b/crates/whirl_server/src/cmd/commands/action.rs
new file mode 100644
index 0000000..8d1fb0b
--- /dev/null
+++ b/crates/whirl_server/src/cmd/commands/action.rs
@@ -0,0 +1,26 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+// TODO: of2m-ify
+//
+// Of2m-ifying isn't much of a priority right now as the whole action ordeal
+// hasn't been fully dissected yet. Once more is known about the inner working
+// of actions, it will be of2m-ified.
+
+use bytes::{BufMut, BytesMut};
+
+pub fn create_action() -> Vec<u8> {
+ let mut command = BytesMut::new();
+
+ command.put_slice(&[
+ 0x01, 0x11, 0x00, 0x05, 0x54, 0x52, 0x41, 0x44, 0x45, 0x07, 0x26, 0x7c, 0x2b, 0x69, 0x6e, 0x76,
+ 0x3e,
+ ]);
+
+ // Convert to vector and insert the length
+ let mut command_as_vec = command.to_vec();
+ command_as_vec.insert(0, command.len() as u8 + 1);
+
+ // Return bytes
+ command_as_vec
+}
diff --git a/crates/whirl_server/src/cmd/commands/buddy_list.rs b/crates/whirl_server/src/cmd/commands/buddy_list.rs
new file mode 100644
index 0000000..931db52
--- /dev/null
+++ b/crates/whirl_server/src/cmd/commands/buddy_list.rs
@@ -0,0 +1,48 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::str::from_utf8;
+
+use bytes::{BufMut, BytesMut};
+
+use crate::cmd::{
+ constants::BUDDYLISTNOTIFY,
+ extendable::{Creatable, Parsable},
+};
+
+#[derive(Clone)]
+pub struct BuddyList {
+ pub buddy: String,
+ pub add: i8,
+}
+impl Parsable for BuddyList {
+ fn parse(data: Vec<u8>) -> Self {
+ Self {
+ buddy: from_utf8(&data[4..data[0] as usize - 1])
+ .unwrap()
+ .to_string(),
+
+ // Get the last byte
+ add: data[data[0] as usize - 1] as i8,
+ }
+ }
+}
+impl Creatable for BuddyList {
+ fn create(self) -> Vec<u8> {
+ let mut command = BytesMut::new();
+
+ // Header
+ command.put_u8(0x01); // ObjId
+ command.put_i8(BUDDYLISTNOTIFY as i8); // Type
+
+ // Content
+ command.put_u8(self.buddy.len() as u8); // Buddy (name) length
+ command.put_slice(self.buddy.as_bytes()); // Buddy (name)
+ command.put_u8(self.add as u8); // "Is buddy logged on?" (?)
+
+ let mut command_as_vec = command.to_vec();
+ command_as_vec.insert(0, command.len() as u8 + 1);
+
+ command_as_vec
+ }
+}
diff --git a/crates/whirl_server/src/cmd/commands/mod.rs b/crates/whirl_server/src/cmd/commands/mod.rs
new file mode 100644
index 0000000..49758c2
--- /dev/null
+++ b/crates/whirl_server/src/cmd/commands/mod.rs
@@ -0,0 +1,12 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub mod action;
+pub mod buddy_list;
+pub mod property;
+pub mod redirect_id;
+pub mod room_id_request;
+pub mod subscribe_distance;
+pub mod subscribe_room;
+pub mod teleport;
+pub mod text;
diff --git a/crates/whirl_server/src/cmd/commands/property/create.rs b/crates/whirl_server/src/cmd/commands/property/create.rs
new file mode 100644
index 0000000..40ec2be
--- /dev/null
+++ b/crates/whirl_server/src/cmd/commands/property/create.rs
@@ -0,0 +1,168 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+// TODO: of2m-ify?
+
+use whirl_config::Config;
+
+use crate::{
+ cmd::constants::{PROPUPD, SESSINIT},
+ net::{
+ constants::{
+ VAR_APPNAME,
+ VAR_CHANNEL,
+ VAR_ERROR,
+ VAR_EXTERNAL_HTTP_SERVER,
+ VAR_MAIL_DOMAIN,
+ VAR_PRIV,
+ VAR_PROTOCOL,
+ VAR_SCRIPT_SERVER,
+ VAR_SERIAL,
+ VAR_SERVERTYPE,
+ VAR_SMTP_SERVER,
+ VAR_UPDATETIME,
+ },
+ converter::property_list_to_bytes,
+ structure::NetworkProperty,
+ },
+};
+
+pub fn create_property_update_as_distributor() -> Vec<u8> {
+ property_list_to_bytes(
+ PROPUPD,
+ 0xFF,
+ vec![
+ NetworkProperty {
+ prop_id: VAR_MAIL_DOMAIN,
+ value: "worlds3d.com".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_SMTP_SERVER,
+ value: "mail.worlds.net:25".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_SCRIPT_SERVER,
+ value: "http://www-dynamic.us.worlds.net/cgi-bin".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_EXTERNAL_HTTP_SERVER,
+ value: "http://www-static.us.worlds.net".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_SERVERTYPE,
+ value: "1".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_PROTOCOL,
+ value: "24".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_APPNAME,
+ value: Config::get().whirlsplash.worldsmaster_username,
+ },
+ ],
+ )
+}
+
+pub fn create_property_update_as_hub() -> Vec<u8> {
+ property_list_to_bytes(
+ PROPUPD,
+ 0xFF,
+ vec![
+ NetworkProperty {
+ prop_id: VAR_UPDATETIME,
+ value: "1000000".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_MAIL_DOMAIN,
+ value: "worlds3d.com".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_SMTP_SERVER,
+ value: "mail.worlds.net:25".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_SCRIPT_SERVER,
+ value: "http://www-dynamic.us.worlds.net/cgi-bin".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_EXTERNAL_HTTP_SERVER,
+ value: "http://www-static.us.worlds.net".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_SERVERTYPE,
+ value: "3".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_PROTOCOL,
+ value: "24".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_APPNAME,
+ value: Config::get().whirlsplash.worldsmaster_username,
+ },
+ ],
+ )
+}
+
+pub fn create_property_request_as_distributor() -> Vec<u8> {
+ property_list_to_bytes(
+ SESSINIT as i32,
+ 0x01,
+ vec![
+ NetworkProperty {
+ prop_id: VAR_ERROR,
+ value: "0".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_APPNAME,
+ value: Config::get().whirlsplash.worldsmaster_username,
+ },
+ NetworkProperty {
+ prop_id: VAR_PROTOCOL,
+ value: "24".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_SERVERTYPE,
+ value: "1".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_SERIAL,
+ value: "DWLV000000000000".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_PRIV,
+ value: "0".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_CHANNEL,
+ value: "dimension-1".to_string(),
+ },
+ ],
+ )
+}
+
+pub fn create_property_request_as_hub() -> Vec<u8> {
+ property_list_to_bytes(
+ SESSINIT as i32,
+ 0x01,
+ vec![
+ NetworkProperty {
+ prop_id: VAR_ERROR,
+ value: "0".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_SERVERTYPE,
+ value: "3".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_UPDATETIME,
+ value: "1000000".to_string(),
+ },
+ NetworkProperty {
+ prop_id: VAR_PROTOCOL,
+ value: "24".to_string(),
+ },
+ ],
+ )
+}
diff --git a/crates/whirl_server/src/cmd/commands/property/mod.rs b/crates/whirl_server/src/cmd/commands/property/mod.rs
new file mode 100644
index 0000000..83b015b
--- /dev/null
+++ b/crates/whirl_server/src/cmd/commands/property/mod.rs
@@ -0,0 +1,5 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub mod create;
+pub mod parse;
diff --git a/crates/whirl_server/src/cmd/commands/property/parse.rs b/crates/whirl_server/src/cmd/commands/property/parse.rs
new file mode 100644
index 0000000..415d19f
--- /dev/null
+++ b/crates/whirl_server/src/cmd/commands/property/parse.rs
@@ -0,0 +1,14 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::net::structure::NetworkProperty;
+
+pub fn find_property_in_property_list(
+ property_list: &[NetworkProperty],
+ property: i32,
+) -> &NetworkProperty {
+ property_list
+ .iter()
+ .find(|i| i.prop_id == property)
+ .unwrap()
+}
diff --git a/crates/whirl_server/src/cmd/commands/redirect_id.rs b/crates/whirl_server/src/cmd/commands/redirect_id.rs
new file mode 100644
index 0000000..8f56c86
--- /dev/null
+++ b/crates/whirl_server/src/cmd/commands/redirect_id.rs
@@ -0,0 +1,42 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use bytes::{BufMut, BytesMut};
+use whirl_config::Config;
+
+use crate::cmd::{constants::REDIRID, extendable::Creatable};
+
+#[derive(Debug)]
+pub struct RedirectId {
+ pub room_name: String,
+ pub room_number: i8,
+}
+impl Creatable for RedirectId {
+ fn create(self) -> Vec<u8> {
+ let mut command = BytesMut::new();
+
+ // Header
+ command.put_u8(0x01); // ObjId
+ command.put_u8(REDIRID as u8); // Type
+
+ // Content
+ command.put_u8(self.room_name.len() as u8); // Room name length
+ command.put_slice(self.room_name.as_bytes()); // Room name
+ // command.put_u8(0x00); // Unimplemented byte (?)
+ // command.put_u8(room_id); // Room ID
+ command.put_u16(self.room_number as u16); // Room ID
+
+ // IP
+ for byte in Config::get().whirlsplash.ip.split('.') {
+ command.put_u8(byte.parse::<u8>().unwrap());
+ }
+ command.put_u16(Config::get().hub.port as u16); // Port
+
+ // Length
+ let mut command_as_vec = command.to_vec();
+ command_as_vec.insert(0, command.len() as u8 + 1);
+
+ // Return
+ command_as_vec
+ }
+}
diff --git a/crates/whirl_server/src/cmd/commands/room_id_request.rs b/crates/whirl_server/src/cmd/commands/room_id_request.rs
new file mode 100644
index 0000000..cf507fa
--- /dev/null
+++ b/crates/whirl_server/src/cmd/commands/room_id_request.rs
@@ -0,0 +1,18 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::str::from_utf8;
+
+use crate::cmd::extendable::Parsable;
+
+#[derive(Debug)]
+pub struct RoomIdRequest {
+ pub room_name: String,
+}
+impl Parsable for RoomIdRequest {
+ fn parse(data: Vec<u8>) -> Self {
+ Self {
+ room_name: from_utf8(&data[4..data[0] as usize]).unwrap().to_string(),
+ }
+ }
+}
diff --git a/crates/whirl_server/src/cmd/commands/subscribe_distance.rs b/crates/whirl_server/src/cmd/commands/subscribe_distance.rs
new file mode 100644
index 0000000..d5cbcf6
--- /dev/null
+++ b/crates/whirl_server/src/cmd/commands/subscribe_distance.rs
@@ -0,0 +1,24 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use byteorder::{BigEndian, ReadBytesExt};
+use bytes::{Buf, BytesMut};
+
+use crate::cmd::extendable::Parsable;
+
+#[derive(Debug)]
+pub struct SubscribeDistance {
+ pub distance: i16,
+ pub room_number: i16,
+}
+impl Parsable for SubscribeDistance {
+ fn parse(data: Vec<u8>) -> Self {
+ // https://stackoverflow.com/questions/41034635/how-do-i-convert-between-string-str-vecu8-and-u8
+ let mut data = BytesMut::from(data.as_slice()).reader();
+
+ Self {
+ distance: data.read_i16::<BigEndian>().unwrap(),
+ room_number: data.read_i16::<BigEndian>().unwrap(),
+ }
+ }
+}
diff --git a/crates/whirl_server/src/cmd/commands/subscribe_room.rs b/crates/whirl_server/src/cmd/commands/subscribe_room.rs
new file mode 100644
index 0000000..9e7d732
--- /dev/null
+++ b/crates/whirl_server/src/cmd/commands/subscribe_room.rs
@@ -0,0 +1,30 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use byteorder::{BigEndian, ReadBytesExt};
+use bytes::{Buf, BytesMut};
+
+use crate::cmd::extendable::Parsable;
+
+#[derive(Debug)]
+pub struct SubscribeRoom {
+ pub room_number: i8,
+ pub x: f32,
+ pub y: f32,
+ pub z: f32,
+ pub distance: f32,
+}
+impl Parsable for SubscribeRoom {
+ fn parse(data: Vec<u8>) -> Self {
+ // https://stackoverflow.com/questions/41034635/how-do-i-convert-between-string-str-vecu8-and-u8
+ let mut data = BytesMut::from(data.as_slice()).reader();
+
+ Self {
+ room_number: data.read_i16::<BigEndian>().unwrap() as i8,
+ x: data.read_i16::<BigEndian>().unwrap() as i8 as f32,
+ y: data.read_i16::<BigEndian>().unwrap() as i8 as f32,
+ z: data.read_i16::<BigEndian>().unwrap() as i8 as f32,
+ distance: data.read_i16::<BigEndian>().unwrap() as i8 as f32, // + 100
+ }
+ }
+}
diff --git a/crates/whirl_server/src/cmd/commands/teleport.rs b/crates/whirl_server/src/cmd/commands/teleport.rs
new file mode 100644
index 0000000..ef8f6b2
--- /dev/null
+++ b/crates/whirl_server/src/cmd/commands/teleport.rs
@@ -0,0 +1,34 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use byteorder::{BigEndian, ReadBytesExt};
+use bytes::{Buf, BytesMut};
+
+use crate::cmd::extendable::Parsable;
+
+#[derive(Debug)]
+pub struct Teleport {
+ pub room_id: i8,
+ pub exit_type: u8,
+ pub entry_type: u8,
+ pub x: f32, // i16
+ pub y: f32,
+ pub z: f32,
+ pub direction: f32,
+}
+impl Parsable for Teleport {
+ fn parse(data: Vec<u8>) -> Self {
+ // https://stackoverflow.com/questions/41034635/how-do-i-convert-between-string-str-vecu8-and-u8
+ let mut data = BytesMut::from(data.as_slice()).reader();
+
+ Self {
+ room_id: data.read_u16::<BigEndian>().unwrap() as i8,
+ exit_type: data.read_u8().unwrap(),
+ entry_type: data.read_u8().unwrap(),
+ x: data.read_i16::<BigEndian>().unwrap() as f32,
+ y: data.read_i16::<BigEndian>().unwrap() as f32,
+ z: data.read_i16::<BigEndian>().unwrap() as f32,
+ direction: data.read_i16::<BigEndian>().unwrap() as f32,
+ }
+ }
+}
diff --git a/crates/whirl_server/src/cmd/commands/text.rs b/crates/whirl_server/src/cmd/commands/text.rs
new file mode 100644
index 0000000..2bf7e17
--- /dev/null
+++ b/crates/whirl_server/src/cmd/commands/text.rs
@@ -0,0 +1,67 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::str::from_utf8;
+
+use bytes::{BufMut, BytesMut};
+
+use crate::cmd::{
+ constants::TEXT,
+ extendable::{Creatable, ParsableWithArguments},
+};
+
+pub struct Text {
+ pub sender: String,
+ pub content: String,
+}
+impl Creatable for Text {
+ fn create(self) -> Vec<u8> {
+ let mut command = BytesMut::new();
+
+ // Header
+ command.put_u8(0x01);
+ command.put_i8(TEXT as i8);
+
+ // Content
+ // TODO: Find a way to parse ObjIds.
+ //
+ // The below byte is suspected to be the sender's short ObjId.
+ command.put_i8(0x00);
+
+ command.put_u8(self.sender.len() as u8);
+ command.put_slice(self.sender.as_bytes());
+ command.put_u8(self.content.len() as u8);
+ command.put_slice(self.content.as_bytes());
+
+ // Convert to vector and insert the length
+ let mut command_as_vec = command.to_vec();
+ command_as_vec.insert(0, command.len() as u8 + 1);
+
+ // Return bytes
+ command_as_vec
+ }
+}
+impl ParsableWithArguments for Text {
+ /// The first and only element of `args` *should* be the username of the
+ /// sender.
+ ///
+ /// There isn't anything currently stopping someone from passing some other
+ /// value so that might be annoying at times.
+ ///
+ /// Realistically, this method is mostly static so the username will *always*
+ /// be passed properly unless someone intentionally commits breaking changes
+ /// on purpose regarding what is passed to to this method where called.
+ ///
+ /// It would be neat to have some sort of ability to statically check if the
+ /// `args` argument contains x number of elements at compile time or
+ /// something of the sort but the Rust RFC is probably not focused on that.
+ ///
+ /// So, right now, trust is in the developers' hands to make sure to pass the
+ /// right -- number -- of elements to `args`.
+ fn parse(data: Vec<u8>, args: &[&str]) -> Self {
+ Self {
+ sender: args[0].to_string(),
+ content: from_utf8(&data[6..]).unwrap().to_string(),
+ }
+ }
+}
diff --git a/crates/whirl_server/src/cmd/constants.rs b/crates/whirl_server/src/cmd/constants.rs
new file mode 100644
index 0000000..22d29c1
--- /dev/null
+++ b/crates/whirl_server/src/cmd/constants.rs
@@ -0,0 +1,32 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub const LONGLOC: i32 = 1;
+pub const STATE: i32 = 2;
+pub const PROP: i32 = 3;
+pub const SHORTLOC: i32 = 4;
+pub const ROOMCHNG: i32 = 5;
+pub const SESSINIT: i32 = 6;
+pub const SESSEXIT: i32 = 7;
+pub const APPINIT: i32 = 8;
+pub const PROPREQ: i32 = 10;
+pub const DISAPPR: i32 = 11;
+pub const APPRACTR: i32 = 12;
+pub const REGOBJID: i32 = 13;
+pub const TEXT: i32 = 14;
+pub const PROPSET: i32 = 15;
+pub const PROPUPD: i32 = 16;
+pub const WHISPER: i32 = 17;
+pub const TELEPORT: i32 = 18;
+pub const ROOMIDRQ: i32 = 20;
+pub const ROOMID: i32 = 21;
+pub const SUBSCRIB: i32 = 22;
+pub const UNSUBSCR: i32 = 23;
+pub const SUB_DIST: i32 = 24; // SUB-DIST
+pub const REDIRECT: i32 = 25;
+pub const REDIRID: i32 = 26;
+pub const FINGREQ: i32 = 27;
+pub const FINGREP: i32 = 28;
+pub const BUDDYLISTUPDATE: i32 = 29;
+pub const BUDDYLISTNOTIFY: i32 = 30;
+pub const CHANNEL: i32 = 31;
diff --git a/crates/whirl_server/src/cmd/extendable.rs b/crates/whirl_server/src/cmd/extendable.rs
new file mode 100644
index 0000000..e6f3c2b
--- /dev/null
+++ b/crates/whirl_server/src/cmd/extendable.rs
@@ -0,0 +1,18 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub trait Parsable {
+ fn parse(data: Vec<u8>) -> Self;
+}
+
+pub trait Creatable {
+ fn create(self) -> Vec<u8>;
+}
+
+/// Having to do this makes me with there was operator overloading in Rust.
+///
+/// I *could* do this with a macro but since Text is the only struct that
+/// implements this trait, it shouldn't be that big of a deal.
+pub trait ParsableWithArguments {
+ fn parse(data: Vec<u8>, args: &[&str]) -> Self;
+}
diff --git a/crates/whirl_server/src/cmd/mod.rs b/crates/whirl_server/src/cmd/mod.rs
new file mode 100644
index 0000000..ef91de7
--- /dev/null
+++ b/crates/whirl_server/src/cmd/mod.rs
@@ -0,0 +1,9 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub mod commands;
+
+pub mod constants;
+pub mod extendable;
+mod set_parser;
+mod structure;
diff --git a/crates/whirl_server/src/cmd/set_parser.rs b/crates/whirl_server/src/cmd/set_parser.rs
new file mode 100644
index 0000000..9d6a4b3
--- /dev/null
+++ b/crates/whirl_server/src/cmd/set_parser.rs
@@ -0,0 +1,39 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use crate::cmd::structure::Command;
+
+/// Iterate over a command set in the from of bytes and return a list of
+/// human-readable commands.
+fn _parse_command_set(mut data: Vec<u8>) -> Vec<Command> {
+ let mut command_set = vec![];
+
+ // Iterate over all commands
+ loop {
+ // Check if any commands are present
+ if data.len() <= 2 {
+ break;
+ }
+ if data[0] == 0 {
+ break;
+ }
+
+ let command_length = data[0];
+ let mut command = Command {
+ length: command_length as i32,
+ obj_id: data[1] as i32,
+ id: data[2] as i32,
+ body: vec![],
+ };
+ if command.length > 3 {
+ command.body = data[3..].to_owned();
+ }
+ command_set.push(command);
+
+ // Remove current command from the command set
+ data = data[command_length as usize..].to_vec();
+ }
+
+ // Return the human-readable command set
+ command_set
+}
diff --git a/crates/whirl_server/src/cmd/structure.rs b/crates/whirl_server/src/cmd/structure.rs
new file mode 100644
index 0000000..23e91ca
--- /dev/null
+++ b/crates/whirl_server/src/cmd/structure.rs
@@ -0,0 +1,22 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub struct Command {
+ pub length: i32,
+ pub obj_id: i32,
+ pub id: i32,
+ pub body: Vec<u8>,
+}
+impl Command {
+ pub fn _new() -> Self { Command::default() }
+}
+impl Default for Command {
+ fn default() -> Self {
+ Command {
+ length: 0,
+ obj_id: 0,
+ id: 0,
+ body: vec![],
+ }
+ }
+}
diff --git a/crates/whirl_server/src/distributor.rs b/crates/whirl_server/src/distributor.rs
new file mode 100644
index 0000000..22b698b
--- /dev/null
+++ b/crates/whirl_server/src/distributor.rs
@@ -0,0 +1,148 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+//! The distributor functions as bare-minimal
+//! [AutoServer](http://dev.worlds.net/private/GammaDocs/WorldServer.html#AutoServer).
+//!
+//! It intercepts a client and distributes it to a
+//! [RoomServer](http://dev.worlds.net/private/GammaDocs/WorldServer.html#RoomServer).
+//!
+//! This is not meant to be a high performant section of code as the distributor
+//! is only meant to handle the initial and brief session initialization of the
+//! client.
+
+use std::{error::Error, net::SocketAddr, sync::Arc};
+
+use tokio::{io::AsyncWriteExt, net::TcpStream, sync::Mutex};
+use tokio_stream::StreamExt;
+use tokio_util::codec::{BytesCodec, Decoder};
+use whirl_config::Config;
+
+use crate::{
+ cmd::{
+ commands::{
+ action::create_action,
+ buddy_list::BuddyList,
+ property::{
+ create::{create_property_request_as_distributor, create_property_update_as_distributor},
+ parse::find_property_in_property_list,
+ },
+ redirect_id::RedirectId,
+ room_id_request::RoomIdRequest,
+ text::Text,
+ },
+ constants::*,
+ extendable::{Creatable, Parsable},
+ },
+ interaction::{peer::Peer, shared::Shared},
+ net::{constants::VAR_USERNAME, property_parser::parse_network_property},
+ packet_parser::parse_commands_from_packet,
+ Server,
+};
+
+pub struct Distributor;
+#[async_trait]
+impl Server for Distributor {
+ async fn handle(
+ state: Arc<Mutex<Shared>>,
+ stream: TcpStream,
+ _address: SocketAddr,
+ count: usize,
+ ) -> Result<(), Box<dyn Error>> {
+ let bytes = BytesCodec::new().framed(stream);
+ let mut peer = Peer::new(state.clone(), bytes, count.to_string()).await?;
+ let mut room_ids = vec![];
+ let mut username = String::from("unknown");
+
+ loop {
+ tokio::select! {
+ Some(msg) = peer.rx.recv() => {
+ peer.bytes.get_mut().write_all(&msg).await?;
+ }
+ result = peer.bytes.next() => match result {
+ Some(Ok(msg)) => {
+ for msg in parse_commands_from_packet(msg) {
+ match msg.get(2).unwrap().to_owned() as i32 {
+ PROPREQ => {
+ debug!("received property request from client");
+
+ peer.bytes.get_mut()
+ .write_all(&create_property_update_as_distributor()).await?;
+ trace!("sent property update to client");
+ }
+ SESSINIT => {
+ username = (&*find_property_in_property_list(
+ &parse_network_property(msg[3..].to_vec()),
+ VAR_USERNAME,
+ ).value).to_string();
+
+ debug!("received session initialization from {}", username);
+
+ peer.bytes.get_mut()
+ .write_all(&create_property_request_as_distributor()).await?;
+ trace!("sent property request to {}", username);
+ }
+ PROPSET => {
+ debug!("received property set from {}", username);
+
+ peer.bytes.get_mut()
+ .write_all(&Text {
+ sender: Config::get().whirlsplash.worldsmaster_username,
+ content: Config::get().distributor.worldsmaster_greeting,
+ }.create()).await?;
+ peer.bytes.get_mut()
+ .write_all(&create_action()).await?;
+ trace!("sent text to {}", username);
+ }
+ BUDDYLISTUPDATE => {
+ let buddy = BuddyList::parse(msg.to_vec());
+ debug!("received buddy list update from {}: {}", username, buddy.buddy);
+ peer.bytes.get_mut().write_all(&buddy.clone().create()).await?;
+ trace!("sent buddy list notify to {}: {}", username, buddy.buddy);
+ }
+ ROOMIDRQ => {
+ let room = RoomIdRequest::parse(msg.to_vec());
+ debug!("received room id request from {}: {}", username, &room.room_name);
+
+ let room_id;
+ if !room_ids.contains(&room.room_name) {
+ room_ids.push((&*room.room_name).to_string());
+ room_id = room_ids.iter().position(|r| r == &room.room_name).unwrap();
+ trace!("inserted room: {}", room.room_name);
+ } else {
+ let position = room_ids.iter().position(|r| r == &room.room_name).unwrap();
+ trace!("found room: {}", room.room_name);
+ room_id = position;
+ }
+
+ peer.bytes.get_mut().write_all(&RedirectId {
+ room_name: (&*room.room_name).to_string(),
+ room_number: room_id as i8,
+ }.create()).await?;
+ trace!("sent redirect id to {}: {}", username, room.room_name);
+ }
+ SESSEXIT => {
+ debug!("received session exit from {}", username); break;
+ }
+ _ => (),
+ }
+ }
+ }
+ Some(Err(e)) => {
+ error!("error while processing message (s): {}", e); break;
+ }
+ None => break,
+ }
+ }
+ }
+
+ // Deregister client
+ debug!("de-registering client");
+ {
+ state.lock().await.peers.remove(&count.to_string());
+ }
+ debug!("de-registered client");
+
+ Ok(())
+ }
+}
diff --git a/crates/whirl_server/src/hub.rs b/crates/whirl_server/src/hub.rs
new file mode 100644
index 0000000..6de4bca
--- /dev/null
+++ b/crates/whirl_server/src/hub.rs
@@ -0,0 +1,161 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+//! The hub functions as a
+//! [RoomServer](http://dev.worlds.net/private/GammaDocs/WorldServer.html#AutoServer).
+//!
+//! The RoomServer is responsible for handling just about every request from the
+//! client after they have been redirected to a room (hub).
+
+use std::{error::Error, net::SocketAddr, sync::Arc};
+
+use tokio::{io::AsyncWriteExt, net::TcpStream, sync::Mutex};
+use tokio_stream::StreamExt;
+use tokio_util::codec::{BytesCodec, Decoder};
+use whirl_config::Config;
+
+use crate::{
+ cmd::{
+ commands::{
+ action::create_action,
+ buddy_list::BuddyList,
+ property::{
+ create::{create_property_request_as_hub, create_property_update_as_hub},
+ parse::find_property_in_property_list,
+ },
+ subscribe_distance::SubscribeDistance,
+ subscribe_room::SubscribeRoom,
+ teleport::Teleport,
+ text::Text,
+ },
+ constants::*,
+ extendable::{Creatable, Parsable, ParsableWithArguments},
+ },
+ interaction::{peer::Peer, shared::Shared},
+ net::{constants::VAR_USERNAME, property_parser::parse_network_property},
+ packet_parser::parse_commands_from_packet,
+ Server,
+};
+
+pub struct Hub;
+#[async_trait]
+impl Server for Hub {
+ async fn handle(
+ state: Arc<Mutex<Shared>>,
+ stream: TcpStream,
+ _address: SocketAddr,
+ count: usize,
+ ) -> Result<(), Box<dyn Error>> {
+ let bytes = BytesCodec::new().framed(stream);
+ let mut peer = Peer::new(state.clone(), bytes, count.to_string()).await?;
+ // let mut room_ids = vec![];
+ let mut username = String::from("unknown");
+
+ loop {
+ tokio::select! {
+ Some(msg) = peer.rx.recv() => {
+ // trace!("got peer activity: {:?}", &msg);
+ peer.bytes.get_mut().write_all(&msg).await?;
+ }
+ result = peer.bytes.next() => match result {
+ Some(Ok(msg)) => {
+ // trace!("got some bytes: {:?}", &msg);
+ for msg in parse_commands_from_packet(msg) {
+ match msg.get(2).unwrap().to_owned() as i32 {
+ PROPREQ => {
+ debug!("received property request from client");
+
+ peer.bytes.get_mut()
+ .write_all(&create_property_update_as_hub()).await?;
+ trace!("sent property update to client");
+ }
+ SESSINIT => {
+ username = (&*find_property_in_property_list(
+ &parse_network_property(msg[3..].to_vec()),
+ VAR_USERNAME,
+ ).value).to_string();
+
+ debug!("received session initialization from {}", username);
+
+ peer.bytes.get_mut()
+ .write_all(&create_property_request_as_hub()).await?;
+ trace!("sent property request to {}", username);
+ }
+ PROPSET => {
+ debug!("received property set from {}", username);
+
+ peer.bytes.get_mut()
+ .write_all(&Text {
+ sender: Config::get().whirlsplash.worldsmaster_username,
+ content: Config::get().distributor.worldsmaster_greeting,
+ }.create()).await?;
+ peer.bytes.get_mut()
+ .write_all(&create_action()).await?;
+ trace!("sent text to {}", username);
+ }
+ BUDDYLISTUPDATE => {
+ let buddy = BuddyList::parse(msg.to_vec());
+ debug!("received buddy list update from {}: {}", username, buddy.buddy);
+ peer.bytes.get_mut().write_all(&buddy.clone().create()).await?;
+ trace!("sent buddy list notify to {}: {}", username, buddy.buddy);
+ }
+ // TODO: Figure out if this is actually even needed.
+ // ROOMIDRQ => {
+ // let room = RoomIdRequest::parse(msg.to_vec());
+ // debug!("received room id request from {}: {}", username, room.room_name);
+ // trace!("{:?}", create_room_id_request(&room.room_name, 0x00));
+ // }
+ SESSEXIT => {
+ debug!("received session exit from {}", username); break;
+ }
+ TEXT => {
+ let text = Text::parse(msg.to_vec(), &[&username]);
+ debug!("received text from {}:{}", username, text.content);
+
+ {
+ state.lock().await.broadcast(&Text {
+ sender: (&*username).to_string(),
+ content: text.content,
+ }.create()).await;
+ }
+ debug!("broadcasted text to hub");
+ }
+ SUBSCRIB => {
+ let subscribe_room = SubscribeRoom::parse(msg[3..].to_vec());
+ debug!("received subscribe room from {}: {:?}",
+ username, subscribe_room);
+ }
+ SUB_DIST => {
+ let subscribe_distance = SubscribeDistance::parse(msg[3..].to_vec());
+ debug!("received subscribe distance from {}: {:?}",
+ username, subscribe_distance);
+ }
+ TELEPORT => {
+ let teleport = Teleport::parse(msg[3..].to_vec());
+ debug!("received teleport from {}: {:?}",
+ username, teleport);
+ }
+ _ => (),
+ }
+ }
+ }
+ Some(Err(e)) => {
+ error!("error while processing message (s): {}", e); break;
+ }
+ None => {
+ trace!("nothing"); break;
+ },
+ }
+ }
+ }
+
+ // Deregister client
+ debug!("de-registering client");
+ {
+ state.lock().await.peers.remove(&count.to_string());
+ }
+ debug!("de-registered client");
+
+ Ok(())
+ }
+}
diff --git a/crates/whirl_server/src/interaction/mod.rs b/crates/whirl_server/src/interaction/mod.rs
new file mode 100644
index 0000000..c85e09d
--- /dev/null
+++ b/crates/whirl_server/src/interaction/mod.rs
@@ -0,0 +1,5 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub mod peer;
+pub mod shared;
diff --git a/crates/whirl_server/src/interaction/peer.rs b/crates/whirl_server/src/interaction/peer.rs
new file mode 100644
index 0000000..38c02c5
--- /dev/null
+++ b/crates/whirl_server/src/interaction/peer.rs
@@ -0,0 +1,49 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::sync::Arc;
+
+use tokio::{
+ net::TcpStream,
+ sync::{mpsc, Mutex},
+};
+use tokio_util::codec::{BytesCodec, Framed};
+
+use crate::{interaction::shared::Shared, types::Rx};
+
+pub struct Peer {
+ pub bytes: Framed<TcpStream, BytesCodec>,
+ pub rx: Rx,
+}
+impl Peer {
+ pub async fn new(
+ state: Arc<Mutex<Shared>>,
+ bytes: Framed<TcpStream, BytesCodec>,
+ username: String,
+ ) -> std::io::Result<Peer> {
+ let (tx, rx) = mpsc::unbounded_channel();
+ state.lock().await.peers.insert(username, tx);
+
+ Ok(Peer {
+ bytes,
+ rx,
+ })
+ }
+
+ pub async fn _change_username(
+ self,
+ state: Arc<Mutex<Shared>>,
+ username: &str,
+ new_username: &str,
+ ) {
+ // Remove peer from peers
+ {
+ state.lock().await.peers.remove(username);
+ }
+
+ // Add the peer back with the new username
+ Self::new(state, self.bytes, new_username.to_string())
+ .await
+ .unwrap();
+ }
+}
diff --git a/crates/whirl_server/src/interaction/shared.rs b/crates/whirl_server/src/interaction/shared.rs
new file mode 100644
index 0000000..c2ee671
--- /dev/null
+++ b/crates/whirl_server/src/interaction/shared.rs
@@ -0,0 +1,28 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::collections::HashMap;
+
+use bytes::BytesMut;
+
+use crate::types::Tx;
+
+pub struct Shared {
+ pub peers: HashMap<String, Tx>,
+}
+impl Shared {
+ pub fn new() -> Self {
+ Shared {
+ peers: HashMap::new(),
+ }
+ }
+
+ pub async fn broadcast(&mut self, message: &[u8]) {
+ for peer in self.peers.iter_mut() {
+ peer.1.send(BytesMut::from(message)).unwrap();
+ }
+ }
+}
+impl Default for Shared {
+ fn default() -> Self { Self::new() }
+}
diff --git a/crates/whirl_server/src/lib.rs b/crates/whirl_server/src/lib.rs
new file mode 100644
index 0000000..9c3e9d0
--- /dev/null
+++ b/crates/whirl_server/src/lib.rs
@@ -0,0 +1,90 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+#![feature(
+ type_ascription,
+ hash_set_entry,
+ type_name_of_val,
+ decl_macro,
+ proc_macro_hygiene
+)]
+#![warn(rust_2018_idioms)]
+#![recursion_limit = "128"]
+
+#[macro_use]
+extern crate log;
+#[macro_use]
+extern crate async_trait;
+
+pub mod cmd;
+mod interaction;
+pub mod net;
+
+pub mod distributor;
+pub mod hub;
+mod packet_parser;
+mod types;
+
+use std::{error::Error, fmt, net::SocketAddr, sync::Arc};
+
+use tokio::{
+ net::{TcpListener, TcpStream},
+ sync::Mutex,
+};
+
+use crate::interaction::shared::Shared;
+
+#[derive(Debug)]
+pub enum ServerType {
+ AnonRoomServer,
+ AnonUserServer,
+ AutoServer,
+ RoomServer,
+ UserServer,
+}
+// https://stackoverflow.com/a/32712140/14452787
+impl fmt::Display for ServerType {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self) }
+}
+
+#[async_trait]
+pub trait Server {
+ async fn listen(address: &str, server_type: ServerType) -> Result<(), Box<dyn Error>> {
+ let listener = TcpListener::bind(address).await?;
+ let state = Arc::new(Mutex::new(Shared::new()));
+ let mut counter = 0;
+
+ info!(
+ "server of type {} now listening at {}",
+ server_type.to_string(),
+ address
+ );
+
+ loop {
+ let (stream, address) = listener.accept().await?;
+ counter += 1;
+ let state = Arc::clone(&state);
+
+ debug!("accepted client at {}", address);
+
+ tokio::spawn(async move {
+ if let Err(e) = Self::handle(state, stream, address, counter).await {
+ error!("an error occurred: {}", e);
+ }
+
+ if std::env::var("EXIT_ON_CLIENT_DISCONNECT").unwrap_or_else(|_| "false".to_string())
+ == "true"
+ {
+ std::process::exit(0);
+ }
+ });
+ }
+ }
+
+ async fn handle(
+ state: Arc<Mutex<Shared>>,
+ stream: TcpStream,
+ _address: SocketAddr,
+ count: usize,
+ ) -> Result<(), Box<dyn Error>>;
+}
diff --git a/crates/whirl_server/src/net/constants.rs b/crates/whirl_server/src/net/constants.rs
new file mode 100644
index 0000000..169b461
--- /dev/null
+++ b/crates/whirl_server/src/net/constants.rs
@@ -0,0 +1,93 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub const VAR_PROTOCOL_VERSION: i32 = 24;
+pub const STATECMD: i32 = 2;
+pub const MAXCMD: i32 = 255;
+pub const CURRENT_ROOM: i32 = 253;
+pub const CLIENT: i32 = 1;
+pub const CO: i32 = 254;
+pub const PO: i32 = 255;
+pub const VAR_APPNAME: i32 = 1;
+pub const VAR_USERNAME: i32 = 2;
+pub const VAR_PROTOCOL: i32 = 3;
+pub const VAR_ERROR: i32 = 4;
+pub const VAR_CHANNEL: i32 = 5;
+pub const VAR_BITMAP: i32 = 5;
+pub const VAR_PASSWORD: i32 = 6;
+pub const VAR_AVATARS: i32 = 7;
+pub const VAR_UPDATETIME: i32 = 8;
+pub const VAR_CLIENT: i32 = 9;
+pub const VAR_SERIAL: i32 = 10;
+pub const VAR_EMAIL: i32 = 11;
+pub const VAR_LOGONOFF: i32 = 12;
+pub const VAR_DURATION: i32 = 13;
+pub const VAR_GUEST: i32 = 14;
+pub const VAR_SERVERTYPE: i32 = 15;
+pub const VAR_VIZCARD: i32 = 16;
+pub const VAR_NEW_PASSWD: i32 = 20;
+pub const VAR_PRIV: i32 = 22;
+pub const VAR_ASLEEP: i32 = 23;
+pub const VAR_EXTERNAL_HTTP_SERVER: i32 = 24;
+pub const VAR_SCRIPT_SERVER: i32 = 25;
+pub const VAR_SMTP_SERVER: i32 = 26;
+pub const VAR_MAIL_DOMAIN: i32 = 27;
+pub const VAR_NEW_USERNAME: i32 = 28;
+pub const VAR_INTERNAL_HTTP_SERVER: i32 = 29;
+pub const VAR_INVENTORY: i32 = 32;
+pub const ACK: i32 = 0;
+pub const NAK_BAD_USER: i32 = 1;
+pub const NAK_MAX_ORDINARY: i32 = 2;
+pub const NAK_MAX_PRIORITY: i32 = 3;
+pub const NAL_BAD_WORLD: i32 = 4;
+pub const NAK_FATAIL: i32 = 5;
+pub const NAK_BAD_PROTOCOL: i32 = 6;
+pub const NAK_BAD_CLIENTSW: i32 = 7;
+pub const NAK_BAD_ROOM: i32 = 8;
+pub const NAK_BAD_SERIAL: i32 = 9;
+pub const NAK_TAKEN_SERIAL: i32 = 10;
+pub const NAK_TAKEN_USER: i32 = 11;
+pub const NAK_NO_SUCH_USER: i32 = 12;
+pub const NAK_BAD_PASSWORD: i32 = 13;
+pub const NAK_BAD_ACCOUNT: i32 = 14;
+pub const NAK_NOT_LOGGEDON: i32 = 15;
+pub const NAK_BAD_IPADDRESS: i32 = 16;
+pub const NAK_LOGGEDON: i32 = 17;
+pub const NAK_CRYPT_METHOD: i32 = 18;
+pub const NAK_CRYPT_ERROR: i32 = 19;
+pub const NAK_SESSIONINIT: i32 = 20;
+pub const NAK_ROOM_FULL: i32 = 21;
+pub const NAK_SHUTDOWN: i32 = 100;
+pub const NAK_WRITE_ERROR: i32 = 101;
+pub const NAK_READ_ERROR: i32 = 102;
+pub const NAK_UNEXPECTED: i32 = 103;
+pub const NAK_CONNECTION: i32 = 104;
+pub const NAK_IOSTREAMS: i32 = 105;
+pub const NAK_TIMEOUT: i32 = 106;
+pub const NAK_UNREACHABLE: i32 = 107;
+pub const STATUS_CONNECTED: i32 = 200;
+pub const STATUS_DETACHING: i32 = 201;
+pub const STATUS_WILLRETRY: i32 = 202;
+pub const STATUS_DISCONNECTED: i32 = 203;
+pub const STATUS_DEAD: i32 = 204;
+pub const STATUS_OFFLINE: i32 = 205;
+pub const STATUS_GALAXY_ONLINE: i32 = 206;
+pub const STATUS_GALAXY_OFFLINE: i32 = 206;
+pub const PROPFLAG_BINARY: i32 = 16;
+pub const PROPFLAG_FINGER: i32 = 32;
+pub const PROPFLAG_AUTOUPDATE: i32 = 64;
+pub const PROPFLAG_DBSTORE: i32 = 128;
+pub const PROPACCESS_POSSESS: i32 = 1;
+pub const PROPACCESS_PRIVATE: i32 = 2;
+pub const SERVER_UNKNOWN: i32 = 0;
+pub const USER_SERVER_DB: i32 = 1;
+pub const USER_SERVER_ANON: i32 = 2;
+pub const ROOM_SERVER_US: i32 = 3;
+pub const ROOM_SERVER_ANON: i32 = 4;
+pub const PRIV_NONE: i32 = 0;
+pub const PRIV_BUILD: i32 = 1;
+pub const PRIV_BROADCAST: i32 = 2;
+pub const PRIV_PROPERTY: i32 = 4;
+pub const PRIV_VIP: i32 = 8;
+pub const PRIV_VIP2: i32 = 16;
+pub const PRIV_SPECIALGUEST: i32 = 64;
diff --git a/crates/whirl_server/src/net/converter.rs b/crates/whirl_server/src/net/converter.rs
new file mode 100644
index 0000000..c976dff
--- /dev/null
+++ b/crates/whirl_server/src/net/converter.rs
@@ -0,0 +1,57 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use bytes::{BufMut, BytesMut};
+
+use crate::{
+ cmd::constants::PROPUPD,
+ net::{
+ constants::{PROPACCESS_POSSESS, PROPFLAG_DBSTORE},
+ structure::NetworkProperty,
+ },
+};
+
+pub fn property_list_to_bytes(
+ command_id: i32,
+ obj_id: i32,
+ mut property_list: Vec<NetworkProperty>,
+) -> Vec<u8> {
+ let mut command = BytesMut::new();
+
+ // Iterate over all network properties
+ loop {
+ // Check if there are any properties left
+ trace!("props left: {}", property_list.len());
+ if property_list.is_empty() {
+ break;
+ }
+
+ let property = &property_list[0]; // Property we are currently iterating over
+ trace!("current prop: {}:{}", property.prop_id, property.value);
+
+ command.put_u8(property.prop_id as u8); // Property ID
+
+ // NOTE: THIS IS SUPER BAD DO NOT DO THIS! But it works!
+ if command_id == PROPUPD {
+ command.put_u8(PROPFLAG_DBSTORE as u8); // Flag (s)
+ command.put_u8(PROPACCESS_POSSESS as u8); // Access
+ }
+
+ command.put_u8(property.value.len() as u8); // Property UTF-8 Length
+ command.put_slice(property.value.as_bytes()); // Property UTF-8
+
+ property_list.reverse();
+ property_list.pop();
+ property_list.reverse();
+ }
+
+ // Convert to vector and insert the header
+ let mut command_as_vec = command.to_vec();
+
+ command_as_vec.insert(0, command_id as u8); // Command ID
+ command_as_vec.insert(0, obj_id as u8); // ObjId
+ command_as_vec.insert(0, command.len() as u8 + 3); // Data length
+
+ // Return bytes
+ command_as_vec
+}
diff --git a/crates/whirl_server/src/net/mod.rs b/crates/whirl_server/src/net/mod.rs
new file mode 100644
index 0000000..afa45c8
--- /dev/null
+++ b/crates/whirl_server/src/net/mod.rs
@@ -0,0 +1,7 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub mod constants;
+pub mod converter;
+pub mod property_parser;
+pub mod structure;
diff --git a/crates/whirl_server/src/net/property_parser.rs b/crates/whirl_server/src/net/property_parser.rs
new file mode 100644
index 0000000..f86b767
--- /dev/null
+++ b/crates/whirl_server/src/net/property_parser.rs
@@ -0,0 +1,38 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use std::str::from_utf8;
+
+use crate::net::structure::NetworkProperty;
+
+/// Iterate over a network property in the form of bytes and return a list of
+/// human-readable properties.
+pub fn parse_network_property(mut data: Vec<u8>) -> Vec<NetworkProperty> {
+ let mut property_list = vec![];
+
+ // Iterate over all network properties
+ loop {
+ // Check if any commands are present
+ if data.len() <= 2 {
+ break;
+ }
+ trace!("iteration: {:?}", data);
+ // if data[0] == 0 {
+ // break;
+ // }
+
+ let property_length = data[1] + 2;
+ property_list.push(NetworkProperty {
+ prop_id: data[0] as i32,
+ value: from_utf8(&data[2..data[1] as usize + 2])
+ .unwrap()
+ .to_string(),
+ });
+
+ // Remove current property from the network property
+ data = data[property_length as usize..].to_vec();
+ }
+
+ // Return the human-readable network property
+ property_list
+}
diff --git a/crates/whirl_server/src/net/structure.rs b/crates/whirl_server/src/net/structure.rs
new file mode 100644
index 0000000..1fb1051
--- /dev/null
+++ b/crates/whirl_server/src/net/structure.rs
@@ -0,0 +1,18 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+pub struct NetworkProperty {
+ pub prop_id: i32,
+ pub value: String,
+}
+impl NetworkProperty {
+ pub fn new() -> Self { NetworkProperty::default() }
+}
+impl Default for NetworkProperty {
+ fn default() -> Self {
+ NetworkProperty {
+ prop_id: 0,
+ value: "".to_string(),
+ }
+ }
+}
diff --git a/crates/whirl_server/src/packet_parser.rs b/crates/whirl_server/src/packet_parser.rs
new file mode 100644
index 0000000..bfeba9e
--- /dev/null
+++ b/crates/whirl_server/src/packet_parser.rs
@@ -0,0 +1,38 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use bytes::BytesMut;
+
+/// Read all commands from the given buffer.
+///
+/// # Process
+/// 1. Get a command from `buffer` based on first byte.
+/// 2. Push command to `commands`.
+/// 3. Remove command from `buffer`.
+/// 4. Iterate and do this for all commands within `buffer`.
+pub fn parse_commands_from_packet(mut buffer: BytesMut) -> Vec<BytesMut> {
+ let mut commands: Vec<BytesMut> = Vec::new();
+ trace!("initial buffer: {:?}, length: {}", buffer, buffer.len());
+
+ let data_length = buffer.get(0).unwrap().to_owned() as usize;
+ if buffer.len() > data_length {
+ loop {
+ trace!("loop: {:?}, length: {}", buffer, buffer.len());
+ let command_length = buffer.get(0).unwrap().to_owned() as usize;
+ commands.push(BytesMut::from(buffer.get(0..command_length).unwrap()));
+
+ // Remove command from buffer
+ buffer = buffer.split_off(command_length);
+
+ // Check if any more commands are present
+ if buffer.is_empty() {
+ break;
+ }
+ }
+ } else {
+ // There will always be at least one command, push it.
+ commands.push(BytesMut::from(buffer.get(0..data_length).unwrap()));
+ }
+
+ commands // Return command (s)
+}
diff --git a/crates/whirl_server/src/types.rs b/crates/whirl_server/src/types.rs
new file mode 100644
index 0000000..3d49752
--- /dev/null
+++ b/crates/whirl_server/src/types.rs
@@ -0,0 +1,8 @@
+// Copyleft (ɔ) 2021-2021 The Whirlsplash Collective
+// SPDX-License-Identifier: GPL-3.0-only
+
+use bytes::BytesMut;
+use tokio::sync::mpsc;
+
+pub type Tx = mpsc::UnboundedSender<BytesMut>;
+pub type Rx = mpsc::UnboundedReceiver<BytesMut>;