aboutsummaryrefslogtreecommitdiff
path: root/crates/whirl_server/src
diff options
context:
space:
mode:
authorFuwn <[email protected]>2021-05-20 17:05:59 -0700
committerFuwn <[email protected]>2021-05-20 17:05:59 -0700
commit9e2121baf98b6fdc15cde6c387a7845a0b3f95d6 (patch)
tree15460f59799a9f655ac5b213e4b8a8903d1e57e4 /crates/whirl_server/src
parentfeat(readme): add sqlfluff as a dev dep (diff)
downloadwhirl-9e2121baf98b6fdc15cde6c387a7845a0b3f95d6.tar.xz
whirl-9e2121baf98b6fdc15cde6c387a7845a0b3f95d6.zip
refactor(global): move crates around, stricter module isolation
Diffstat (limited to 'crates/whirl_server/src')
-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
30 files changed, 1348 insertions, 0 deletions
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>;